import { and, eq, isNull } from "drizzle-orm";
import { generateOpaqueToken, hashToken, verifyToken } from "@platform-modules/util/tokens";
import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { timingSafeEqual } from "@platform-modules/util/crypto";
//#region src/oauth-provider/errors.ts
var InvalidOAuthClientNameError = class extends Error {
	name = "InvalidOAuthClientNameError";
	code = "OAUTH_CLIENT_NAME_INVALID";
	constructor() {
		super("registerClient: name must be a non-empty string");
	}
};
var InvalidOAuthRedirectUrisError = class extends Error {
	name = "InvalidOAuthRedirectUrisError";
	code = "OAUTH_REDIRECT_URIS_INVALID";
	constructor() {
		super("registerClient: redirectUris must contain only absolute URLs");
	}
};
var InvalidOAuthScopesError = class extends Error {
	name = "InvalidOAuthScopesError";
	code = "OAUTH_SCOPES_INVALID";
	constructor() {
		super("registerClient: scopes must contain only non-empty strings");
	}
};
var OAuthClientNotFoundError = class extends Error {
	clientId;
	name = "OAuthClientNotFoundError";
	code = "OAUTH_CLIENT_NOT_FOUND";
	constructor(clientId) {
		super(`oauth client not found: ${clientId}`);
		this.clientId = clientId;
	}
};
var OAuthRedirectUriMismatchError = class extends Error {
	redirectUri;
	name = "OAuthRedirectUriMismatchError";
	code = "OAUTH_REDIRECT_URI_MISMATCH";
	constructor(redirectUri) {
		super(`oauth redirect URI mismatch: ${redirectUri}`);
		this.redirectUri = redirectUri;
	}
};
var InvalidOAuthScopeError = class extends Error {
	scope;
	name = "InvalidOAuthScopeError";
	code = "OAUTH_SCOPE_INVALID";
	constructor(scope) {
		super(`oauth scope is not allowed: ${scope}`);
		this.scope = scope;
	}
};
var UnsupportedCodeChallengeMethodError = class extends Error {
	codeChallengeMethod;
	name = "UnsupportedCodeChallengeMethodError";
	code = "OAUTH_UNSUPPORTED_CODE_CHALLENGE_METHOD";
	constructor(codeChallengeMethod) {
		super(`oauth code challenge method must be S256: ${codeChallengeMethod}`);
		this.codeChallengeMethod = codeChallengeMethod;
	}
};
var InvalidOAuthCodeChallengeError = class extends Error {
	name = "InvalidOAuthCodeChallengeError";
	code = "OAUTH_CODE_CHALLENGE_INVALID";
	constructor() {
		super("oauth code challenge must be a non-empty string");
	}
};
var OAuthCodeNotFoundError = class extends Error {
	name = "OAuthCodeNotFoundError";
	code = "OAUTH_CODE_NOT_FOUND";
	constructor() {
		super("oauth authorization code not found");
	}
};
var OAuthCodeAlreadyUsedError = class extends Error {
	name = "OAuthCodeAlreadyUsedError";
	code = "OAUTH_CODE_ALREADY_USED";
	constructor() {
		super("oauth authorization code has already been used");
	}
};
var OAuthCodeExpiredError = class extends Error {
	name = "OAuthCodeExpiredError";
	code = "OAUTH_CODE_EXPIRED";
	constructor() {
		super("oauth authorization code has expired");
	}
};
var OAuthCodePrincipalMissingError = class extends Error {
	name = "OAuthCodePrincipalMissingError";
	code = "OAUTH_CODE_PRINCIPAL_MISSING";
	constructor() {
		super("oauth authorization code has no valid persisted principal");
	}
};
var OAuthCodeClientMismatchError = class extends Error {
	clientId;
	name = "OAuthCodeClientMismatchError";
	code = "OAUTH_CODE_CLIENT_MISMATCH";
	constructor(clientId) {
		super(`oauth authorization code was not issued for client: ${clientId}`);
		this.clientId = clientId;
	}
};
var InvalidOAuthCodeVerifierError = class extends Error {
	name = "InvalidOAuthCodeVerifierError";
	code = "OAUTH_CODE_VERIFIER_INVALID";
	constructor() {
		super("oauth code verifier does not match the stored S256 challenge");
	}
};
var MissingOAuthClientSecretError = class extends Error {
	clientId;
	name = "MissingOAuthClientSecretError";
	code = "OAUTH_CLIENT_SECRET_MISSING";
	constructor(clientId) {
		super(`oauth client secret is required for confidential client: ${clientId}`);
		this.clientId = clientId;
	}
};
var InvalidOAuthClientSecretError = class extends Error {
	clientId;
	name = "InvalidOAuthClientSecretError";
	code = "OAUTH_CLIENT_SECRET_INVALID";
	constructor(clientId) {
		super(`oauth client secret is invalid for confidential client: ${clientId}`);
		this.clientId = clientId;
	}
};
function hasErrorShape(error, name, code) {
	return typeof error === "object" && error !== null && error.name === name && error.code === code;
}
function isOAuthProviderError(error) {
	return isInvalidOAuthClientNameError(error) || isInvalidOAuthRedirectUrisError(error) || isInvalidOAuthScopesError(error) || isOAuthClientNotFoundError(error) || isOAuthRedirectUriMismatchError(error) || isInvalidOAuthScopeError(error) || isUnsupportedCodeChallengeMethodError(error) || isInvalidOAuthCodeChallengeError(error) || isOAuthCodeNotFoundError(error) || isOAuthCodeAlreadyUsedError(error) || isOAuthCodeExpiredError(error) || isOAuthCodePrincipalMissingError(error) || isOAuthCodeClientMismatchError(error) || isInvalidOAuthCodeVerifierError(error) || isMissingOAuthClientSecretError(error) || isInvalidOAuthClientSecretError(error);
}
function isInvalidOAuthClientNameError(error) {
	return hasErrorShape(error, "InvalidOAuthClientNameError", "OAUTH_CLIENT_NAME_INVALID");
}
function isInvalidOAuthRedirectUrisError(error) {
	return hasErrorShape(error, "InvalidOAuthRedirectUrisError", "OAUTH_REDIRECT_URIS_INVALID");
}
function isInvalidOAuthScopesError(error) {
	return hasErrorShape(error, "InvalidOAuthScopesError", "OAUTH_SCOPES_INVALID");
}
function isOAuthClientNotFoundError(error) {
	return hasErrorShape(error, "OAuthClientNotFoundError", "OAUTH_CLIENT_NOT_FOUND");
}
function isOAuthRedirectUriMismatchError(error) {
	return hasErrorShape(error, "OAuthRedirectUriMismatchError", "OAUTH_REDIRECT_URI_MISMATCH");
}
function isInvalidOAuthScopeError(error) {
	return hasErrorShape(error, "InvalidOAuthScopeError", "OAUTH_SCOPE_INVALID");
}
function isUnsupportedCodeChallengeMethodError(error) {
	return hasErrorShape(error, "UnsupportedCodeChallengeMethodError", "OAUTH_UNSUPPORTED_CODE_CHALLENGE_METHOD");
}
function isInvalidOAuthCodeChallengeError(error) {
	return hasErrorShape(error, "InvalidOAuthCodeChallengeError", "OAUTH_CODE_CHALLENGE_INVALID");
}
function isOAuthCodeNotFoundError(error) {
	return hasErrorShape(error, "OAuthCodeNotFoundError", "OAUTH_CODE_NOT_FOUND");
}
function isOAuthCodeAlreadyUsedError(error) {
	return hasErrorShape(error, "OAuthCodeAlreadyUsedError", "OAUTH_CODE_ALREADY_USED");
}
function isOAuthCodeExpiredError(error) {
	return hasErrorShape(error, "OAuthCodeExpiredError", "OAUTH_CODE_EXPIRED");
}
function isOAuthCodePrincipalMissingError(error) {
	return hasErrorShape(error, "OAuthCodePrincipalMissingError", "OAUTH_CODE_PRINCIPAL_MISSING");
}
function isOAuthCodeClientMismatchError(error) {
	return hasErrorShape(error, "OAuthCodeClientMismatchError", "OAUTH_CODE_CLIENT_MISMATCH");
}
function isInvalidOAuthCodeVerifierError(error) {
	return hasErrorShape(error, "InvalidOAuthCodeVerifierError", "OAUTH_CODE_VERIFIER_INVALID");
}
function isMissingOAuthClientSecretError(error) {
	return hasErrorShape(error, "MissingOAuthClientSecretError", "OAUTH_CLIENT_SECRET_MISSING");
}
function isInvalidOAuthClientSecretError(error) {
	return hasErrorShape(error, "InvalidOAuthClientSecretError", "OAUTH_CLIENT_SECRET_INVALID");
}
//#endregion
//#region src/oauth-provider/pkce.ts
function toBase64Url(bytes) {
	let binary = "";
	for (const byte of bytes) binary += String.fromCharCode(byte);
	return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}
async function deriveCodeChallengeS256(codeVerifier) {
	const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
	return toBase64Url(new Uint8Array(digest));
}
async function verifyCodeVerifierS256(codeVerifier, codeChallenge) {
	return timingSafeEqual(await deriveCodeChallengeS256(codeVerifier), codeChallenge);
}
//#endregion
//#region src/oauth-provider/schema.ts
const oauthClients = pgTable("oauth_clients", {
	id: text("id").primaryKey(),
	name: text("name").notNull(),
	redirectUris: text("redirect_uris").notNull(),
	scopes: text("scopes").notNull(),
	confidential: boolean("confidential").notNull().default(false),
	clientSecretHash: text("client_secret_hash"),
	createdAt: timestamp("created_at", { withTimezone: true }).notNull()
});
const oauthCodes = pgTable("oauth_codes", {
	id: text("id").primaryKey(),
	clientId: text("client_id").notNull().references(() => oauthClients.id),
	redirectUri: text("redirect_uri").notNull(),
	scope: text("scope").notNull(),
	codeChallenge: text("code_challenge").notNull(),
	codeChallengeMethod: text("code_challenge_method").notNull(),
	codeHash: text("code_hash").notNull().unique(),
	subjectId: text("subject_id"),
	tenantId: text("tenant_id"),
	createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
	expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
	usedAt: timestamp("used_at", { withTimezone: true })
});
const oauthTokens = pgTable("oauth_tokens", {
	id: text("id").primaryKey(),
	clientId: text("client_id").notNull().references(() => oauthClients.id),
	codeId: text("code_id").notNull().references(() => oauthCodes.id),
	accessTokenPrefix: text("access_token_prefix").notNull().unique(),
	accessTokenHash: text("access_token_hash").notNull().unique(),
	scope: text("scope").notNull(),
	tokenType: text("token_type").notNull(),
	createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
	expiresAt: timestamp("expires_at", { withTimezone: true }).notNull()
});
const oauthProviderCreationSql = () => `CREATE TABLE IF NOT EXISTS oauth_clients (
    id text PRIMARY KEY,
    name text NOT NULL,
    redirect_uris text NOT NULL,
    scopes text NOT NULL,
    confidential boolean NOT NULL DEFAULT false,
    client_secret_hash text,
    created_at timestamptz NOT NULL
  );
  CREATE TABLE IF NOT EXISTS oauth_codes (
    id text PRIMARY KEY,
    client_id text NOT NULL REFERENCES oauth_clients(id),
    redirect_uri text NOT NULL,
    scope text NOT NULL,
    code_challenge text NOT NULL,
    code_challenge_method text NOT NULL,
    code_hash text NOT NULL UNIQUE,
    subject_id text NOT NULL,
    tenant_id text NOT NULL,
    created_at timestamptz NOT NULL,
    expires_at timestamptz NOT NULL,
    used_at timestamptz
  );
  CREATE TABLE IF NOT EXISTS oauth_tokens (
    id text PRIMARY KEY,
    client_id text NOT NULL REFERENCES oauth_clients(id),
    code_id text NOT NULL REFERENCES oauth_codes(id),
    access_token_prefix text NOT NULL UNIQUE,
    access_token_hash text NOT NULL UNIQUE,
    scope text NOT NULL,
    token_type text NOT NULL,
    created_at timestamptz NOT NULL,
    expires_at timestamptz NOT NULL
  )`;
const oauthProviderMigrationSql = () => `ALTER TABLE oauth_codes
    ADD COLUMN IF NOT EXISTS subject_id text,
    ADD COLUMN IF NOT EXISTS tenant_id text`;
const oauthProviderSchema = {
	oauthClients,
	oauthCodes,
	oauthTokens
};
//#endregion
//#region src/oauth-provider/index.ts
const ACCESS_TOKEN_TTL_SECS = 3600;
const AUTHORIZATION_CODE_TTL_MS = 3e5;
const ACCESS_TOKEN_PREFIX_BYTES = 9;
const ACCESS_TOKEN_SECRET_BYTES = 32;
const CLIENT_SECRET_BYTES = 32;
function normalizeName(name) {
	if (typeof name !== "string" || name.trim().length === 0) throw new InvalidOAuthClientNameError();
	return name.trim();
}
function isAbsoluteUrl(value) {
	try {
		new URL(value);
		return true;
	} catch {
		return false;
	}
}
function normalizeRedirectUris(redirectUris) {
	if (!Array.isArray(redirectUris) || redirectUris.length === 0 || redirectUris.some((redirectUri) => typeof redirectUri !== "string" || !isAbsoluteUrl(redirectUri))) throw new InvalidOAuthRedirectUrisError();
	return [...redirectUris];
}
function normalizeScopes(scopes) {
	if (!Array.isArray(scopes) || scopes.length === 0 || scopes.some((scope) => typeof scope !== "string" || scope.trim().length === 0)) throw new InvalidOAuthScopesError();
	return [...scopes];
}
function normalizeRequestedScope(scope, allowedScopes) {
	if (typeof scope !== "string" || scope.trim().length === 0) throw new InvalidOAuthScopeError(String(scope));
	const requestedScopes = scope.trim().split(/\s+/);
	for (const requestedScope of requestedScopes) if (!allowedScopes.includes(requestedScope)) throw new InvalidOAuthScopeError(scope);
	return requestedScopes.join(" ");
}
function normalizeCodeChallenge(codeChallenge) {
	if (typeof codeChallenge !== "string" || codeChallenge.trim().length === 0) throw new InvalidOAuthCodeChallengeError();
	return codeChallenge.trim();
}
function parseStringArray(raw) {
	try {
		const parsed = JSON.parse(raw);
		if (!Array.isArray(parsed) || parsed.some((value) => typeof value !== "string")) return null;
		return [...parsed];
	} catch {
		return null;
	}
}
function mapClientRow(row) {
	if (!row) return null;
	const redirectUris = parseStringArray(row.redirectUris);
	const scopes = parseStringArray(row.scopes);
	if (!redirectUris || !scopes) return null;
	return {
		id: row.id,
		name: row.name,
		redirectUris,
		scopes,
		confidential: row.confidential,
		createdAt: row.createdAt
	};
}
async function issueAccessToken(db, input) {
	const id = crypto.randomUUID();
	const prefix = generateOpaqueToken(ACCESS_TOKEN_PREFIX_BYTES);
	const secret = generateOpaqueToken(ACCESS_TOKEN_SECRET_BYTES);
	const accessTokenHash = await hashToken(secret);
	const expiresAt = new Date(input.now.getTime() + ACCESS_TOKEN_TTL_SECS * 1e3);
	await db.insert(oauthTokens).values({
		id,
		clientId: input.clientId,
		codeId: input.codeId,
		accessTokenPrefix: prefix,
		accessTokenHash,
		scope: input.scope,
		tokenType: "Bearer",
		createdAt: input.now,
		expiresAt
	});
	return {
		token: {
			accessToken: `${prefix}.${secret}`,
			tokenType: "Bearer",
			expiresIn: ACCESS_TOKEN_TTL_SECS,
			scope: input.scope
		},
		authorizationPrincipal: input.authorizationPrincipal
	};
}
function hasAuthorizationPrincipal(value) {
	if (typeof value !== "object" || value === null) return false;
	const principal = value;
	return typeof principal.subjectId === "string" && principal.subjectId.trim().length > 0 && typeof principal.tenantId === "string" && principal.tenantId.trim().length > 0;
}
async function registerClient(db, input) {
	const id = typeof input.id === "string" && input.id.trim().length > 0 ? input.id.trim() : crypto.randomUUID();
	const name = normalizeName(input.name);
	const redirectUris = normalizeRedirectUris(input.redirectUris);
	const scopes = normalizeScopes(input.scopes);
	const now = /* @__PURE__ */ new Date();
	const clientSecret = input.confidential ? generateOpaqueToken(CLIENT_SECRET_BYTES) : void 0;
	const clientSecretHash = clientSecret ? await hashToken(clientSecret) : null;
	await db.insert(oauthClients).values({
		id,
		name,
		redirectUris: JSON.stringify(redirectUris),
		scopes: JSON.stringify(scopes),
		confidential: input.confidential,
		clientSecretHash,
		createdAt: now
	});
	return {
		clientId: id,
		clientSecret
	};
}
async function getClient(db, id) {
	const [row] = await db.select({
		id: oauthClients.id,
		name: oauthClients.name,
		redirectUris: oauthClients.redirectUris,
		scopes: oauthClients.scopes,
		confidential: oauthClients.confidential,
		createdAt: oauthClients.createdAt
	}).from(oauthClients).where(eq(oauthClients.id, id)).limit(1);
	return mapClientRow(row);
}
async function authorize(db, input, authorizationPrincipal) {
	const principalSnapshot = {
		subjectId: authorizationPrincipal?.subjectId,
		tenantId: authorizationPrincipal?.tenantId
	};
	if (!hasAuthorizationPrincipal(principalSnapshot)) throw new OAuthCodePrincipalMissingError();
	if (input.codeChallengeMethod !== "S256") throw new UnsupportedCodeChallengeMethodError(String(input.codeChallengeMethod));
	const client = await getClient(db, input.clientId);
	if (!client) throw new OAuthClientNotFoundError(input.clientId);
	if (!client.redirectUris.includes(input.redirectUri)) throw new OAuthRedirectUriMismatchError(input.redirectUri);
	const scope = normalizeRequestedScope(input.scope, client.scopes);
	const codeChallenge = normalizeCodeChallenge(input.codeChallenge);
	const code = generateOpaqueToken();
	const codeHash = await hashToken(code);
	const now = /* @__PURE__ */ new Date();
	await db.insert(oauthCodes).values({
		id: crypto.randomUUID(),
		clientId: client.id,
		redirectUri: input.redirectUri,
		scope,
		codeChallenge,
		codeChallengeMethod: "S256",
		codeHash,
		subjectId: principalSnapshot.subjectId,
		tenantId: principalSnapshot.tenantId,
		createdAt: now,
		expiresAt: new Date(now.getTime() + AUTHORIZATION_CODE_TTL_MS),
		usedAt: null
	});
	return { code };
}
async function exchangeToken(db, input) {
	const codeHash = await hashToken(input.code);
	const [row] = await db.select({
		id: oauthCodes.id,
		clientId: oauthCodes.clientId,
		scope: oauthCodes.scope,
		codeChallenge: oauthCodes.codeChallenge,
		codeChallengeMethod: oauthCodes.codeChallengeMethod,
		redirectUri: oauthCodes.redirectUri,
		subjectId: oauthCodes.subjectId,
		tenantId: oauthCodes.tenantId,
		expiresAt: oauthCodes.expiresAt,
		usedAt: oauthCodes.usedAt,
		confidential: oauthClients.confidential,
		clientSecretHash: oauthClients.clientSecretHash
	}).from(oauthCodes).innerJoin(oauthClients, eq(oauthClients.id, oauthCodes.clientId)).where(eq(oauthCodes.codeHash, codeHash)).limit(1);
	if (!row) throw new OAuthCodeNotFoundError();
	if (row.clientId !== input.clientId) throw new OAuthCodeClientMismatchError(input.clientId);
	if (row.redirectUri !== input.redirectUri) throw new OAuthRedirectUriMismatchError(input.redirectUri);
	if (row.usedAt) throw new OAuthCodeAlreadyUsedError();
	const now = /* @__PURE__ */ new Date();
	if (row.expiresAt.getTime() <= now.getTime()) throw new OAuthCodeExpiredError();
	if (row.codeChallengeMethod !== "S256") throw new UnsupportedCodeChallengeMethodError(row.codeChallengeMethod);
	const authorizationPrincipal = {
		subjectId: row.subjectId,
		tenantId: row.tenantId
	};
	if (!hasAuthorizationPrincipal(authorizationPrincipal)) throw new OAuthCodePrincipalMissingError();
	if (!await verifyCodeVerifierS256(input.codeVerifier, row.codeChallenge)) throw new InvalidOAuthCodeVerifierError();
	if (row.confidential) {
		if (!input.clientSecret) throw new MissingOAuthClientSecretError(input.clientId);
		if (!row.clientSecretHash || !await verifyToken(input.clientSecret, row.clientSecretHash)) throw new InvalidOAuthClientSecretError(input.clientId);
	}
	return db.transaction(async (tx) => {
		if ((await tx.update(oauthCodes).set({ usedAt: now }).where(and(eq(oauthCodes.id, row.id), isNull(oauthCodes.usedAt))).returning({ id: oauthCodes.id })).length === 0) throw new OAuthCodeAlreadyUsedError();
		return issueAccessToken(tx, {
			clientId: row.clientId,
			codeId: row.id,
			scope: row.scope,
			now,
			authorizationPrincipal
		});
	});
}
//#endregion
export { InvalidOAuthClientNameError, InvalidOAuthClientSecretError, InvalidOAuthCodeChallengeError, InvalidOAuthCodeVerifierError, InvalidOAuthRedirectUrisError, InvalidOAuthScopeError, InvalidOAuthScopesError, MissingOAuthClientSecretError, OAuthClientNotFoundError, OAuthCodeAlreadyUsedError, OAuthCodeClientMismatchError, OAuthCodeExpiredError, OAuthCodeNotFoundError, OAuthCodePrincipalMissingError, OAuthRedirectUriMismatchError, UnsupportedCodeChallengeMethodError, authorize, deriveCodeChallengeS256, exchangeToken, getClient, isInvalidOAuthClientNameError, isInvalidOAuthClientSecretError, isInvalidOAuthCodeChallengeError, isInvalidOAuthCodeVerifierError, isInvalidOAuthRedirectUrisError, isInvalidOAuthScopeError, isInvalidOAuthScopesError, isMissingOAuthClientSecretError, isOAuthClientNotFoundError, isOAuthCodeAlreadyUsedError, isOAuthCodeClientMismatchError, isOAuthCodeExpiredError, isOAuthCodeNotFoundError, isOAuthCodePrincipalMissingError, isOAuthProviderError, isOAuthRedirectUriMismatchError, isUnsupportedCodeChallengeMethodError, oauthClients, oauthCodes, oauthProviderCreationSql, oauthProviderMigrationSql, oauthProviderSchema, oauthTokens, registerClient, verifyCodeVerifierS256 };
