@broberg/sso 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/jwks.ts","../src/client.ts","../src/session.ts","../src/hono.ts"],"names":["importJWK","decodeProtectedHeader","jwtVerify","b64url","Hono","require"],"mappings":"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","file":"hono.cjs","sourcesContent":["/**\n * Configuration, read from the environment and nowhere else (F084.4 AC#5).\n *\n * An app mounts this package and sets env vars. It does not pass options in\n * code, because the moment configuration lives in code, two deployments of the\n * same app can disagree about who their identity provider is — and the symptom\n * is a token rejection nobody can trace back to a config line.\n */\n\nexport class SsoConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"SsoConfigError\";\n }\n}\n\nexport interface SsoConfig {\n /** Broberg ID's origin, e.g. https://id.broberg.ai — the BARE origin. */\n issuer: string;\n /** The client id this app was registered under (administratively, in BID). */\n clientId: string;\n /** Exactly the redirect registered in BID. Exact match — one character decides. */\n redirectUri: string;\n /** Requested scopes. */\n scopes: string[];\n /** Signs the local session cookie. 32+ bytes of randomness. */\n cookieSecret: string;\n /** Local session cookie name. */\n cookieName: string;\n /**\n * How long this app trusts its OWN session, in seconds.\n *\n * F084.7 decided the fleet's numbers: 7 days for a normal app, and 12 hours\n * + 30 minutes of inactivity for anything holding personal or health data.\n * The default here is the 7 days. An app handling patient data MUST set\n * SSO_SESSION_MAX_AGE=43200 — the default is a normal-app default, not a\n * safe-for-everything one.\n */\n sessionMaxAge: number;\n /** Where to send the browser after a logout completes. */\n postLogoutRedirectUri?: string;\n}\n\n/**\n * NOT `!value`. A blank string is falsy in JavaScript, so the obvious guard\n * lets `SSO_COOKIE_SECRET=\" \"` boot an app whose sessions are signed with\n * whitespace. This fleet has measured that exact defect before (components\n * F004.7), which is why it is a trim-and-compare rather than a truthiness test.\n */\nfunction required(env: NodeJS.ProcessEnv, name: string, hint: string): string {\n const raw = env[name];\n if (typeof raw !== \"string\" || raw.trim() === \"\") {\n throw new SsoConfigError(`${name} is not set (or is blank). ${hint}`);\n }\n return raw.trim();\n}\n\n/** Seconds in a week — the fleet default from F084.7. */\nexport const DEFAULT_SESSION_MAX_AGE = 60 * 60 * 24 * 7;\n\nexport function loadSsoConfig(env: NodeJS.ProcessEnv = process.env): SsoConfig {\n const rawIssuer = required(\n env,\n \"BID_ISSUER\",\n \"It is Broberg ID's bare origin, e.g. https://id.broberg.ai\",\n );\n\n let issuer: string;\n try {\n const url = new URL(rawIssuer);\n if (url.protocol !== \"https:\" && url.hostname !== \"localhost\") {\n throw new SsoConfigError(\n `BID_ISSUER must be https (got ${url.protocol}//). Only localhost may be http.`,\n );\n }\n // Normalised to the origin: a trailing slash makes it a DIFFERENT issuer to\n // a strict OIDC client, and the mismatch surfaces as an opaque token\n // rejection rather than as a configuration error anyone can read.\n issuer = url.origin;\n } catch (err) {\n if (err instanceof SsoConfigError) throw err;\n throw new SsoConfigError(`BID_ISSUER is not a valid URL: ${rawIssuer}`);\n }\n\n const cookieSecret = required(\n env,\n \"SSO_COOKIE_SECRET\",\n \"Generate one with `openssl rand -hex 32`. It signs this app's session cookie.\",\n );\n if (cookieSecret.length < 32) {\n throw new SsoConfigError(\n `SSO_COOKIE_SECRET is ${cookieSecret.length} characters; it must be at least 32. ` +\n \"A short secret is a forgeable session, and a forged session is any user you like.\",\n );\n }\n\n const rawMaxAge = env.SSO_SESSION_MAX_AGE?.trim();\n let sessionMaxAge = DEFAULT_SESSION_MAX_AGE;\n if (rawMaxAge) {\n const n = Number(rawMaxAge);\n // A non-number here would silently become NaN and then a cookie that\n // expires immediately — an app that cannot keep anyone logged in, with no\n // error anywhere. Refuse instead.\n if (!Number.isFinite(n) || n <= 0) {\n throw new SsoConfigError(\n `SSO_SESSION_MAX_AGE must be a positive number of seconds (got ${JSON.stringify(rawMaxAge)}).`,\n );\n }\n sessionMaxAge = Math.floor(n);\n }\n\n return {\n issuer,\n clientId: required(env, \"SSO_CLIENT_ID\", \"The id this app is registered under in Broberg ID.\"),\n redirectUri: required(\n env,\n \"SSO_REDIRECT_URI\",\n \"Must match the registered redirect EXACTLY — one trailing slash is a different address.\",\n ),\n scopes: (env.SSO_SCOPES?.trim() || \"openid profile email\").split(/\\s+/),\n cookieSecret,\n cookieName: env.SSO_COOKIE_NAME?.trim() || \"bid_session\",\n sessionMaxAge,\n postLogoutRedirectUri: env.SSO_POST_LOGOUT_REDIRECT_URI?.trim() || undefined,\n };\n}\n","/**\n * The signing-key cache.\n *\n * ── THE ONE BEHAVIOUR THIS FILE EXISTS FOR ────────────────────────────────\n *\n * It refetches on an UNKNOWN KEY ID, not on a timer (F084.4's constraint, and\n * it is the right one). An interval is a guess about when somebody else will\n * rotate their key; an unknown kid is the event itself. With an interval, the\n * window between \"BID rotated\" and \"the interval elapsed\" is a window where\n * every login in every app fails, and the length of that window is a number\n * nobody chose on purpose.\n *\n * ── AND THE PART AN OBVIOUS IMPLEMENTATION GETS WRONG ─────────────────────\n *\n * \"Unknown kid ⇒ refetch\" turns anyone who can send this app a token into\n * someone who can make it hammer BID: a stream of tokens with random kids is a\n * stream of fetches against the one service the whole fleet logs in through.\n * So a refetch is rate-limited by time. The cost of the floor is real and worth\n * stating: a rotation landing inside the cooldown makes logins fail for up to\n * that many milliseconds. Seconds of failure for one app beats a way to aim\n * traffic at BID from outside.\n */\n// jose 6 dropped the `KeyLike` alias; importJWK now answers\n// `CryptoKey | Uint8Array` directly. Taking the type FROM the function\n// rather than naming it means a future rename cannot silently widen it.\nimport { importJWK, type JWK } from \"jose\";\n\ntype SigningKey = Awaited<ReturnType<typeof importJWK>>;\n\nexport interface JwksCacheOptions {\n /** Absolute URL of the key set, taken from BID's discovery document. */\n jwksUri: string;\n /**\n * The floor between two refetches. Below it, an unknown kid is rejected\n * without asking BID again.\n */\n minRefetchIntervalMs?: number;\n /** Injectable for tests. */\n fetchImpl?: typeof fetch;\n /** Injectable for tests, so the cooldown can be exercised without waiting. */\n now?: () => number;\n}\n\nexport interface JwksCache {\n /** Resolve a key for this kid, refetching once if it is unknown. */\n getKey(kid: string, alg: string): Promise<SigningKey>;\n /** How many times the remote key set has actually been fetched. */\n readonly fetchCount: number;\n}\n\nexport class JwksError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"JwksError\";\n }\n}\n\nexport function createJwksCache(options: JwksCacheOptions): JwksCache {\n const {\n jwksUri,\n minRefetchIntervalMs = 10_000,\n fetchImpl = fetch,\n now = () => Date.now(),\n } = options;\n\n let keys: JWK[] = [];\n let lastFetchAt = -Infinity;\n let fetchCount = 0;\n /** Collapses concurrent refetches into one request. */\n let inFlight: Promise<void> | null = null;\n\n async function refresh(): Promise<void> {\n if (inFlight) return inFlight;\n inFlight = (async () => {\n const res = await fetchImpl(jwksUri);\n if (!res.ok) {\n throw new JwksError(`${jwksUri} answered ${res.status} — cannot verify any token`);\n }\n const body = (await res.json()) as { keys?: JWK[] };\n if (!Array.isArray(body.keys)) {\n throw new JwksError(`${jwksUri} returned no \"keys\" array`);\n }\n // Replace rather than merge. Merging would keep a REVOKED key usable\n // forever, which is the one thing rotating a key is meant to stop.\n keys = body.keys;\n lastFetchAt = now();\n fetchCount++;\n })().finally(() => {\n inFlight = null;\n });\n return inFlight;\n }\n\n return {\n get fetchCount() {\n return fetchCount;\n },\n\n async getKey(kid: string, alg: string) {\n let jwk = keys.find((k) => k.kid === kid);\n\n if (!jwk) {\n const sinceLast = now() - lastFetchAt;\n if (sinceLast < minRefetchIntervalMs) {\n throw new JwksError(\n `no signing key with kid ${kid}, and the key set was refreshed ${sinceLast}ms ago ` +\n `(floor is ${minRefetchIntervalMs}ms). Refusing to refetch — retry shortly.`,\n );\n }\n await refresh();\n jwk = keys.find((k) => k.kid === kid);\n }\n\n if (!jwk) {\n throw new JwksError(\n `Broberg ID does not publish a signing key with kid ${kid}. ` +\n `The token was not signed by this issuer.`,\n );\n }\n return importJWK(jwk, alg);\n },\n };\n}\n","/**\n * The core: framework-free, public-client OAuth 2.1 against Broberg ID.\n *\n * ── THIS PACKAGE IS A PUBLIC CLIENT. THERE IS NO CLIENT SECRET. ───────────\n *\n * Deliberate, and F084.4 AC#4 greps the published tarball to keep it that way.\n * PKCE alone carries the exchange. That is safe here for a reason worth stating\n * rather than assuming: BID matches redirect addresses EXACTLY (measured — one\n * trailing slash is refused), so the authorization code is delivered to this\n * app's own server and nowhere else. An attacker who knows the client id can\n * start a flow; they cannot receive its result.\n *\n * What it buys is the thing the card actually asks for: a secret that does not\n * exist cannot be committed, leaked in a log, copied into a second app, or left\n * behind in a repository someone later makes public.\n *\n * ── AND WHAT THIS PACKAGE MUST NEVER LEARN TO DO ──────────────────────────\n *\n * No passwords. No passkey registration. No social-provider keys. No email\n * verification. All of it lives in BID. A client that CAN do any of it is a\n * client somebody eventually uses to do it — and then the identity rules exist\n * in two places and drift.\n */\nimport { decodeProtectedHeader, jwtVerify, type JWTPayload } from \"jose\";\nimport type { SsoConfig } from \"./config.js\";\nimport { createJwksCache, type JwksCache } from \"./jwks.js\";\n\n/* ── discovery ───────────────────────────────────────────────────────────── */\n\nexport interface Discovery {\n issuer: string;\n authorization_endpoint: string;\n token_endpoint: string;\n jwks_uri: string;\n userinfo_endpoint?: string;\n end_session_endpoint?: string;\n}\n\nexport class SsoError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"SsoError\";\n }\n}\n\n/* ── PKCE ────────────────────────────────────────────────────────────────── */\n\nconst b64url = (bytes: ArrayBuffer | Uint8Array) => {\n const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);\n let s = \"\";\n for (const b of view) s += String.fromCharCode(b);\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n};\n\nconst randomToken = (bytes = 32) => b64url(crypto.getRandomValues(new Uint8Array(bytes)));\n\nasync function challengeFor(verifier: string): Promise<string> {\n return b64url(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(verifier)));\n}\n\n/* ── the shapes a caller handles ─────────────────────────────────────────── */\n\n/**\n * What `beginLogin` produces. Every field except `url` must survive the round\n * trip to BID and back — the adapter stores them in a short-lived cookie.\n *\n * They are NOT optional extras. `state` is what makes the callback provably the\n * answer to THIS request, and `nonce` is what stops a valid token minted for\n * some other login from being replayed into this one.\n */\nexport interface LoginStart {\n url: string;\n state: string;\n codeVerifier: string;\n nonce: string;\n}\n\nexport interface SsoClaims extends JWTPayload {\n sub: string;\n email?: string;\n name?: string;\n picture?: string;\n email_verified?: boolean;\n}\n\nexport interface LoginResult {\n claims: SsoClaims;\n idToken: string;\n accessToken?: string;\n refreshToken?: string;\n}\n\n/**\n * `prompt=none` asks BID to answer WITHOUT showing anything (F084.6).\n *\n * The delivery method decides whether it works, and this package only ever\n * produces a URL for a FULL TOP-LEVEL REDIRECT. It never returns anything an\n * app could put in a hidden iframe, because an iframe against the identity\n * provider is third-party context: Safari has blocked it for years and Chrome\n * is retiring it. That path works in testing on a Mac and fails for every user\n * on an iPhone — a failure that looks like \"you are not logged in\".\n */\nexport interface BeginLoginOptions {\n prompt?: \"none\" | \"login\" | \"consent\" | \"select_account\";\n /** Extra scopes for this one request, on top of the configured set. */\n scopes?: string[];\n}\n\nexport interface SsoClient {\n discovery(): Promise<Discovery>;\n beginLogin(options?: BeginLoginOptions): Promise<LoginStart>;\n completeLogin(input: {\n params: URLSearchParams;\n state: string;\n codeVerifier: string;\n nonce: string;\n }): Promise<LoginResult>;\n verifyIdToken(idToken: string, options?: { nonce?: string }): Promise<SsoClaims>;\n logoutUrl(options?: { idTokenHint?: string; postLogoutRedirectUri?: string }): Promise<string>;\n /** Exposed for tests and for a health check; not needed in normal use. */\n readonly jwks: JwksCache;\n}\n\nexport interface CreateSsoClientOptions {\n fetchImpl?: typeof fetch;\n /** Passed through to the key cache; see jwks.ts for why there is a floor. */\n minRefetchIntervalMs?: number;\n}\n\nexport function createSsoClient(\n config: SsoConfig,\n options: CreateSsoClientOptions = {},\n): SsoClient {\n const fetchImpl = options.fetchImpl ?? fetch;\n let discoveryPromise: Promise<Discovery> | null = null;\n let jwksCache: JwksCache | null = null;\n\n async function discovery(): Promise<Discovery> {\n discoveryPromise ??= (async () => {\n const url = `${config.issuer}/.well-known/openid-configuration`;\n const res = await fetchImpl(url);\n if (!res.ok) throw new SsoError(`${url} answered ${res.status}`);\n const doc = (await res.json()) as Discovery;\n\n // STRICT equality, and it is not pedantry. Better Auth's default basePath\n // advertised `<origin>/api/auth` as the issuer — measured on a running\n // BID during F084.1. If the document's issuer and our configured issuer\n // disagree, every token this app later verifies will be rejected for a\n // reason that reads as a signature problem. Refuse at boot instead.\n if (doc.issuer !== config.issuer) {\n throw new SsoError(\n `BID_ISSUER is ${config.issuer} but ${url} says its issuer is ${doc.issuer}. ` +\n `These must match exactly — tokens are validated against the issuer string.`,\n );\n }\n return doc;\n })().catch((err) => {\n discoveryPromise = null; // let a later call retry rather than cache a failure\n throw err;\n });\n return discoveryPromise;\n }\n\n async function keys(): Promise<JwksCache> {\n if (!jwksCache) {\n const { jwks_uri } = await discovery();\n jwksCache = createJwksCache({\n jwksUri: jwks_uri,\n fetchImpl,\n ...(options.minRefetchIntervalMs !== undefined\n ? { minRefetchIntervalMs: options.minRefetchIntervalMs }\n : {}),\n });\n }\n return jwksCache;\n }\n\n async function verifyIdToken(idToken: string, opts: { nonce?: string } = {}) {\n const cache = await keys();\n const header = decodeProtectedHeader(idToken);\n if (!header.kid) throw new SsoError(\"ID token has no kid — cannot pick a signing key\");\n\n const { payload } = await jwtVerify(\n idToken,\n async () => cache.getKey(header.kid!, header.alg ?? \"RS256\"),\n { issuer: config.issuer, audience: config.clientId },\n );\n\n if (opts.nonce !== undefined && payload.nonce !== opts.nonce) {\n throw new SsoError(\n \"ID token nonce does not match this login request — refusing a token minted for another sign-in.\",\n );\n }\n if (typeof payload.sub !== \"string\" || payload.sub === \"\") {\n throw new SsoError(\"ID token has no sub — there is no user to be\");\n }\n return payload as SsoClaims;\n }\n\n /**\n * Fetch the profile claims and fold them in.\n *\n * NOT an optimisation — it is the only way to learn a user's name. MEASURED\n * against the live Broberg ID on 16 Sep 2026:\n *\n * ID token iss · sub · aud · iat · exp · auth_time · acr · at_hash\n * userinfo sub · name · given_name · family_name · email · email_verified\n *\n * That is correct OIDC for an authorization-code flow: profile claims belong\n * to the userinfo endpoint, and an ID token proving WHO you are does not have\n * to say what you are called. An app that only reads the ID token gets a\n * signed identity and a blank name — which is exactly what the first run of\n * the example app showed on screen.\n *\n * ── THE CHECK A NAIVE VERSION SKIPS ───────────────────────────────────────\n *\n * The `sub` from userinfo MUST equal the `sub` in the ID token (OIDC Core\n * 5.3.2 says MUST). Without it, a userinfo response for a DIFFERENT user\n * would be merged over a correctly verified identity — the app would show,\n * and act as, somebody else, with a valid signature underneath. A mismatch is\n * refused outright rather than reconciled.\n */\n async function withUserInfo(claims: SsoClaims, accessToken: string): Promise<SsoClaims> {\n const doc = await discovery();\n if (!doc.userinfo_endpoint) return claims;\n\n const res = await fetchImpl(doc.userinfo_endpoint, {\n headers: { authorization: `Bearer ${accessToken}` },\n });\n // A failure here must NOT lose the sign-in: the identity is already proven\n // by the ID token. The user ends up with a session and no display name,\n // which is worse than having one and far better than being logged out.\n if (!res.ok) return claims;\n\n const info = (await res.json()) as Record<string, unknown>;\n if (info.sub !== claims.sub) {\n throw new SsoError(\n `userinfo describes ${String(info.sub)} but the ID token is for ${claims.sub} — ` +\n `refusing to merge another user's profile onto this session.`,\n );\n }\n return { ...claims, ...info, sub: claims.sub };\n }\n\n return {\n discovery,\n get jwks() {\n if (!jwksCache) throw new SsoError(\"the key cache is created on first verification\");\n return jwksCache;\n },\n verifyIdToken,\n\n async beginLogin(opts: BeginLoginOptions = {}): Promise<LoginStart> {\n const { authorization_endpoint } = await discovery();\n const codeVerifier = randomToken();\n const state = randomToken(16);\n const nonce = randomToken(16);\n\n const params = new URLSearchParams({\n response_type: \"code\",\n client_id: config.clientId,\n redirect_uri: config.redirectUri,\n scope: [...new Set([...config.scopes, ...(opts.scopes ?? [])])].join(\" \"),\n state,\n nonce,\n code_challenge: await challengeFor(codeVerifier),\n code_challenge_method: \"S256\",\n });\n if (opts.prompt) params.set(\"prompt\", opts.prompt);\n\n return { url: `${authorization_endpoint}?${params}`, state, codeVerifier, nonce };\n },\n\n async completeLogin({ params, state, codeVerifier, nonce }) {\n // An ERROR comes back on the same redirect as a success — BID answers a\n // refused `prompt=none` by redirecting here with ?error=login_required.\n // Reading only for `code` would make a refusal look like a malformed\n // response instead of the expected answer it is.\n const error = params.get(\"error\");\n if (error) {\n throw new SsoError(\n `Broberg ID refused this login: ${error}` +\n (params.get(\"error_description\") ? ` — ${params.get(\"error_description\")}` : \"\"),\n );\n }\n\n const returned = params.get(\"state\");\n if (!returned || returned !== state) {\n throw new SsoError(\n \"state does not match the login this browser started — refusing the callback.\",\n );\n }\n\n const code = params.get(\"code\");\n if (!code) throw new SsoError(\"callback carried neither an error nor a code\");\n\n const { token_endpoint } = await discovery();\n const res = await fetchImpl(token_endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n grant_type: \"authorization_code\",\n code,\n redirect_uri: config.redirectUri,\n client_id: config.clientId,\n code_verifier: codeVerifier,\n }),\n });\n\n const body = (await res.json()) as {\n id_token?: string;\n access_token?: string;\n refresh_token?: string;\n error?: string;\n error_description?: string;\n };\n if (!res.ok || !body.id_token) {\n throw new SsoError(\n `token exchange failed (${res.status}): ${body.error ?? \"no id_token in response\"}` +\n (body.error_description ? ` — ${body.error_description}` : \"\"),\n );\n }\n\n const claims = await verifyIdToken(body.id_token, { nonce });\n\n return {\n claims: body.access_token ? await withUserInfo(claims, body.access_token) : claims,\n idToken: body.id_token,\n ...(body.access_token ? { accessToken: body.access_token } : {}),\n ...(body.refresh_token ? { refreshToken: body.refresh_token } : {}),\n };\n },\n\n async logoutUrl(opts = {}) {\n const doc = await discovery();\n if (!doc.end_session_endpoint) {\n throw new SsoError(\n `${config.issuer} does not advertise end_session_endpoint — central logout is unavailable.`,\n );\n }\n const params = new URLSearchParams();\n if (opts.idTokenHint) params.set(\"id_token_hint\", opts.idTokenHint);\n const post = opts.postLogoutRedirectUri ?? config.postLogoutRedirectUri;\n if (post) params.set(\"post_logout_redirect_uri\", post);\n params.set(\"client_id\", config.clientId);\n return `${doc.end_session_endpoint}?${params}`;\n },\n };\n}\n","/**\n * The app's OWN session cookie — signed, not encrypted.\n *\n * Signed is the right choice and the distinction matters: the contents are not\n * secret (a user may read their own id and name), but they must not be\n * FORGEABLE. Encryption would hide a subject id the user already knows while\n * doing nothing extra about forgery, which is the actual risk.\n *\n * What goes in is deliberately small: who you are and when this stops being\n * true. Claims that can change — a name, a role, a picture — belong in the\n * app's own store keyed by `sub`, because a cookie is a cache nobody can\n * invalidate, and a stale role in a cookie is a permission that outlives its\n * revocation.\n */\n\nexport interface SessionPayload {\n /** The subject from Broberg ID. The one stable identifier. */\n sub: string;\n /** Unix seconds. Checked on every read. */\n exp: number;\n /** Optional convenience copies; never authorisation data. */\n email?: string;\n name?: string;\n}\n\nconst enc = new TextEncoder();\n\nconst b64url = (bytes: Uint8Array) => {\n let s = \"\";\n for (const b of bytes) s += String.fromCharCode(b);\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n};\n\nconst fromB64url = (s: string) => {\n const pad = s.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const bin = atob(pad + \"=\".repeat((4 - (pad.length % 4)) % 4));\n return Uint8Array.from(bin, (c) => c.charCodeAt(0));\n};\n\nasync function hmacKey(secret: string) {\n return crypto.subtle.importKey(\"raw\", enc.encode(secret), { name: \"HMAC\", hash: \"SHA-256\" }, false, [\n \"sign\",\n \"verify\",\n ]);\n}\n\n/**\n * Sign an arbitrary string. The primitive underneath BOTH cookies this package\n * sets — the session and the short-lived login transaction.\n *\n * They are separate functions on purpose. The first version of this file had\n * the transaction ride inside the session envelope with `exp: 0`, and the\n * expiry check (`exp * 1000 <= now()`) then rejected it every single time: the\n * transaction cookie could never be read back, so every login would have failed\n * with \"state does not match\" — a message pointing at the wrong thing entirely.\n * Two different lifetimes wanted two different envelopes, not one envelope with\n * a sentinel in it.\n */\nexport async function signValue(value: string, secret: string): Promise<string> {\n const body = b64url(enc.encode(value));\n const sig = new Uint8Array(await crypto.subtle.sign(\"HMAC\", await hmacKey(secret), enc.encode(body)));\n return `${body}.${b64url(sig)}`;\n}\n\n/** Verify the SIGNATURE only, returning the original string. No expiry notion. */\nexport async function verifyValue(\n token: string | undefined | null,\n secret: string,\n): Promise<string | null> {\n if (!token) return null;\n const dot = token.lastIndexOf(\".\");\n if (dot <= 0) return null;\n const body = token.slice(0, dot);\n try {\n const ok = await crypto.subtle.verify(\n \"HMAC\",\n await hmacKey(secret),\n fromB64url(token.slice(dot + 1)),\n enc.encode(body),\n );\n if (!ok) return null;\n return new TextDecoder().decode(fromB64url(body));\n } catch {\n return null;\n }\n}\n\nexport async function signSession(payload: SessionPayload, secret: string): Promise<string> {\n return signValue(JSON.stringify(payload), secret);\n}\n\n/**\n * Returns null for ANY reason the cookie cannot be trusted — tampered,\n * truncated, wrong secret, expired, or simply not one of ours.\n *\n * Deliberately one return value rather than distinguishing them to the caller:\n * an app that can tell \"bad signature\" from \"expired\" will eventually branch on\n * it, and there is no branch where a forged cookie should do anything other\n * than what an absent one does.\n */\nexport async function verifySession(\n token: string | undefined | null,\n secret: string,\n now: () => number = Date.now,\n): Promise<SessionPayload | null> {\n // crypto.subtle.verify is constant-time; a hand-rolled string compare would\n // leak the signature one byte at a time.\n const body = await verifyValue(token, secret);\n if (body === null) return null;\n\n try {\n const payload = JSON.parse(body) as SessionPayload;\n if (typeof payload.sub !== \"string\" || payload.sub === \"\") return null;\n if (typeof payload.exp !== \"number\" || payload.exp * 1000 <= now()) return null;\n return payload;\n } catch {\n return null;\n }\n}\n\n/** Serialise a Set-Cookie value. `secure` is off only for http://localhost. */\nexport function cookieHeader(\n name: string,\n value: string,\n opts: { maxAge: number; secure: boolean; sameSite?: \"Lax\" | \"Strict\"; path?: string },\n): string {\n const parts = [\n `${name}=${value}`,\n `Path=${opts.path ?? \"/\"}`,\n `Max-Age=${opts.maxAge}`,\n \"HttpOnly\",\n // Lax, NOT Strict, and this is load-bearing: the callback from Broberg ID\n // is a top-level GET navigation from another site. Strict withholds the\n // cookie on exactly that navigation, so the login transaction cookie would\n // be missing when it is needed and every sign-in would fail with \"state\n // does not match\" — a message that sends you looking in the wrong place.\n `SameSite=${opts.sameSite ?? \"Lax\"}`,\n ];\n if (opts.secure) parts.push(\"Secure\");\n return parts.join(\"; \");\n}\n\nexport function readCookie(header: string | null | undefined, name: string): string | undefined {\n if (!header) return undefined;\n for (const part of header.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq === -1) continue;\n if (part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();\n }\n return undefined;\n}\n","/**\n * The Hono adapter — mounting, and nothing else.\n *\n * Every rule about what a valid session IS lives in the core (client.ts,\n * session.ts). This file only turns those into routes. F084.5 adds a Next.js\n * adapter on the SAME core, because a rule implemented twice is a rule that\n * eventually says two things.\n */\nimport { Hono, type Context, type MiddlewareHandler } from \"hono\";\nimport { loadSsoConfig, type SsoConfig } from \"./config.js\";\nimport { createSsoClient, type SsoClient } from \"./client.js\";\nimport {\n cookieHeader,\n readCookie,\n signSession,\n signValue,\n verifySession,\n verifyValue,\n type SessionPayload,\n} from \"./session.js\";\n\n/** Where the adapter parks the session on the request, for `getSession`. */\nconst SESSION_KEY = \"bidSession\";\n\n/** Short-lived cookie holding the in-flight login. Five minutes is plenty for\n * a person to type a password; longer just widens the window. */\nconst TRANSACTION_MAX_AGE = 300;\n\nexport interface SsoRoutesOptions {\n config?: SsoConfig;\n client?: SsoClient;\n /** Path prefix these routes are mounted under. Used to build the return URL. */\n loginPath?: string;\n /** Where to send a user after a successful login when no returnTo is given. */\n defaultReturnTo?: string;\n}\n\nfunction isSecure(c: Context): boolean {\n // localhost over http is the one case where a Secure cookie would simply\n // never be stored, making local development impossible for a reason no error\n // would explain.\n return new URL(c.req.url).protocol === \"https:\";\n}\n\n/**\n * Only same-origin, path-only return targets are honoured.\n *\n * Without this, `/auth/login?returnTo=https://evil.example` turns this app's\n * own login into an open redirect that arrives wearing our domain — the\n * classic phishing lever, and it is one line to close.\n */\nfunction safeReturnTo(raw: string | undefined, fallback: string): string {\n if (!raw) return fallback;\n if (!raw.startsWith(\"/\") || raw.startsWith(\"//\")) return fallback;\n return raw;\n}\n\nexport function ssoRoutes(options: SsoRoutesOptions = {}) {\n const config = options.config ?? loadSsoConfig();\n const client = options.client ?? createSsoClient(config);\n const loginPath = options.loginPath ?? \"/auth\";\n const defaultReturnTo = options.defaultReturnTo ?? \"/\";\n const txCookie = `${config.cookieName}_tx`;\n\n const app = new Hono();\n\n app.get(\"/login\", async (c) => {\n const prompt = c.req.query(\"prompt\");\n const start = await client.beginLogin(\n prompt === \"none\" ? { prompt: \"none\" } : {},\n );\n const tx = JSON.stringify({\n state: start.state,\n codeVerifier: start.codeVerifier,\n nonce: start.nonce,\n returnTo: safeReturnTo(c.req.query(\"returnTo\"), defaultReturnTo),\n });\n c.header(\n \"Set-Cookie\",\n cookieHeader(txCookie, await signValue(tx, config.cookieSecret), {\n maxAge: TRANSACTION_MAX_AGE,\n secure: isSecure(c),\n }),\n );\n // A FULL top-level redirect. Never a hidden iframe — see client.ts.\n return c.redirect(start.url, 302);\n });\n\n app.get(\"/callback\", async (c) => {\n const parsed = await parseTransaction(\n readCookie(c.req.header(\"cookie\"), txCookie),\n config.cookieSecret,\n );\n if (!parsed) {\n return c.json(\n { error: \"no_login_in_progress\", message: \"This browser did not start a login here, or it expired.\" },\n 400,\n );\n }\n\n const result = await client.completeLogin({\n params: new URL(c.req.url).searchParams,\n state: parsed.state,\n codeVerifier: parsed.codeVerifier,\n nonce: parsed.nonce,\n });\n\n const exp = Math.floor(Date.now() / 1000) + config.sessionMaxAge;\n const session: SessionPayload = {\n sub: result.claims.sub,\n exp,\n ...(result.claims.email ? { email: result.claims.email } : {}),\n ...(result.claims.name ? { name: result.claims.name } : {}),\n };\n\n c.header(\n \"Set-Cookie\",\n cookieHeader(config.cookieName, await signSession(session, config.cookieSecret), {\n maxAge: config.sessionMaxAge,\n secure: isSecure(c),\n }),\n );\n // Clear the transaction cookie: it has done its job, and a replayed one is\n // only ever useful to someone who should not have it.\n c.header(\n \"Set-Cookie\",\n cookieHeader(txCookie, \"\", { maxAge: 0, secure: isSecure(c) }),\n { append: true },\n );\n return c.redirect(parsed.returnTo, 302);\n });\n\n app.get(\"/logout\", async (c) => {\n c.header(\n \"Set-Cookie\",\n cookieHeader(config.cookieName, \"\", { maxAge: 0, secure: isSecure(c) }),\n );\n // Local cookie cleared FIRST, then central logout. If the redirect to BID\n // fails or the user closes the tab, the worst case is \"signed out here but\n // not everywhere\" — never the reverse, which would leave this app trusting\n // a session the user believes is gone.\n return c.redirect(await client.logoutUrl(), 302);\n });\n\n /** Reads the session and puts it on the context. Never blocks. */\n const attach: MiddlewareHandler = async (c, next) => {\n const cookie = readCookie(c.req.header(\"cookie\"), config.cookieName);\n c.set(SESSION_KEY, await verifySession(cookie, config.cookieSecret));\n await next();\n };\n\n /** Blocks and redirects to login, preserving where the user was going. */\n const require: MiddlewareHandler = async (c, next) => {\n const cookie = readCookie(c.req.header(\"cookie\"), config.cookieName);\n const session = await verifySession(cookie, config.cookieSecret);\n if (!session) {\n const url = new URL(c.req.url);\n return c.redirect(\n `${loginPath}/login?returnTo=${encodeURIComponent(url.pathname + url.search)}`,\n 302,\n );\n }\n c.set(SESSION_KEY, session);\n await next();\n };\n\n return { app, attach, require, client, config };\n}\n\n/**\n * Reads the login transaction back out of its signed cookie.\n *\n * Signature only — the transaction's lifetime is the cookie's own Max-Age, not\n * a claim inside it. That is why it does NOT go through verifySession: an\n * envelope carrying an `exp` would need one, and the first version of this file\n * used `exp: 0`, which the expiry check rejected every time.\n */\nasync function parseTransaction(raw: string | undefined, secret: string) {\n const body = await verifyValue(raw, secret);\n if (body === null) return null;\n try {\n const tx = JSON.parse(body) as {\n state: string;\n codeVerifier: string;\n nonce: string;\n returnTo: string;\n };\n if (!tx.state || !tx.codeVerifier || !tx.nonce) return null;\n return tx;\n } catch {\n return null;\n }\n}\n\n/** Read the session a middleware attached. */\nexport function getSession(c: Context): SessionPayload | null {\n return (c.get(SESSION_KEY) as SessionPayload | null) ?? null;\n}\n"]}
@@ -0,0 +1,199 @@
1
+ import * as hono_types from 'hono/types';
2
+ import { Context, Hono, MiddlewareHandler } from 'hono';
3
+ import { importJWK, JWTPayload } from 'jose';
4
+
5
+ interface SsoConfig {
6
+ /** Broberg ID's origin, e.g. https://id.broberg.ai — the BARE origin. */
7
+ issuer: string;
8
+ /** The client id this app was registered under (administratively, in BID). */
9
+ clientId: string;
10
+ /** Exactly the redirect registered in BID. Exact match — one character decides. */
11
+ redirectUri: string;
12
+ /** Requested scopes. */
13
+ scopes: string[];
14
+ /** Signs the local session cookie. 32+ bytes of randomness. */
15
+ cookieSecret: string;
16
+ /** Local session cookie name. */
17
+ cookieName: string;
18
+ /**
19
+ * How long this app trusts its OWN session, in seconds.
20
+ *
21
+ * F084.7 decided the fleet's numbers: 7 days for a normal app, and 12 hours
22
+ * + 30 minutes of inactivity for anything holding personal or health data.
23
+ * The default here is the 7 days. An app handling patient data MUST set
24
+ * SSO_SESSION_MAX_AGE=43200 — the default is a normal-app default, not a
25
+ * safe-for-everything one.
26
+ */
27
+ sessionMaxAge: number;
28
+ /** Where to send the browser after a logout completes. */
29
+ postLogoutRedirectUri?: string;
30
+ }
31
+
32
+ /**
33
+ * The signing-key cache.
34
+ *
35
+ * ── THE ONE BEHAVIOUR THIS FILE EXISTS FOR ────────────────────────────────
36
+ *
37
+ * It refetches on an UNKNOWN KEY ID, not on a timer (F084.4's constraint, and
38
+ * it is the right one). An interval is a guess about when somebody else will
39
+ * rotate their key; an unknown kid is the event itself. With an interval, the
40
+ * window between "BID rotated" and "the interval elapsed" is a window where
41
+ * every login in every app fails, and the length of that window is a number
42
+ * nobody chose on purpose.
43
+ *
44
+ * ── AND THE PART AN OBVIOUS IMPLEMENTATION GETS WRONG ─────────────────────
45
+ *
46
+ * "Unknown kid ⇒ refetch" turns anyone who can send this app a token into
47
+ * someone who can make it hammer BID: a stream of tokens with random kids is a
48
+ * stream of fetches against the one service the whole fleet logs in through.
49
+ * So a refetch is rate-limited by time. The cost of the floor is real and worth
50
+ * stating: a rotation landing inside the cooldown makes logins fail for up to
51
+ * that many milliseconds. Seconds of failure for one app beats a way to aim
52
+ * traffic at BID from outside.
53
+ */
54
+
55
+ type SigningKey = Awaited<ReturnType<typeof importJWK>>;
56
+ interface JwksCache {
57
+ /** Resolve a key for this kid, refetching once if it is unknown. */
58
+ getKey(kid: string, alg: string): Promise<SigningKey>;
59
+ /** How many times the remote key set has actually been fetched. */
60
+ readonly fetchCount: number;
61
+ }
62
+
63
+ /**
64
+ * The core: framework-free, public-client OAuth 2.1 against Broberg ID.
65
+ *
66
+ * ── THIS PACKAGE IS A PUBLIC CLIENT. THERE IS NO CLIENT SECRET. ───────────
67
+ *
68
+ * Deliberate, and F084.4 AC#4 greps the published tarball to keep it that way.
69
+ * PKCE alone carries the exchange. That is safe here for a reason worth stating
70
+ * rather than assuming: BID matches redirect addresses EXACTLY (measured — one
71
+ * trailing slash is refused), so the authorization code is delivered to this
72
+ * app's own server and nowhere else. An attacker who knows the client id can
73
+ * start a flow; they cannot receive its result.
74
+ *
75
+ * What it buys is the thing the card actually asks for: a secret that does not
76
+ * exist cannot be committed, leaked in a log, copied into a second app, or left
77
+ * behind in a repository someone later makes public.
78
+ *
79
+ * ── AND WHAT THIS PACKAGE MUST NEVER LEARN TO DO ──────────────────────────
80
+ *
81
+ * No passwords. No passkey registration. No social-provider keys. No email
82
+ * verification. All of it lives in BID. A client that CAN do any of it is a
83
+ * client somebody eventually uses to do it — and then the identity rules exist
84
+ * in two places and drift.
85
+ */
86
+
87
+ interface Discovery {
88
+ issuer: string;
89
+ authorization_endpoint: string;
90
+ token_endpoint: string;
91
+ jwks_uri: string;
92
+ userinfo_endpoint?: string;
93
+ end_session_endpoint?: string;
94
+ }
95
+ /**
96
+ * What `beginLogin` produces. Every field except `url` must survive the round
97
+ * trip to BID and back — the adapter stores them in a short-lived cookie.
98
+ *
99
+ * They are NOT optional extras. `state` is what makes the callback provably the
100
+ * answer to THIS request, and `nonce` is what stops a valid token minted for
101
+ * some other login from being replayed into this one.
102
+ */
103
+ interface LoginStart {
104
+ url: string;
105
+ state: string;
106
+ codeVerifier: string;
107
+ nonce: string;
108
+ }
109
+ interface SsoClaims extends JWTPayload {
110
+ sub: string;
111
+ email?: string;
112
+ name?: string;
113
+ picture?: string;
114
+ email_verified?: boolean;
115
+ }
116
+ interface LoginResult {
117
+ claims: SsoClaims;
118
+ idToken: string;
119
+ accessToken?: string;
120
+ refreshToken?: string;
121
+ }
122
+ /**
123
+ * `prompt=none` asks BID to answer WITHOUT showing anything (F084.6).
124
+ *
125
+ * The delivery method decides whether it works, and this package only ever
126
+ * produces a URL for a FULL TOP-LEVEL REDIRECT. It never returns anything an
127
+ * app could put in a hidden iframe, because an iframe against the identity
128
+ * provider is third-party context: Safari has blocked it for years and Chrome
129
+ * is retiring it. That path works in testing on a Mac and fails for every user
130
+ * on an iPhone — a failure that looks like "you are not logged in".
131
+ */
132
+ interface BeginLoginOptions {
133
+ prompt?: "none" | "login" | "consent" | "select_account";
134
+ /** Extra scopes for this one request, on top of the configured set. */
135
+ scopes?: string[];
136
+ }
137
+ interface SsoClient {
138
+ discovery(): Promise<Discovery>;
139
+ beginLogin(options?: BeginLoginOptions): Promise<LoginStart>;
140
+ completeLogin(input: {
141
+ params: URLSearchParams;
142
+ state: string;
143
+ codeVerifier: string;
144
+ nonce: string;
145
+ }): Promise<LoginResult>;
146
+ verifyIdToken(idToken: string, options?: {
147
+ nonce?: string;
148
+ }): Promise<SsoClaims>;
149
+ logoutUrl(options?: {
150
+ idTokenHint?: string;
151
+ postLogoutRedirectUri?: string;
152
+ }): Promise<string>;
153
+ /** Exposed for tests and for a health check; not needed in normal use. */
154
+ readonly jwks: JwksCache;
155
+ }
156
+
157
+ /**
158
+ * The app's OWN session cookie — signed, not encrypted.
159
+ *
160
+ * Signed is the right choice and the distinction matters: the contents are not
161
+ * secret (a user may read their own id and name), but they must not be
162
+ * FORGEABLE. Encryption would hide a subject id the user already knows while
163
+ * doing nothing extra about forgery, which is the actual risk.
164
+ *
165
+ * What goes in is deliberately small: who you are and when this stops being
166
+ * true. Claims that can change — a name, a role, a picture — belong in the
167
+ * app's own store keyed by `sub`, because a cookie is a cache nobody can
168
+ * invalidate, and a stale role in a cookie is a permission that outlives its
169
+ * revocation.
170
+ */
171
+ interface SessionPayload {
172
+ /** The subject from Broberg ID. The one stable identifier. */
173
+ sub: string;
174
+ /** Unix seconds. Checked on every read. */
175
+ exp: number;
176
+ /** Optional convenience copies; never authorisation data. */
177
+ email?: string;
178
+ name?: string;
179
+ }
180
+
181
+ interface SsoRoutesOptions {
182
+ config?: SsoConfig;
183
+ client?: SsoClient;
184
+ /** Path prefix these routes are mounted under. Used to build the return URL. */
185
+ loginPath?: string;
186
+ /** Where to send a user after a successful login when no returnTo is given. */
187
+ defaultReturnTo?: string;
188
+ }
189
+ declare function ssoRoutes(options?: SsoRoutesOptions): {
190
+ app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
191
+ attach: MiddlewareHandler;
192
+ require: MiddlewareHandler;
193
+ client: SsoClient;
194
+ config: SsoConfig;
195
+ };
196
+ /** Read the session a middleware attached. */
197
+ declare function getSession(c: Context): SessionPayload | null;
198
+
199
+ export { type SsoRoutesOptions, getSession, ssoRoutes };
package/dist/hono.d.ts ADDED
@@ -0,0 +1,199 @@
1
+ import * as hono_types from 'hono/types';
2
+ import { Context, Hono, MiddlewareHandler } from 'hono';
3
+ import { importJWK, JWTPayload } from 'jose';
4
+
5
+ interface SsoConfig {
6
+ /** Broberg ID's origin, e.g. https://id.broberg.ai — the BARE origin. */
7
+ issuer: string;
8
+ /** The client id this app was registered under (administratively, in BID). */
9
+ clientId: string;
10
+ /** Exactly the redirect registered in BID. Exact match — one character decides. */
11
+ redirectUri: string;
12
+ /** Requested scopes. */
13
+ scopes: string[];
14
+ /** Signs the local session cookie. 32+ bytes of randomness. */
15
+ cookieSecret: string;
16
+ /** Local session cookie name. */
17
+ cookieName: string;
18
+ /**
19
+ * How long this app trusts its OWN session, in seconds.
20
+ *
21
+ * F084.7 decided the fleet's numbers: 7 days for a normal app, and 12 hours
22
+ * + 30 minutes of inactivity for anything holding personal or health data.
23
+ * The default here is the 7 days. An app handling patient data MUST set
24
+ * SSO_SESSION_MAX_AGE=43200 — the default is a normal-app default, not a
25
+ * safe-for-everything one.
26
+ */
27
+ sessionMaxAge: number;
28
+ /** Where to send the browser after a logout completes. */
29
+ postLogoutRedirectUri?: string;
30
+ }
31
+
32
+ /**
33
+ * The signing-key cache.
34
+ *
35
+ * ── THE ONE BEHAVIOUR THIS FILE EXISTS FOR ────────────────────────────────
36
+ *
37
+ * It refetches on an UNKNOWN KEY ID, not on a timer (F084.4's constraint, and
38
+ * it is the right one). An interval is a guess about when somebody else will
39
+ * rotate their key; an unknown kid is the event itself. With an interval, the
40
+ * window between "BID rotated" and "the interval elapsed" is a window where
41
+ * every login in every app fails, and the length of that window is a number
42
+ * nobody chose on purpose.
43
+ *
44
+ * ── AND THE PART AN OBVIOUS IMPLEMENTATION GETS WRONG ─────────────────────
45
+ *
46
+ * "Unknown kid ⇒ refetch" turns anyone who can send this app a token into
47
+ * someone who can make it hammer BID: a stream of tokens with random kids is a
48
+ * stream of fetches against the one service the whole fleet logs in through.
49
+ * So a refetch is rate-limited by time. The cost of the floor is real and worth
50
+ * stating: a rotation landing inside the cooldown makes logins fail for up to
51
+ * that many milliseconds. Seconds of failure for one app beats a way to aim
52
+ * traffic at BID from outside.
53
+ */
54
+
55
+ type SigningKey = Awaited<ReturnType<typeof importJWK>>;
56
+ interface JwksCache {
57
+ /** Resolve a key for this kid, refetching once if it is unknown. */
58
+ getKey(kid: string, alg: string): Promise<SigningKey>;
59
+ /** How many times the remote key set has actually been fetched. */
60
+ readonly fetchCount: number;
61
+ }
62
+
63
+ /**
64
+ * The core: framework-free, public-client OAuth 2.1 against Broberg ID.
65
+ *
66
+ * ── THIS PACKAGE IS A PUBLIC CLIENT. THERE IS NO CLIENT SECRET. ───────────
67
+ *
68
+ * Deliberate, and F084.4 AC#4 greps the published tarball to keep it that way.
69
+ * PKCE alone carries the exchange. That is safe here for a reason worth stating
70
+ * rather than assuming: BID matches redirect addresses EXACTLY (measured — one
71
+ * trailing slash is refused), so the authorization code is delivered to this
72
+ * app's own server and nowhere else. An attacker who knows the client id can
73
+ * start a flow; they cannot receive its result.
74
+ *
75
+ * What it buys is the thing the card actually asks for: a secret that does not
76
+ * exist cannot be committed, leaked in a log, copied into a second app, or left
77
+ * behind in a repository someone later makes public.
78
+ *
79
+ * ── AND WHAT THIS PACKAGE MUST NEVER LEARN TO DO ──────────────────────────
80
+ *
81
+ * No passwords. No passkey registration. No social-provider keys. No email
82
+ * verification. All of it lives in BID. A client that CAN do any of it is a
83
+ * client somebody eventually uses to do it — and then the identity rules exist
84
+ * in two places and drift.
85
+ */
86
+
87
+ interface Discovery {
88
+ issuer: string;
89
+ authorization_endpoint: string;
90
+ token_endpoint: string;
91
+ jwks_uri: string;
92
+ userinfo_endpoint?: string;
93
+ end_session_endpoint?: string;
94
+ }
95
+ /**
96
+ * What `beginLogin` produces. Every field except `url` must survive the round
97
+ * trip to BID and back — the adapter stores them in a short-lived cookie.
98
+ *
99
+ * They are NOT optional extras. `state` is what makes the callback provably the
100
+ * answer to THIS request, and `nonce` is what stops a valid token minted for
101
+ * some other login from being replayed into this one.
102
+ */
103
+ interface LoginStart {
104
+ url: string;
105
+ state: string;
106
+ codeVerifier: string;
107
+ nonce: string;
108
+ }
109
+ interface SsoClaims extends JWTPayload {
110
+ sub: string;
111
+ email?: string;
112
+ name?: string;
113
+ picture?: string;
114
+ email_verified?: boolean;
115
+ }
116
+ interface LoginResult {
117
+ claims: SsoClaims;
118
+ idToken: string;
119
+ accessToken?: string;
120
+ refreshToken?: string;
121
+ }
122
+ /**
123
+ * `prompt=none` asks BID to answer WITHOUT showing anything (F084.6).
124
+ *
125
+ * The delivery method decides whether it works, and this package only ever
126
+ * produces a URL for a FULL TOP-LEVEL REDIRECT. It never returns anything an
127
+ * app could put in a hidden iframe, because an iframe against the identity
128
+ * provider is third-party context: Safari has blocked it for years and Chrome
129
+ * is retiring it. That path works in testing on a Mac and fails for every user
130
+ * on an iPhone — a failure that looks like "you are not logged in".
131
+ */
132
+ interface BeginLoginOptions {
133
+ prompt?: "none" | "login" | "consent" | "select_account";
134
+ /** Extra scopes for this one request, on top of the configured set. */
135
+ scopes?: string[];
136
+ }
137
+ interface SsoClient {
138
+ discovery(): Promise<Discovery>;
139
+ beginLogin(options?: BeginLoginOptions): Promise<LoginStart>;
140
+ completeLogin(input: {
141
+ params: URLSearchParams;
142
+ state: string;
143
+ codeVerifier: string;
144
+ nonce: string;
145
+ }): Promise<LoginResult>;
146
+ verifyIdToken(idToken: string, options?: {
147
+ nonce?: string;
148
+ }): Promise<SsoClaims>;
149
+ logoutUrl(options?: {
150
+ idTokenHint?: string;
151
+ postLogoutRedirectUri?: string;
152
+ }): Promise<string>;
153
+ /** Exposed for tests and for a health check; not needed in normal use. */
154
+ readonly jwks: JwksCache;
155
+ }
156
+
157
+ /**
158
+ * The app's OWN session cookie — signed, not encrypted.
159
+ *
160
+ * Signed is the right choice and the distinction matters: the contents are not
161
+ * secret (a user may read their own id and name), but they must not be
162
+ * FORGEABLE. Encryption would hide a subject id the user already knows while
163
+ * doing nothing extra about forgery, which is the actual risk.
164
+ *
165
+ * What goes in is deliberately small: who you are and when this stops being
166
+ * true. Claims that can change — a name, a role, a picture — belong in the
167
+ * app's own store keyed by `sub`, because a cookie is a cache nobody can
168
+ * invalidate, and a stale role in a cookie is a permission that outlives its
169
+ * revocation.
170
+ */
171
+ interface SessionPayload {
172
+ /** The subject from Broberg ID. The one stable identifier. */
173
+ sub: string;
174
+ /** Unix seconds. Checked on every read. */
175
+ exp: number;
176
+ /** Optional convenience copies; never authorisation data. */
177
+ email?: string;
178
+ name?: string;
179
+ }
180
+
181
+ interface SsoRoutesOptions {
182
+ config?: SsoConfig;
183
+ client?: SsoClient;
184
+ /** Path prefix these routes are mounted under. Used to build the return URL. */
185
+ loginPath?: string;
186
+ /** Where to send a user after a successful login when no returnTo is given. */
187
+ defaultReturnTo?: string;
188
+ }
189
+ declare function ssoRoutes(options?: SsoRoutesOptions): {
190
+ app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
191
+ attach: MiddlewareHandler;
192
+ require: MiddlewareHandler;
193
+ client: SsoClient;
194
+ config: SsoConfig;
195
+ };
196
+ /** Read the session a middleware attached. */
197
+ declare function getSession(c: Context): SessionPayload | null;
198
+
199
+ export { type SsoRoutesOptions, getSession, ssoRoutes };