export interface OidcMetadata { issuer: string; authorization_endpoint: string; token_endpoint: string; userinfo_endpoint?: string; jwks_uri: string; scopes_supported?: string[]; } function secureUrl(value: string, field: string): URL { const url = new URL(value); if (url.protocol !== "https:" && url.hostname !== "localhost") throw new Error(`WRN-IDENTITY-URL: ${field} must use HTTPS.`); return url; } export async function discoverOidc( issuer: string, options: { fetch?: typeof fetch } = {}, ): Promise { const expected = secureUrl(issuer, "issuer"); const endpoint = new URL( ".well-known/openid-configuration", `${expected.href.replace(/\/$/, "")}/`, ); const response = await (options.fetch ?? fetch)(endpoint, { headers: { accept: "application/json" }, }); if (!response.ok) throw new Error(`WRN-OIDC-DISCOVERY: provider returned ${response.status}.`); const metadata = (await response.json()) as OidcMetadata; if (metadata.issuer.replace(/\/$/, "") !== expected.href.replace(/\/$/, "")) throw new Error("WRN-OIDC-ISSUER: discovered issuer does not match configuration."); for (const field of ["authorization_endpoint", "token_endpoint", "jwks_uri"] as const) secureUrl(metadata[field], field); return metadata; } export function oidcAuthorizationUrl( metadata: OidcMetadata, input: { clientId: string; redirectUri: string; state: string; nonce: string; codeChallenge: string; scopes?: string[]; }, ): string { const url = secureUrl(metadata.authorization_endpoint, "authorization_endpoint"); const values = { response_type: "code", client_id: input.clientId, redirect_uri: input.redirectUri, scope: (input.scopes ?? ["openid", "profile", "email"]).join(" "), state: input.state, nonce: input.nonce, code_challenge: input.codeChallenge, code_challenge_method: "S256", }; for (const [key, value] of Object.entries(values)) url.searchParams.set(key, value); return url.href; } export interface EnterpriseIdentity { externalId: string; username: string; displayName?: string; email?: string; groups: string[]; active: boolean; attributes?: Record; } export interface SamlAssertion { id: string; issuer: string; audience: string; recipient: string; expiresAt: number; identity: EnterpriseIdentity; } export interface SamlAdapter { createLoginRequest(input: { requestId: string; callbackUrl: string; relayState: string; }): Promise | string; verifySignedResponse(response: string): Promise; } export interface ReplayStore { consume(id: string, expiresAt: number): Promise; } export function memoryReplayStore(now: () => number = Date.now): ReplayStore { const ids = new Map(); return { async consume(id, expiresAt) { for (const [key, expiry] of ids) if (expiry <= now()) ids.delete(key); if (ids.has(id)) return false; ids.set(id, expiresAt); return true; }, }; } export function createSamlFederation(options: { adapter: SamlAdapter; issuer: string; audience: string; recipient: string; replayStore?: ReplayStore; now?: () => number; }) { const replay = options.replayStore ?? memoryReplayStore(options.now); const now = options.now ?? Date.now; return { login: options.adapter.createLoginRequest.bind(options.adapter), async callback(encodedResponse: string): Promise { const assertion = await options.adapter.verifySignedResponse(encodedResponse); if ( assertion.issuer !== options.issuer || assertion.audience !== options.audience || assertion.recipient !== options.recipient ) throw new Error("WRN-SAML-BOUNDARY: issuer, audience, or recipient mismatch."); if (assertion.expiresAt <= now()) throw new Error("WRN-SAML-EXPIRED: assertion has expired."); if (!(await replay.consume(assertion.id, assertion.expiresAt))) throw new Error("WRN-SAML-REPLAY: assertion was already consumed."); return assertion.identity; }, }; } export interface DirectoryAdapter { kind: "ldap" | "active-directory"; search(input: { baseDn: string; filter: string; attributes: string[]; signal?: AbortSignal; }): Promise; authenticate?( username: string, password: string, signal?: AbortSignal, ): Promise; } export async function syncDirectory( adapter: DirectoryAdapter, options: { baseDn: string; filter?: string; attributes?: string[]; signal?: AbortSignal; upsert: (identity: EnterpriseIdentity) => void | Promise; disableMissing?: (externalIds: string[]) => void | Promise; }, ) { const identities = await adapter.search({ baseDn: options.baseDn, filter: options.filter ?? "(objectClass=person)", attributes: options.attributes ?? ["uid", "mail", "displayName", "memberOf"], signal: options.signal, }); for (const identity of identities) await options.upsert(identity); await options.disableMissing?.(identities.map((identity) => identity.externalId)); return { provider: adapter.kind, synchronized: identities.length }; } export interface ScimUser extends EnterpriseIdentity { id: string; /** RFC 7643 field accepted at the HTTP boundary. */ userName?: string; schemas?: string[]; } export interface ScimStore { list(): Promise; get(id: string): Promise; create(user: Omit): Promise; update(id: string, user: Partial): Promise; delete(id: string): Promise; } export function memoryScimStore(): ScimStore { const users = new Map(); return { async list() { return [...users.values()]; }, async get(id) { return users.get(id) ?? null; }, async create(user) { const value = { ...user, id: crypto.randomUUID() }; users.set(value.id, value); return value; }, async update(id, patch) { const current = users.get(id); if (!current) return null; const value = { ...current, ...patch, id }; users.set(id, value); return value; }, async delete(id) { return users.delete(id); }, }; } function constantTimeText(left: string, right: string): boolean { const a = new TextEncoder().encode(left); const b = new TextEncoder().encode(right); let mismatch = a.length ^ b.length; for (let index = 0; index < Math.max(a.length, b.length); index++) mismatch |= (a[index] ?? 0) ^ (b[index] ?? 0); return mismatch === 0; } const SCIM_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:ListResponse"; export function createScimHandler(options: { store: ScimStore; bearerToken: string; basePath?: string; maxBodyBytes?: number; }) { if (options.bearerToken.length < 24) throw new Error("WRN-SCIM-TOKEN: bearer token must contain at least 24 characters."); const base = options.basePath ?? "/scim/v2"; return async (request: Request): Promise => { if ( !constantTimeText(request.headers.get("authorization") ?? "", `Bearer ${options.bearerToken}`) ) return Response.json({ detail: "Unauthorized" }, { status: 401 }); const url = new URL(request.url); const match = new RegExp( `^${base.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/Users(?:/([^/]+))?$`, ).exec(url.pathname); if (!match) return Response.json({ detail: "Not found" }, { status: 404 }); const id = match[1]; if (request.method === "GET" && !id) { const values = await options.store.list(); return Response.json({ schemas: [SCIM_SCHEMA], totalResults: values.length, startIndex: 1, itemsPerPage: values.length, Resources: values, }); } if (request.method === "GET" && id) { const value = await options.store.get(id); return value ? Response.json(value) : Response.json({ detail: "Not found" }, { status: 404 }); } if (["POST", "PUT", "PATCH"].includes(request.method)) { const text = await request.text(); if (new TextEncoder().encode(text).byteLength > (options.maxBodyBytes ?? 64 * 1024)) return Response.json({ detail: "Too large" }, { status: 413 }); let body: ScimUser; try { body = JSON.parse(text) as ScimUser; } catch { return Response.json({ detail: "Invalid JSON" }, { status: 400 }); } if (!body || typeof body.userName !== "string") return Response.json({ detail: "userName is required" }, { status: 400 }); const normalized = { externalId: String(body.externalId ?? body.userName), username: body.userName, displayName: body.displayName, email: body.email, groups: Array.isArray(body.groups) ? body.groups : [], active: body.active !== false, attributes: body.attributes, }; const value = request.method === "POST" ? await options.store.create(normalized) : id ? await options.store.update(id, normalized) : null; return value ? Response.json(value, { status: request.method === "POST" ? 201 : 200 }) : Response.json({ detail: "Not found" }, { status: 404 }); } if (request.method === "DELETE" && id) return new Response(null, { status: (await options.store.delete(id)) ? 204 : 404 }); return new Response("Method Not Allowed", { status: 405 }); }; } export interface MachineCredential { id: string; ownerId: string; kind: "api-key" | "service-account"; name: string; scopes: string[]; secretHash: string; createdAt: number; expiresAt?: number; revokedAt?: number; } const hex = (bytes: Uint8Array) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); async function hash(value: string): Promise { return hex( new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))), ); } export function createMachineIdentityManager(now: () => number = Date.now) { const records = new Map(); return { async issue(input: { ownerId: string; name: string; scopes: string[]; kind?: MachineCredential["kind"]; expiresAt?: number; }) { const id = crypto.randomUUID(); const secret = `wrn_${id.replaceAll("-", "")}_${hex(crypto.getRandomValues(new Uint8Array(24)))}`; const record: MachineCredential = { id, ownerId: input.ownerId, kind: input.kind ?? "api-key", name: input.name, scopes: [...new Set(input.scopes)].sort(), secretHash: await hash(secret), createdAt: now(), expiresAt: input.expiresAt, }; records.set(id, record); return { secret, credential: { ...record, secretHash: "[REDACTED]" } }; }, async authenticate(secret: string, requiredScope?: string) { const digest = await hash(secret); for (const record of records.values()) if ( constantTimeText(record.secretHash, digest) && !record.revokedAt && (!record.expiresAt || record.expiresAt > now()) && (!requiredScope || record.scopes.includes(requiredScope) || record.scopes.includes("*")) ) return { ...record, secretHash: "[REDACTED]" }; return null; }, revoke(id: string) { const record = records.get(id); if (!record) return false; record.revokedAt = now(); return true; }, list(ownerId: string) { return [...records.values()] .filter((record) => record.ownerId === ownerId) .map((record) => ({ ...record, secretHash: "[REDACTED]" })); }, }; } export interface GovernanceEvent { id: string; type: string; subjectId: string; actorId?: string; createdAt: number; data?: Record; } export function createGovernance( options: { now?: () => number; audit?: (event: GovernanceEvent) => void | Promise; exportSubject?: (subjectId: string) => unknown | Promise; deleteSubject?: (subjectId: string) => void | Promise; } = {}, ) { const now = options.now ?? Date.now; const consents = new Map< string, Map >(); const approvals = new Map< string, { id: string; subjectId: string; action: "export" | "delete"; status: "pending" | "approved" | "rejected"; requestedAt: number; decidedAt?: number; decidedBy?: string; } >(); const emit = async ( type: string, subjectId: string, actorId?: string, data?: Record, ) => options.audit?.({ id: crypto.randomUUID(), type, subjectId, actorId, createdAt: now(), data }); return { async consent(subjectId: string, purpose: string, granted: boolean, version: string) { const values = consents.get(subjectId) ?? new Map(); const value = { granted, version, at: now() }; values.set(purpose, value); consents.set(subjectId, values); await emit("consent.changed", subjectId, subjectId, { purpose, granted, version }); return value; }, consents(subjectId: string) { return Object.fromEntries(consents.get(subjectId) ?? []); }, async request(subjectId: string, action: "export" | "delete") { const value = { id: crypto.randomUUID(), subjectId, action, status: "pending" as const, requestedAt: now(), }; approvals.set(value.id, value); await emit(`privacy.${action}.requested`, subjectId); return value; }, async decide(id: string, actorId: string, approved: boolean) { const request = approvals.get(id); if (!request || request.status !== "pending") throw new Error("WRN-GOVERNANCE-APPROVAL: request is missing or already decided."); const decision = { ...request, status: approved ? ("approved" as const) : ("rejected" as const), decidedAt: now(), decidedBy: actorId, }; approvals.set(id, decision); let result: unknown; if (approved && request.action === "export") result = await options.exportSubject?.(request.subjectId); if (approved && request.action === "delete") await options.deleteSubject?.(request.subjectId); await emit(`privacy.${request.action}.${decision.status}`, request.subjectId, actorId); return { decision, result }; }, async enforceRetention( records: Array<{ subjectId: string; createdAt: number }>, maxAgeMs: number, remove: (record: { subjectId: string; createdAt: number }) => void | Promise, ) { if (maxAgeMs < 0) throw new RangeError("retention duration must not be negative"); const expired = records.filter((record) => record.createdAt + maxAgeMs <= now()); for (const record of expired) { await remove(record); await emit("retention.deleted", record.subjectId, undefined, { createdAt: record.createdAt, }); } return expired.length; }, }; }