@broberg/sso 0.16.0 → 0.17.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.
- package/README.md +21 -0
- package/dist/{chunk-ZPRWYUFG.js → chunk-AVXQLEYK.js} +88 -3
- package/dist/chunk-AVXQLEYK.js.map +1 -0
- package/dist/{chunk-JGIO7XPR.cjs → chunk-QYMEISTU.cjs} +89 -2
- package/dist/chunk-QYMEISTU.cjs.map +1 -0
- package/dist/hono.cjs +37 -37
- package/dist/hono.d.cts +1 -1
- package/dist/hono.d.ts +1 -1
- package/dist/hono.js +1 -1
- package/dist/index.cjs +70 -62
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/{session-DsjOyCJb.d.cts → session-CIm4_raQ.d.cts} +47 -1
- package/dist/{session-DsjOyCJb.d.ts → session-CIm4_raQ.d.ts} +47 -1
- package/package.json +1 -1
- package/dist/chunk-JGIO7XPR.cjs.map +0 -1
- package/dist/chunk-ZPRWYUFG.js.map +0 -1
package/README.md
CHANGED
|
@@ -219,6 +219,27 @@ the only thing that throws `SsoConfigError` on a missing or blank value, naming
|
|
|
219
219
|
the variable. Hand-build the object from your own env schema and a blank string
|
|
220
220
|
is a valid, empty config that fails much later and somewhere else.
|
|
221
221
|
|
|
222
|
+
## Checking a token someone else holds — `introspectToken` (0.17.0, F084.158)
|
|
223
|
+
|
|
224
|
+
When a client app (a browser extension, a native app) logs in with BID and calls YOUR backend with its access token, your backend asks BID whether that token is good — RFC 7662 introspection, with your app's own client id and secret:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
const sso = createSsoClient(loadSsoConfig());
|
|
228
|
+
try {
|
|
229
|
+
const t = await sso.introspectToken(bearer, { requireClientId: "trail-clipper", cacheMs: 30_000 });
|
|
230
|
+
// t = { active: true, sub, clientId, scope, exp } — serve t.sub's own data only
|
|
231
|
+
} catch (e) {
|
|
232
|
+
if (e instanceof SsoTokenInactiveError) return c.json({ error: "login_required" }, 401); // inactive, wrong client, expired
|
|
233
|
+
if (e instanceof SsoIntrospectionUnavailableError) return c.json({ error: "bid_unavailable" }, 503); // BID down / timeout / our credentials refused
|
|
234
|
+
throw e;
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
- **Two failures, never confused.** A BID hiccup must not log a user out, and an expired login must not be retried as if BID were down.
|
|
239
|
+
- **`requireClientId`** is the check that matters: BID user tokens carry no per-app audience today, so "issued to the clipper client" is what proves the call came from your clipper.
|
|
240
|
+
- **Needs `SSO_CLIENT_SECRET`** — a public client cannot introspect (you get `SsoIntrospectionUnavailableError` without a request being sent). A wrong secret is reported as unavailable with "client id/secret was refused" — measured against id.broberg.ai.
|
|
241
|
+
- **Cache** (optional, `cacheMs`): an active answer is reused up to that long and never past the token's `exp`; an inactive answer is never cached. The token itself never appears in an error message.
|
|
242
|
+
|
|
222
243
|
## What it will never do
|
|
223
244
|
|
|
224
245
|
No passwords. No passkey registration. No social-provider keys. No email
|
|
@@ -195,6 +195,22 @@ var SsoInsufficientScopeError = class extends SsoAppApiError {
|
|
|
195
195
|
}
|
|
196
196
|
scope;
|
|
197
197
|
};
|
|
198
|
+
var SsoTokenInactiveError = class extends SsoError {
|
|
199
|
+
constructor(message, reason) {
|
|
200
|
+
super(message);
|
|
201
|
+
this.reason = reason;
|
|
202
|
+
this.name = "SsoTokenInactiveError";
|
|
203
|
+
}
|
|
204
|
+
reason;
|
|
205
|
+
};
|
|
206
|
+
var SsoIntrospectionUnavailableError = class extends SsoError {
|
|
207
|
+
constructor(message, status) {
|
|
208
|
+
super(message);
|
|
209
|
+
this.status = status;
|
|
210
|
+
this.name = "SsoIntrospectionUnavailableError";
|
|
211
|
+
}
|
|
212
|
+
status;
|
|
213
|
+
};
|
|
198
214
|
var SsoAvatarRejectedError = class extends SsoError {
|
|
199
215
|
constructor(message, reason) {
|
|
200
216
|
super(message);
|
|
@@ -468,8 +484,77 @@ function createSsoClient(config, options = {}) {
|
|
|
468
484
|
const unverifiedClaims = Object.keys(info).filter((k) => k !== "sub" && info[k] !== void 0 && !(k in claims)).sort();
|
|
469
485
|
return { claims: { ...info, ...claims, sub: claims.sub }, unverifiedClaims };
|
|
470
486
|
}
|
|
487
|
+
const introspectCache = /* @__PURE__ */ new Map();
|
|
488
|
+
async function introspectToken(token, options2) {
|
|
489
|
+
const now = options2.now ?? Date.now;
|
|
490
|
+
const key = Array.from(
|
|
491
|
+
new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token))),
|
|
492
|
+
(b) => b.toString(16).padStart(2, "0")
|
|
493
|
+
).join("");
|
|
494
|
+
const hit = introspectCache.get(key);
|
|
495
|
+
if (hit && now() < hit.until) return hit.result;
|
|
496
|
+
introspectCache.delete(key);
|
|
497
|
+
let endpoint;
|
|
498
|
+
try {
|
|
499
|
+
endpoint = (await discovery()).introspection_endpoint ?? new URL("/oauth2/introspect", config.issuer).toString();
|
|
500
|
+
} catch (e) {
|
|
501
|
+
throw new SsoIntrospectionUnavailableError(`could not read ${config.issuer}'s discovery document: ${e instanceof Error ? e.message : String(e)}`, null);
|
|
502
|
+
}
|
|
503
|
+
if (!config.clientSecret) {
|
|
504
|
+
throw new SsoIntrospectionUnavailableError("introspection needs this app's client secret (SSO_CLIENT_SECRET) \u2014 a public client cannot introspect", null);
|
|
505
|
+
}
|
|
506
|
+
const basic = btoa(`${encodeURIComponent(config.clientId)}:${encodeURIComponent(config.clientSecret)}`);
|
|
507
|
+
const ctrl = new AbortController();
|
|
508
|
+
let timer;
|
|
509
|
+
const timedOut = new Promise((_, reject) => {
|
|
510
|
+
timer = setTimeout(() => {
|
|
511
|
+
reject(new SsoIntrospectionUnavailableError(`${endpoint} did not answer within ${timeoutMs} ms`, null));
|
|
512
|
+
ctrl.abort();
|
|
513
|
+
}, timeoutMs);
|
|
514
|
+
});
|
|
515
|
+
let body;
|
|
516
|
+
try {
|
|
517
|
+
const res = await Promise.race([
|
|
518
|
+
fetchImpl(endpoint, {
|
|
519
|
+
method: "POST",
|
|
520
|
+
headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
521
|
+
body: new URLSearchParams({ token, token_type_hint: "access_token" }),
|
|
522
|
+
signal: ctrl.signal
|
|
523
|
+
}),
|
|
524
|
+
timedOut
|
|
525
|
+
]);
|
|
526
|
+
if (!res.ok) {
|
|
527
|
+
throw new SsoIntrospectionUnavailableError(
|
|
528
|
+
`${endpoint} answered ${res.status}${res.status === 401 ? " \u2014 this app's client id/secret was refused for introspection" : ""}`,
|
|
529
|
+
res.status
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
const parsed = await Promise.race([res.json().catch(() => null), timedOut]);
|
|
533
|
+
if (!parsed || typeof parsed !== "object") throw new SsoIntrospectionUnavailableError(`${endpoint} answered ${res.status} with a body that is not a JSON object`, res.status);
|
|
534
|
+
body = parsed;
|
|
535
|
+
} catch (e) {
|
|
536
|
+
if (e instanceof SsoIntrospectionUnavailableError) throw e;
|
|
537
|
+
throw new SsoIntrospectionUnavailableError(`${endpoint} could not be reached: ${e instanceof Error ? e.message : String(e)}`, null);
|
|
538
|
+
} finally {
|
|
539
|
+
clearTimeout(timer);
|
|
540
|
+
}
|
|
541
|
+
if (body.active !== true) throw new SsoTokenInactiveError("the token is not active at BID", "inactive");
|
|
542
|
+
const clientId = typeof body.client_id === "string" ? body.client_id : "";
|
|
543
|
+
if (clientId !== options2.requireClientId) {
|
|
544
|
+
throw new SsoTokenInactiveError(`the token was issued to client \xAB${clientId || "(none)"}\xBB, not \xAB${options2.requireClientId}\xBB`, "wrong_client");
|
|
545
|
+
}
|
|
546
|
+
const exp = typeof body.exp === "number" ? body.exp : 0;
|
|
547
|
+
if (exp * 1e3 <= now()) throw new SsoTokenInactiveError("the token has expired", "expired");
|
|
548
|
+
if (typeof body.sub !== "string" || body.sub === "") {
|
|
549
|
+
throw new SsoIntrospectionUnavailableError(`${endpoint} said active but named no subject`, 200);
|
|
550
|
+
}
|
|
551
|
+
const result = { active: true, sub: body.sub, clientId, scope: typeof body.scope === "string" ? body.scope : "", exp };
|
|
552
|
+
if (options2.cacheMs && options2.cacheMs > 0) introspectCache.set(key, { result, until: Math.min(now() + options2.cacheMs, exp * 1e3) });
|
|
553
|
+
return result;
|
|
554
|
+
}
|
|
471
555
|
return {
|
|
472
556
|
discovery,
|
|
557
|
+
introspectToken,
|
|
473
558
|
get jwks() {
|
|
474
559
|
if (!jwksCache) throw new SsoError("the key cache is created on first verification");
|
|
475
560
|
return jwksCache;
|
|
@@ -800,6 +885,6 @@ function newSid() {
|
|
|
800
885
|
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
801
886
|
}
|
|
802
887
|
|
|
803
|
-
export { ALLOWED_ALGS, AVATAR_TYPES, DEFAULT_SESSION_MAX_AGE, JwksError, JwksUnavailableError, JwksUnknownKeyError, MAX_AVATAR_BYTES, MAX_INVITATIONS, SsoAppApiError, SsoAvatarRejectedError, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, cookieHeader, createJwksCache, createSsoClient, expiresAtFrom, loadSsoConfig, mediaType, memoryTokenStore, newSid, readCookie, signSession, signValue, verifySession, verifyValue };
|
|
804
|
-
//# sourceMappingURL=chunk-
|
|
805
|
-
//# sourceMappingURL=chunk-
|
|
888
|
+
export { ALLOWED_ALGS, AVATAR_TYPES, DEFAULT_SESSION_MAX_AGE, JwksError, JwksUnavailableError, JwksUnknownKeyError, MAX_AVATAR_BYTES, MAX_INVITATIONS, SsoAppApiError, SsoAvatarRejectedError, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoIntrospectionUnavailableError, SsoReauthError, SsoTokenInactiveError, cookieHeader, createJwksCache, createSsoClient, expiresAtFrom, loadSsoConfig, mediaType, memoryTokenStore, newSid, readCookie, signSession, signValue, verifySession, verifyValue };
|
|
889
|
+
//# sourceMappingURL=chunk-AVXQLEYK.js.map
|
|
890
|
+
//# sourceMappingURL=chunk-AVXQLEYK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/jwks.ts","../src/client.ts","../src/session.ts","../src/tokens.ts"],"names":["options","b64url"],"mappings":"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","file":"chunk-AVXQLEYK.js","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 * Set ONLY by an app that can actually keep a secret — one whose callback\n * runs on a server. Leave it unset for anything shipped to a browser; a\n * secret in a static bundle is not a secret, and PKCE alone is the correct,\n * fully standard design there.\n *\n * Setting it makes this a CONFIDENTIAL client: the secret is sent as\n * `client_secret_post` (in the body), which is the method BID registers\n * confidential clients with.\n *\n * IT DOES NOT RELAX PKCE. A secret proves which APP is asking; the PKCE\n * verifier proves the request belongs to the browser that started the login.\n * They defend against different attacks, so one is never an excuse to drop\n * the other — and there is a test that fails if anyone makes it one.\n */\n clientSecret?: 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 clientSecret: env.SSO_CLIENT_SECRET?.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 * ── TWO FAILURES, NOT ONE (components-F084.50) ───────────────────────────────────────\n *\n * Reported by helpdesk, measured in the published 0.1.0 dist: at process start\n * the key set is EMPTY, so the first verification fetches. If BID is down in\n * that second, NOTHING verifies for that app — not just new logins. A process\n * that has been running and has seen a kid survives the same outage untouched.\n *\n * The recovery was never the defect: `lastFetchAt` is set only on SUCCESS, so a\n * failed fetch does not block the next attempt and the cache self-heals the\n * moment BID answers again. The defect was that `getKey` threw the SAME error\n * for two states whose correct answers are opposites:\n *\n * cannot reach the issuer transient. Retry, or answer 503. The token may\n * well be perfectly good — we simply did not look.\n * kid is not published permanent. This token was not signed by us.\n * Reject it, 401, and do not retry.\n *\n * One error name for both is how a caller ends up rejecting a legitimate user\n * because a foreign service had a bad minute — or, worse, retrying a forgery.\n * Hence two subclasses. They BOTH extend JwksError, so a 0.1.0 consumer whose\n * catch tests `instanceof JwksError` keeps working across the upgrade.\n *\n * ── WHAT WE DELIBERATELY DID NOT DO ───────────────────────────────────────\n *\n * We do NOT persist the key set. A stored key set that survives a REVOCATION is\n * a worse failure than a login that is down for two minutes — helpdesk's own\n * point, and the reason `keys = body.keys` replaces rather than merges. Writing\n * the set to disk would reintroduce exactly what that line exists to prevent.\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 /**\n * Fetch the key set once at construction, so the cold window sits at BOOT\n * rather than in front of the first user who tries to log in.\n *\n * It is fire-and-forget ON PURPOSE: a failed warm-up must never stop the app\n * from starting. A package that refuses to boot because someone else's\n * service is down has made the outage worse, not better — the first getKey\n * will simply fetch, exactly as it does today.\n */\n warmUp?: boolean;\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\n/**\n * The issuer could not be consulted — unreachable, a non-2xx, or a body that is\n * not a key set. Also thrown when an unknown kid arrives inside the refetch\n * cooldown, because there too the honest statement is \"we did not look\".\n *\n * TRANSIENT. The right response is to retry, or to answer 503 — never to\n * conclude anything about the token, which may be perfectly valid.\n */\nexport class JwksUnavailableError extends JwksError {\n constructor(message: string) {\n super(message);\n this.name = \"JwksUnavailableError\";\n }\n}\n\n/**\n * The issuer WAS consulted and does not publish this key id.\n *\n * PERMANENT. The token was not signed by this issuer. Reject it; retrying only\n * asks the same question again and BID will keep giving the same answer.\n */\nexport class JwksUnknownKeyError extends JwksError {\n constructor(message: string) {\n super(message);\n this.name = \"JwksUnknownKeyError\";\n }\n}\n\nexport function createJwksCache(options: JwksCacheOptions): JwksCache {\n const {\n jwksUri,\n minRefetchIntervalMs = 10_000,\n warmUp = false,\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 let res: Response;\n try {\n res = await fetchImpl(jwksUri);\n } catch (cause) {\n // A thrown fetch is DNS, TLS, a refused connection — the issuer was not\n // reached at all. Rethrown as our own type so a caller never has to\n // pattern-match on a runtime's network error to tell this from a\n // rejected token.\n throw new JwksUnavailableError(\n `${jwksUri} could not be reached: ${cause instanceof Error ? cause.message : String(cause)}`,\n );\n }\n if (!res.ok) {\n throw new JwksUnavailableError(\n `${jwksUri} answered ${res.status} — cannot verify any token right now`,\n );\n }\n let body: { keys?: JWK[] } | null;\n try {\n body = (await res.json()) as { keys?: JWK[] } | null;\n } catch {\n // A 200 that is not JSON (a proxy's HTML error page, a captive portal)\n // is still \"we could not look\" — not a raw SyntaxError that slips past\n // every consumer's `instanceof JwksError`.\n throw new JwksUnavailableError(`${jwksUri} answered 200 with a body that is not JSON`);\n }\n if (!Array.isArray(body?.keys)) {\n // Reachable but not serving a key set: still \"we could not look\".\n throw new JwksUnavailableError(`${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 if (warmUp) {\n // Swallowed deliberately — see `warmUp` above. The failure is not lost:\n // the next getKey reports it to the caller who can actually act on it.\n void refresh().catch(() => {});\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 // The wording matters as much as the class, and this sentence is\n // helpdesk's, not mine. The old one said \"Refusing to refetch — retry\n // shortly\": transient ADVICE on a state that does not improve by\n // waiting, which literally instructs a consumer to do the thing the\n // split exists to prevent. It described what the CACHE did; a caller\n // needs to know what the STATE is.\n //\n // It carries the AGE because that is the number that decides. Their\n // measurement against the live issuer showed a key set 2ms old — we\n // had just looked, and the kid was not there. That is evidence of a\n // forgery, not of ignorance. Only a rotation inside the floor can\n // make this a false negative, and the age is what lets a caller see\n // which case it is standing in.\n throw new JwksUnavailableError(\n `kid ${kid} is not in the key set we hold, fetched ${sinceLast}ms ago ` +\n `(we do not ask the issuer again within ${minRefetchIntervalMs}ms). ` +\n `A key set this fresh that lacks the kid means the token was not signed by ` +\n `this issuer — reject it. Only a key rotation within the last ` +\n `${minRefetchIntervalMs}ms could make that wrong.`,\n );\n }\n await refresh();\n jwk = keys.find((k) => k.kid === kid);\n }\n\n if (!jwk) {\n throw new JwksUnknownKeyError(\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 /** What the issuer says it signs with. Absent on some providers. */\n id_token_signing_alg_values_supported?: string[];\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 /** RFC 7662 — where a resource server checks an access token (F084.158). */\n introspection_endpoint?: string;\n}\n\nexport class SsoError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"SsoError\";\n }\n}\n\n/**\n * F095.2 — the user must log in again: there is no live session, no stored\n * tokens, or the renewal was refused. `reason` says which; \"no_session\" means\n * there never was a login to recover, the others mean there was one and it is\n * spent. Thrown by getAccessToken.\n */\nexport class SsoReauthError extends SsoError {\n constructor(\n message: string,\n readonly reason: \"no_session\" | \"no_tokens\" | \"expired\" | \"refresh_failed\",\n ) {\n super(message);\n this.name = \"SsoReauthError\";\n }\n}\n\n/**\n * F095.2 — BID's app API answered with an error, or did not answer at all.\n * `status` is BID's HTTP status (null when nothing came back: timeout, network);\n * `code` is BID's own `error` field when it sent one. The message carries the\n * URL, the status and the code — never the body, never a token.\n */\nexport class SsoAppApiError extends SsoError {\n constructor(\n message: string,\n readonly status: number | null,\n readonly code: string | undefined,\n ) {\n super(message);\n this.name = \"SsoAppApiError\";\n }\n}\n\n/**\n * F095.2 — BID refused because the user's token lacks a scope (403\n * insufficient_scope). For the profile writes that is `profile:write`: the app\n * must request it at login, and a user who logged in before it did must log in\n * again. That is the one actionable fact, so it gets its own class.\n */\nexport class SsoInsufficientScopeError extends SsoAppApiError {\n constructor(message: string, readonly scope: string) {\n super(message, 403, \"insufficient_scope\");\n this.name = \"SsoInsufficientScopeError\";\n }\n}\n\n/** F095.2 — an avatar refused BEFORE it was sent: too big, or not PNG/JPEG/WebP. */\n/**\n * F084.158 — the token is not usable HERE: inactive, expired, or issued to a\n * different client than the one required. Answer 401 — the user has to log in\n * again. Never thrown because BID could not be asked.\n */\nexport class SsoTokenInactiveError extends SsoError {\n constructor(\n message: string,\n /** \"inactive\" | \"wrong_client\" | \"expired\" */\n public readonly reason: \"inactive\" | \"wrong_client\" | \"expired\",\n ) {\n super(message);\n this.name = \"SsoTokenInactiveError\";\n }\n}\n\n/**\n * F084.158 — BID could not answer whether the token is active (network, timeout,\n * 5xx, an unreadable body, or our own client credentials refused). Answer 503 —\n * a BID hiccup must never look like an expired login.\n */\nexport class SsoIntrospectionUnavailableError extends SsoError {\n constructor(message: string, public readonly status: number | null) {\n super(message);\n this.name = \"SsoIntrospectionUnavailableError\";\n }\n}\n\nexport interface IntrospectOptions {\n /** The client the token must have been issued to (e.g. \"trail-clipper\"). */\n requireClientId: string;\n /** Reuse an ACTIVE answer for up to this long, never past the token's exp. Off by default. */\n cacheMs?: number;\n /** For tests. */\n now?: () => number;\n}\n\nexport interface IntrospectedToken {\n active: true;\n sub: string;\n clientId: string;\n scope: string;\n /** Seconds since epoch. */\n exp: number;\n}\n\nexport class SsoAvatarRejectedError extends SsoError {\n constructor(message: string, readonly reason: \"too_large\" | \"unsupported_type\") {\n super(message);\n this.name = \"SsoAvatarRejectedError\";\n }\n}\n\n/** BID's limit for a profile picture (llms.txt step 7a: \"max 2 MB\", read as 2 MiB). */\nexport const MAX_AVATAR_BYTES = 2 * 1024 * 1024;\n/** The picture types BID accepts. */\nexport const AVATAR_TYPES: readonly string[] = [\"image/png\", \"image/jpeg\", \"image/webp\"];\n\n/** \"image/PNG; charset=x\" → \"image/png\". */\nexport function mediaType(contentType: string | null | undefined): string {\n return (contentType ?? \"\").split(\";\")[0]!.trim().toLowerCase();\n}\n\n/** The signed-in user's own profile, as BID's GET /api/app/profile returns it. */\nexport interface BidProfile {\n sub: string;\n name: string | null;\n picture: string | null;\n email: string | null;\n /** BID's own account page — mail, password, passkeys, 2FA and sessions live there. */\n account_url: string;\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 /**\n * The claims in `claims` that came from the UNSIGNED userinfo response and\n * not from the signed ID token (components-F084.51, since 0.8.0). Sorted; an\n * empty list means everything in `claims` was signed (or userinfo was not\n * reached). When both sources carry a claim, the signed token's value wins\n * and the claim is not listed.\n *\n * On BID today this always contains \"email\" and \"email_verified\": the ID\n * token does not carry them (measured by broberg-id, 20 Sep 2026). A decision\n * that must rest on a signed identity binds on `claims.sub`, never on email.\n */\n unverifiedClaims: string[];\n idToken: string;\n accessToken?: string;\n refreshToken?: string;\n /** Seconds the access token lives, when BID said so (`expires_in`). */\n expiresIn?: number;\n}\n\n/**\n * `prompt=none` asks BID to answer WITHOUT showing anything (components-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 /**\n * Does this address belong to the signed-in user's BID account, and is it\n * verified there? Asks BID's POST /api/app/address-ownership with the user's\n * access token. Throws SsoError on anything that is not one of the three\n * answers — see the implementation for why that must never become \"unverified\".\n */\n addressOwnership(accessToken: string, address: string): Promise<AddressOwnership>;\n /**\n * F095.1 — trade a refresh token for a new access token (`grant_type=refresh_token`).\n * Throws SsoError on refusal; the message carries the status and BID's error\n * code, never a token.\n */\n refreshTokens(refreshToken: string): Promise<{ accessToken: string; refreshToken?: string; expiresIn?: number }>;\n /**\n * Invite users to BID on behalf of this app (the app's OWN key, `bidk_…`).\n * One result per email. Throws SsoError on anything BID did not clearly say.\n */\n inviteUsers(appKey: string, input: InviteUsersInput): Promise<InvitationResult[]>;\n /**\n * Where each user is in the move to BID. Throws SsoError on any state it does\n * not know — never a silent \"ready\", which would switch off a user's old login\n * while they cannot get in.\n */\n migrationStatus(appKey: string, emails: string[]): Promise<MigrationStatus>;\n /**\n * Verify an OIDC Back-Channel Logout token from BID («Log ud overalt»).\n * Throws SsoError on anything that is not a valid, sub-only logout token.\n */\n verifyLogoutToken(token: string): Promise<LogoutToken>;\n /**\n * F095.2 — the signed-in user's own profile (scope `profile`), with the user's\n * access token. Throws SsoAppApiError on any refusal.\n */\n getProfile(accessToken: string): Promise<BidProfile>;\n /** F095.2 — change the user's own name (scope `profile:write`). Returns the new profile. */\n updateProfile(accessToken: string, name: string): Promise<BidProfile>;\n /**\n * F095.2 — replace the user's picture (scope `profile:write`). Refuses more than\n * MAX_AVATAR_BYTES or a type outside AVATAR_TYPES with SsoAvatarRejectedError\n * BEFORE anything is sent.\n */\n uploadAvatar(accessToken: string, bytes: Uint8Array | ArrayBuffer, contentType: string): Promise<BidProfile>;\n /** F095.2 — remove the user's picture (scope `profile:write`). */\n removeAvatar(accessToken: string): Promise<BidProfile>;\n /**\n * F084.158 — ask BID (RFC 7662) whether an access token is active, with THIS\n * app's client credentials, and require it was issued to `requireClientId`.\n * Throws SsoTokenInactiveError (→ 401) or SsoIntrospectionUnavailableError\n * (→ 503). The token never appears in an error message.\n */\n introspectToken(token: string, options: IntrospectOptions): Promise<IntrospectedToken>;\n /** Exposed for tests and for a health check; not needed in normal use. */\n readonly jwks: JwksCache;\n}\n\nexport type AddressOwnership = \"verified\" | \"unverified\" | \"not_on_account\";\nconst ADDRESS_OWNERSHIP = new Set<string>([\"verified\", \"unverified\", \"not_on_account\"]);\n\nexport type InvitationOutcome =\n | \"invited\"\n | \"already_invited\"\n | \"existing\"\n | \"invalid\"\n | \"too_soon\"\n | \"too_many_today\"\n | \"send_failed\";\nconst INVITATION_OUTCOMES = new Set<string>([\n \"invited\", \"already_invited\", \"existing\", \"invalid\", \"too_soon\", \"too_many_today\", \"send_failed\",\n]);\nexport interface InviteUsersInput {\n customerName: string;\n appUrl: string;\n appName?: string;\n switchDate?: string;\n users: Array<{ email: string; name: string }>;\n}\nexport interface InvitationResult {\n email: string;\n outcome: InvitationOutcome;\n mailId?: string;\n problem?: string;\n}\n\nexport type MigrationState = \"ready\" | \"invited\" | \"expired\" | \"not_invited\";\nconst MIGRATION_STATES = new Set<string>([\"ready\", \"invited\", \"expired\", \"not_invited\"]);\nexport interface MigrationStatus {\n users: Array<{ email: string; state: MigrationState; expiresAt?: string }>;\n counts: Record<string, number>;\n complete: boolean;\n}\n\n/** A verified back-channel logout: every session for `sub` issued at or before `iat` is over. */\nexport interface LogoutToken {\n sub: string;\n iat: number;\n jti: string;\n exp: number;\n}\nconst BACKCHANNEL_EVENT = \"http://schemas.openid.net/event/backchannel-logout\";\n\n/** BID's own limit per invitation call. */\nexport const MAX_INVITATIONS = 500;\n\nexport interface CreateSsoClientOptions {\n fetchImpl?: typeof fetch;\n /**\n * Ceiling for the app-API calls (addressOwnership, inviteUsers,\n * migrationStatus). A hanging BID must not hang the app. Default 10 000 ms.\n */\n timeoutMs?: number;\n /** Passed through to the key cache; see jwks.ts for why there is a floor. */\n minRefetchIntervalMs?: number;\n}\n\n/**\n * `invalid_client` is returned for two OPPOSITE mistakes, and the issuer cannot\n * tell you which — from outside they are the same three words.\n *\n * we sent a secret, it was refused → wrong secret, or we are registered\n * as PUBLIC and should send none\n * we sent none and it was demanded → we are registered as CONFIDENTIAL\n * and SSO_CLIENT_SECRET is unset\n *\n * The expensive version of this is the plugin storing the secret HASHED\n * (SHA-256 → base64url) and comparing hashes: write the plaintext through the\n * adapter and the row is written, reads back clean, and every token call is\n * refused forever. broberg-id paid for that one. We cannot see their storage\n * from here — but we CAN say what WE sent, which halves the search instead of\n * leaving a caller to guess which end to open.\n *\n * It states our SIDE, never the value. A secret must not reach a log line.\n */\n/**\n * A fragment of a foreign response body, safe to put in a log line (components-F084.52).\n *\n * Three things it has to survive, and each one has bitten somebody:\n *\n * · LENGTH — an HTML error page is kilobytes. A log line is not.\n * · NEWLINES — a multi-line excerpt breaks every log aggregator that treats a\n * line as a record, and the useful half is the part that scrolled away.\n * · OUR OWN SECRET — the body comes from a server we do not control, and some\n * servers echo the request back on an error. If our client_secret is in\n * there, we must not be the ones who write it to disk.\n */\nfunction excerptForLog(raw: string, clientSecret: string | undefined): string {\n let text = raw.replace(/\\s+/g, \" \").trim();\n if (clientSecret) text = text.split(clientSecret).join(\"[redacted client_secret]\");\n if (text === \"\") return \"(empty body)\";\n return text.length > 200 ? `${text.slice(0, 200)}…` : text;\n}\n\nfunction invalidClientHint(error: string | undefined, weSentASecret: boolean): string {\n if (error !== \"invalid_client\") return \"\";\n return weSentASecret\n ? \" — we DID send a client_secret, so either it is wrong or this client is registered as PUBLIC (in which case unset SSO_CLIENT_SECRET).\"\n : \" — we sent NO client_secret, so this client is likely registered as CONFIDENTIAL (set SSO_CLIENT_SECRET to the value it was registered with).\";\n}\n\n/**\n * The algorithms an ID token may be signed with. Asymmetric only, and stated\n * HERE rather than inferred from the token.\n *\n * ── WHY THIS LIST EXISTS WHEN NOTHING WAS EXPLOITABLE WITHOUT IT ──────────\n *\n * `alg` is read from the token's own protected header — that is, from whoever\n * sent it. Before this list, it was handed to importJWK and jwtVerify was\n * called with no restriction at all. Both classic forgeries were built and run\n * against this client in the security review of 2026-09-20:\n *\n * HS256, signed with the PUBLIC key as the HMAC secret → rejected\n * alg: none → rejected\n * control: a genuine RS256 token → accepted\n *\n * So the package was NOT exploitable. But read the rejection: both failed with\n * `JOSENotSupported: Invalid or unsupported JWK \"alg\"`, thrown inside jose's\n * key import because an RSA JWK cannot be imported as an HMAC key. The defence\n * was a dependency's internals, not ours — it would move on a minor upgrade, or\n * the day an issuer publishes a symmetric key, and NOTHING here would go red.\n *\n * A guard you cannot see is a guard you cannot keep. Now jwtVerify refuses\n * first, and there is a test that fails if this list is widened.\n */\nexport const ALLOWED_ALGS = [\n // FIRST, because it is what Broberg ID actually signs with. Measured on the\n // live issuer 2026-09-20:\n // /jwks kid=Iodh… kty=OKP alg=EdDSA crv=Ed25519\n // discovery id_token_signing_alg_values_supported: [\"EdDSA\"]\n //\n // 0.2.1 SHIPPED THIS LIST WITHOUT IT and rejected every real token for an\n // hour. The list was written from what an OIDC client usually allows rather\n // than from what our issuer uses — and the whole suite missed it because the\n // fake issuer in the tests signs RS256. A fake agrees with whoever wrote it.\n // There is now a test that mints EdDSA, the algorithm production uses.\n \"EdDSA\",\n \"RS256\", \"RS384\", \"RS512\",\n \"PS256\", \"PS384\", \"PS512\",\n \"ES256\", \"ES384\", \"ES512\",\n];\n\nexport function createSsoClient(\n config: SsoConfig,\n options: CreateSsoClientOptions = {},\n): SsoClient {\n const fetchImpl = options.fetchImpl ?? fetch;\n const timeoutMs = options.timeoutMs ?? 10_000;\n\n /** POST to one of BID's app-API routes: bounded in time, JSON in and out, loud on failure. */\n async function appPost(path: string, bearer: string, payload: unknown, what: string): Promise<unknown> {\n const url = new URL(path, config.issuer).toString();\n const ctrl = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n ctrl.abort();\n reject(new SsoError(`${url} did not answer within ${timeoutMs} ms — ${what} is unknown.`));\n }, timeoutMs);\n });\n try {\n const res = await Promise.race([\n fetchImpl(url, {\n method: \"POST\",\n headers: { authorization: `Bearer ${bearer}`, \"content-type\": \"application/json\" },\n body: JSON.stringify(payload),\n signal: ctrl.signal,\n }),\n timedOut,\n ]);\n if (!res.ok) {\n const detail = await res.text().catch(() => \"\");\n const hint = detail.includes(\"invalid_app_key\") ? \" (invalid_app_key: this app's bidk_ key was refused)\" : \"\";\n throw new SsoError(`${url} answered ${res.status}${hint} — ${what} is unknown.`);\n }\n return await Promise.race([res.json().catch(() => null), timedOut]);\n } catch (e) {\n if (e instanceof SsoError) throw e;\n throw new SsoError(`${url} could not be reached: ${e instanceof Error ? e.message : String(e)} — ${what} is unknown.`);\n } finally {\n clearTimeout(timer);\n }\n }\n /**\n * F095.2 — the general app-API call beside appPost (which stays as it is):\n * any method, a JSON or raw-bytes body, bounded in time, STATUS FIRST, and\n * BID's `error` code surfaced as SsoAppApiError rather than lost. The message\n * names the URL, status and code only — the body is never quoted, because the\n * request carried the user's token and an echoing server would put it in a log.\n */\n async function appRequest(\n path: string,\n bearer: string,\n init: { method: \"GET\" | \"POST\"; json?: unknown; bytes?: Uint8Array | ArrayBuffer; contentType?: string },\n what: string,\n ): Promise<unknown> {\n const url = new URL(path, config.issuer).toString();\n const ctrl = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n // Reject BEFORE aborting: the abort makes fetch reject too, and the race\n // must be won by the reason that names the timeout.\n reject(new SsoAppApiError(`${url} did not answer within ${timeoutMs} ms — ${what} is unknown.`, null, undefined));\n ctrl.abort();\n }, timeoutMs);\n });\n const headers: Record<string, string> = { authorization: `Bearer ${bearer}` };\n let body: BodyInit | undefined;\n if (init.json !== undefined) {\n headers[\"content-type\"] = \"application/json\";\n body = JSON.stringify(init.json);\n } else if (init.bytes !== undefined) {\n headers[\"content-type\"] = init.contentType ?? \"application/octet-stream\";\n body = init.bytes as BodyInit;\n }\n try {\n const res = await Promise.race([\n fetchImpl(url, { method: init.method, headers, ...(body !== undefined ? { body } : {}), signal: ctrl.signal }),\n timedOut,\n ]);\n const raw = await Promise.race([res.text(), timedOut]);\n let parsed: unknown;\n try {\n parsed = raw.trim() === \"\" ? undefined : JSON.parse(raw);\n } catch {\n parsed = undefined;\n }\n const fields = (parsed && typeof parsed === \"object\" ? parsed : {}) as { error?: unknown; scope?: unknown };\n const code = typeof fields.error === \"string\" ? fields.error : undefined;\n // Status FIRST: a refusal is a refusal whatever its body looks like.\n if (!res.ok) {\n if (res.status === 403 && code === \"insufficient_scope\") {\n const scope =\n typeof fields.scope === \"string\" && fields.scope ? fields.scope : init.method === \"GET\" ? \"profile\" : \"profile:write\";\n throw new SsoInsufficientScopeError(\n `${url} answered 403 insufficient_scope — the user's token lacks scope \"${scope}\"; log in again with it requested.`,\n scope,\n );\n }\n throw new SsoAppApiError(`${url} answered ${res.status}${code ? ` (${code})` : \"\"} — ${what} is unknown.`, res.status, code);\n }\n if (parsed === undefined) {\n throw new SsoAppApiError(`${url} answered ${res.status} with a body that is not JSON — ${what} is unknown.`, res.status, undefined);\n }\n return parsed;\n } catch (e) {\n if (e instanceof SsoError) throw e;\n // The name only, not the message: a fetch error's message can quote the request.\n throw new SsoAppApiError(`${url} could not be reached (${e instanceof Error ? e.name : \"error\"}) — ${what} is unknown.`, null, undefined);\n } finally {\n clearTimeout(timer);\n }\n }\n\n /** A profile body, or SsoAppApiError — never a half-filled object passed off as one. */\n function asProfile(body: unknown, path: string): BidProfile {\n const b = (body ?? {}) as Record<string, unknown>;\n if (typeof b.sub !== \"string\" || b.sub === \"\" || typeof b.account_url !== \"string\") {\n throw new SsoAppApiError(`${path} returned no \"sub\" or \"account_url\" — the profile is unknown.`, 200, undefined);\n }\n const opt = (v: unknown) => (typeof v === \"string\" ? v : null);\n return { sub: b.sub, name: opt(b.name), picture: opt(b.picture), email: opt(b.email), account_url: b.account_url };\n }\n\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 // THE CHECK THAT WOULD HAVE TURNED 0.2.1'S OUTAGE INTO A SENTENCE.\n //\n // An allow-list that omits the algorithm the issuer actually uses does\n // not fail loudly: every token is refused, one at a time, with a message\n // about that token. The app looks broken, the issuer looks broken, and\n // nothing names the real cause. That is what 0.2.1 did for an hour.\n //\n // The issuer already publishes the answer. If it advertises its signing\n // algorithms and NONE of them are ones we accept, that is knowable here —\n // once, at first use — instead of being rediscovered on every login.\n //\n // Deliberately narrow: it fires only on an EMPTY intersection. An issuer\n // that supports one algorithm we allow and three we do not is perfectly\n // workable, and refusing to start there would be a second outage dressed\n // as caution.\n const advertised = doc.id_token_signing_alg_values_supported;\n if (Array.isArray(advertised) && advertised.length > 0) {\n const usable = advertised.filter((a) => ALLOWED_ALGS.includes(a));\n if (usable.length === 0) {\n throw new SsoError(\n `${config.issuer} signs ID tokens with ${advertised.join(\", \")}, and this client ` +\n `accepts none of those (it allows ${ALLOWED_ALGS.join(\", \")}). Every token would be ` +\n `rejected. This is a mismatch between the package and the issuer — report it rather ` +\n `than working around it.`,\n );\n }\n }\n\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 /**\n * OIDC Back-Channel Logout 1.0 §2.6, plus one rule of our own: a token that\n * carries `sid` is REFUSED. BID sends sub-only tokens, and only from «Log ud\n * overalt» — the ordinary «Log ud» must not reach the apps (decision D-376ffa).\n * better-auth fires sid-tokens on every session deletion; accepting one here\n * would quietly turn BID's own logout into a fleet-wide one if that hook were\n * ever wired up.\n */\n async function verifyLogoutToken(token: string): Promise<LogoutToken> {\n const cache = await keys();\n let header;\n try {\n header = decodeProtectedHeader(token);\n } catch {\n throw new SsoError(\"logout_token is not a JWS\");\n }\n if (!header.kid) throw new SsoError(\"logout_token has no kid — cannot pick a signing key\");\n let payload: JWTPayload;\n try {\n ({ payload } = await jwtVerify(token, async () => cache.getKey(header.kid!, header.alg ?? \"RS256\"), {\n issuer: config.issuer,\n audience: config.clientId,\n algorithms: ALLOWED_ALGS,\n typ: \"logout+jwt\",\n requiredClaims: [\"iat\", \"exp\", \"jti\"],\n }));\n } catch (e) {\n throw new SsoError(`logout_token rejected: ${e instanceof Error ? e.message : String(e)}`);\n }\n const events = payload.events as Record<string, unknown> | undefined;\n if (!events || typeof events !== \"object\" || !(BACKCHANNEL_EVENT in events)) {\n throw new SsoError(\"logout_token has no back-channel logout event\");\n }\n if (\"nonce\" in payload) throw new SsoError(\"logout_token carries a nonce — that is an ID token, not a logout token\");\n if (\"sid\" in payload) {\n throw new SsoError(\"logout_token carries sid — only sub-wide «Log ud overalt» is accepted (D-376ffa)\");\n }\n if (typeof payload.sub !== \"string\" || payload.sub === \"\") throw new SsoError(\"logout_token has no sub\");\n return { sub: payload.sub, iat: payload.iat!, jti: String(payload.jti), exp: payload.exp! };\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, algorithms: ALLOWED_ALGS },\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(\n claims: SsoClaims,\n accessToken: string,\n ): Promise<{ claims: SsoClaims; unverifiedClaims: string[] }> {\n const doc = await discovery();\n if (!doc.userinfo_endpoint) return { claims, unverifiedClaims: [] };\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, unverifiedClaims: [] };\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 // components-F084.51 — THE SIGNED TOKEN WINS, and what it did not carry is\n // NAMED. Userinfo is an unsigned HTTP response: it may FILL a hole the token\n // left, never overwrite a claim the token signed. Every key it filled is\n // listed in `unverifiedClaims`, so a consumer gating on email_verified can\n // see the value was never signed — which on BID it never is today (measured\n // by broberg-id, 20 Sep 2026: email + email_verified exist only in userinfo).\n const unverifiedClaims = Object.keys(info)\n .filter((k) => k !== \"sub\" && info[k] !== undefined && !(k in claims))\n .sort();\n return { claims: { ...info, ...claims, sub: claims.sub }, unverifiedClaims };\n }\n\n // F084.158 — introspection. The cache key is a hash of the token, so the map\n // never holds a usable credential; an inactive answer is never cached.\n const introspectCache = new Map<string, { result: IntrospectedToken; until: number }>();\n async function introspectToken(token: string, options: IntrospectOptions): Promise<IntrospectedToken> {\n const now = options.now ?? Date.now;\n const key = Array.from(\n new Uint8Array(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(token))),\n (b) => b.toString(16).padStart(2, \"0\"),\n ).join(\"\");\n const hit = introspectCache.get(key);\n if (hit && now() < hit.until) return hit.result;\n introspectCache.delete(key);\n\n let endpoint: string;\n try {\n endpoint = (await discovery()).introspection_endpoint ?? new URL(\"/oauth2/introspect\", config.issuer).toString();\n } catch (e) {\n throw new SsoIntrospectionUnavailableError(`could not read ${config.issuer}'s discovery document: ${e instanceof Error ? e.message : String(e)}`, null);\n }\n if (!config.clientSecret) {\n throw new SsoIntrospectionUnavailableError(\"introspection needs this app's client secret (SSO_CLIENT_SECRET) — a public client cannot introspect\", null);\n }\n const basic = btoa(`${encodeURIComponent(config.clientId)}:${encodeURIComponent(config.clientSecret)}`);\n const ctrl = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(new SsoIntrospectionUnavailableError(`${endpoint} did not answer within ${timeoutMs} ms`, null));\n ctrl.abort();\n }, timeoutMs);\n });\n let body: Record<string, unknown>;\n try {\n const res = await Promise.race([\n fetchImpl(endpoint, {\n method: \"POST\",\n headers: { authorization: `Basic ${basic}`, \"content-type\": \"application/x-www-form-urlencoded\", accept: \"application/json\" },\n body: new URLSearchParams({ token, token_type_hint: \"access_token\" }),\n signal: ctrl.signal,\n }),\n timedOut,\n ]);\n if (!res.ok) {\n // 401 here is OUR credentials being refused, not the token: that is a\n // configuration fault the app must see, not a user to log out.\n throw new SsoIntrospectionUnavailableError(\n `${endpoint} answered ${res.status}${res.status === 401 ? \" — this app's client id/secret was refused for introspection\" : \"\"}`,\n res.status,\n );\n }\n const parsed = await Promise.race([res.json().catch(() => null), timedOut]);\n if (!parsed || typeof parsed !== \"object\") throw new SsoIntrospectionUnavailableError(`${endpoint} answered ${res.status} with a body that is not a JSON object`, res.status);\n body = parsed as Record<string, unknown>;\n } catch (e) {\n if (e instanceof SsoIntrospectionUnavailableError) throw e;\n throw new SsoIntrospectionUnavailableError(`${endpoint} could not be reached: ${e instanceof Error ? e.message : String(e)}`, null);\n } finally {\n clearTimeout(timer);\n }\n\n if (body.active !== true) throw new SsoTokenInactiveError(\"the token is not active at BID\", \"inactive\");\n const clientId = typeof body.client_id === \"string\" ? body.client_id : \"\";\n if (clientId !== options.requireClientId) {\n throw new SsoTokenInactiveError(`the token was issued to client «${clientId || \"(none)\"}», not «${options.requireClientId}»`, \"wrong_client\");\n }\n const exp = typeof body.exp === \"number\" ? body.exp : 0;\n if (exp * 1000 <= now()) throw new SsoTokenInactiveError(\"the token has expired\", \"expired\");\n if (typeof body.sub !== \"string\" || body.sub === \"\") {\n throw new SsoIntrospectionUnavailableError(`${endpoint} said active but named no subject`, 200);\n }\n const result: IntrospectedToken = { active: true, sub: body.sub, clientId, scope: typeof body.scope === \"string\" ? body.scope : \"\", exp };\n if (options.cacheMs && options.cacheMs > 0) introspectCache.set(key, { result, until: Math.min(now() + options.cacheMs, exp * 1000) });\n return result;\n }\n\n return {\n discovery,\n introspectToken,\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 // ALWAYS sent, secret or not. See SsoConfig.clientSecret: the two\n // prove different things, so a confidential client keeps PKCE.\n code_verifier: codeVerifier,\n ...(config.clientSecret ? { client_secret: config.clientSecret } : {}),\n }),\n });\n\n // STATUS FIRST, PARSE SECOND — and the order IS the fix (components-F084.52).\n //\n // This used to be `await res.json()`, which runs BEFORE the `if (!res.ok)`\n // below. So when the issuer answered with something that is not JSON — an\n // empty 500 body, an HTML error page from a proxy, a gateway timeout — the\n // parse threw `SyntaxError: Unexpected end of JSON input` and the careful\n // message underneath was never reached. In the one case it exists for.\n //\n // The cost is not cosmetic. BID's own public guide (id.broberg.ai/docs §4b)\n // tells every consumer to wrap completeLogin in try/catch and redirect\n // instead of returning 500, so this string is ALL a consumer has: the user\n // sees a redirect, and the diagnosis lives only in what they logged. A line\n // reading \"Unexpected end of JSON input\" sends that reader into their own\n // code to look for a fault that is in the server.\n //\n // Reported by broberg-id, who measured production first: a real\n // `POST /oauth2/token` with a bad code answers 400 + JSON, so the good path\n // was never affected. This only fires against a broken or outdated issuer —\n // which is exactly where somebody is already debugging.\n //\n // NOT a try/catch around res.json(): that catches the throw and still loses\n // res.status, which is the one fact that says WHICH end is at fault.\n const raw = await res.text();\n let body: {\n id_token?: string;\n access_token?: string;\n refresh_token?: string;\n expires_in?: number;\n error?: string;\n error_description?: string;\n };\n try {\n body = raw.trim() === \"\" ? {} : (JSON.parse(raw) as typeof body);\n } catch {\n throw new SsoError(\n `token exchange failed (${res.status}): the response body is not JSON` +\n `${res.headers.get(\"content-type\") ? ` (content-type: ${res.headers.get(\"content-type\")})` : \"\"}` +\n ` — ${excerptForLog(raw, config.clientSecret)}`,\n );\n }\n // An EMPTY body parses to {} and falls through to the check below, which\n // reports the status and \"no id_token in response\" — right for a 500 with\n // no body, and right for a 200 that returned nothing.\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 invalidClientHint(body.error, config.clientSecret !== undefined),\n );\n }\n\n const claims = await verifyIdToken(body.id_token, { nonce });\n\n const merged = body.access_token\n ? await withUserInfo(claims, body.access_token)\n : { claims, unverifiedClaims: [] };\n return {\n claims: merged.claims,\n unverifiedClaims: merged.unverifiedClaims,\n idToken: body.id_token,\n ...(body.access_token ? { accessToken: body.access_token } : {}),\n ...(body.refresh_token ? { refreshToken: body.refresh_token } : {}),\n ...(typeof body.expires_in === \"number\" ? { expiresIn: body.expires_in } : {}),\n };\n },\n\n async refreshTokens(refreshToken) {\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: \"refresh_token\",\n refresh_token: refreshToken,\n client_id: config.clientId,\n ...(config.clientSecret ? { client_secret: config.clientSecret } : {}),\n }),\n });\n // Status first, parse second (the F084.52 order). The message names the\n // status and BID's error code ONLY — the raw body is not quoted here,\n // because this request carried a refresh token and an excerpt is one\n // careless server away from echoing it into a log.\n const raw = await res.text();\n let body: { access_token?: string; refresh_token?: string; expires_in?: number; error?: string };\n try {\n body = raw.trim() === \"\" ? {} : (JSON.parse(raw) as typeof body);\n } catch {\n throw new SsoError(`token refresh failed (${res.status}): the response body is not JSON`);\n }\n if (!res.ok || typeof body.access_token !== \"string\" || body.access_token === \"\") {\n throw new SsoError(`token refresh failed (${res.status}): ${body.error ?? \"no access_token in response\"}`);\n }\n return {\n accessToken: body.access_token,\n ...(typeof body.refresh_token === \"string\" && body.refresh_token ? { refreshToken: body.refresh_token } : {}),\n ...(typeof body.expires_in === \"number\" ? { expiresIn: body.expires_in } : {}),\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 async addressOwnership(accessToken, address) {\n // An app-API route on BID's origin, not an OIDC endpoint, so it is not in\n // discovery. Origin-relative: the issuer may carry a path.\n const body = (await appPost(\n \"/api/app/address-ownership\",\n accessToken,\n { address },\n 'address ownership (not \"unverified\")',\n )) as { status?: unknown } | null;\n // Anything but the three answers THROWS. Mapping a malformed or new status\n // to \"unverified\" would quietly downgrade a real user's verified address,\n // and nothing about that looks broken from the outside.\n const status = body?.status;\n if (typeof status !== \"string\" || !ADDRESS_OWNERSHIP.has(status)) {\n throw new SsoError(`address-ownership returned status ${JSON.stringify(status)} — expected verified, unverified or not_on_account.`);\n }\n return status as AddressOwnership;\n },\n\n verifyLogoutToken,\n\n async getProfile(accessToken) {\n const path = \"/api/app/profile\";\n return asProfile(await appRequest(path, accessToken, { method: \"GET\" }, \"the profile\"), path);\n },\n\n async updateProfile(accessToken, name) {\n const path = \"/api/app/profile\";\n return asProfile(await appRequest(path, accessToken, { method: \"POST\", json: { name } }, \"the new name\"), path);\n },\n\n async uploadAvatar(accessToken, bytes, contentType) {\n // Both refusals BEFORE the request: a picture BID will refuse is not worth\n // 2 MB of upload, and the user gets a reason instead of BID's status.\n const type = mediaType(contentType);\n if (!AVATAR_TYPES.includes(type)) {\n throw new SsoAvatarRejectedError(\n `uploadAvatar: \"${type}\" is not accepted — BID takes ${AVATAR_TYPES.join(\", \")}.`,\n \"unsupported_type\",\n );\n }\n if (bytes.byteLength > MAX_AVATAR_BYTES) {\n throw new SsoAvatarRejectedError(\n `uploadAvatar: ${bytes.byteLength} bytes — BID takes at most ${MAX_AVATAR_BYTES}.`,\n \"too_large\",\n );\n }\n const path = \"/api/app/profile/avatar\";\n return asProfile(await appRequest(path, accessToken, { method: \"POST\", bytes, contentType: type }, \"the new picture\"), path);\n },\n\n async removeAvatar(accessToken) {\n const path = \"/api/app/profile/avatar/remove\";\n return asProfile(await appRequest(path, accessToken, { method: \"POST\" }, \"the removed picture\"), path);\n },\n\n async inviteUsers(appKey, input) {\n if (input.users.length > MAX_INVITATIONS) {\n throw new SsoError(`inviteUsers: ${input.users.length} users, BID accepts at most ${MAX_INVITATIONS} per call.`);\n }\n const body = (await appPost(\"/api/app/invitations\", appKey, input, \"the invitation outcome\")) as {\n results?: unknown;\n } | null;\n if (!Array.isArray(body?.results)) {\n throw new SsoError(`invitations returned no \"results\" array — the invitation outcome is unknown.`);\n }\n for (const r of body.results as Array<{ email?: unknown; outcome?: unknown }>) {\n if (typeof r?.outcome !== \"string\" || !INVITATION_OUTCOMES.has(r.outcome)) {\n throw new SsoError(`invitations returned outcome ${JSON.stringify(r?.outcome)} for ${String(r?.email)} — not one this client knows.`);\n }\n }\n return body.results as InvitationResult[];\n },\n\n async migrationStatus(appKey, emails) {\n const body = (await appPost(\"/api/app/migration-status\", appKey, { emails }, \"the migration state\")) as {\n users?: unknown;\n counts?: unknown;\n complete?: unknown;\n } | null;\n if (!Array.isArray(body?.users) || typeof body.complete !== \"boolean\") {\n throw new SsoError(`migration-status returned no \"users\" array or no \"complete\" flag — the migration state is unknown.`);\n }\n for (const u of body.users as Array<{ email?: unknown; state?: unknown }>) {\n if (typeof u?.state !== \"string\" || !MIGRATION_STATES.has(u.state)) {\n throw new SsoError(`migration-status returned state ${JSON.stringify(u?.state)} for ${String(u?.email)} — never read as \"ready\".`);\n }\n }\n return body as MigrationStatus;\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 /**\n * Unix seconds, when this session was minted. What «Log ud overalt» compares\n * against (F084.151). Absent on sessions minted before 0.6.0 — those count as\n * older than any logout.\n */\n iat?: number;\n /**\n * F095.1 — the key to this session's server-side tokens (ssoRoutes'\n * `tokenStore`). Random, 128 bits. The tokens themselves are NEVER in the\n * cookie: it is signed, not encrypted, and the browser can read it.\n */\n sid?: string;\n /** Optional convenience copies; never authorisation data. */\n email?: string;\n name?: string;\n /** A URL, never bytes. The picture lives wherever BID put it; this is a\n * reference, so a changed picture propagates without every app storing a\n * copy that then goes stale. */\n picture?: string;\n /**\n * Whether BID has verified `email` (F084.152). Absent when BID did not say.\n * Bind access to an address only when this is `true`.\n */\n email_verified?: boolean;\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(\n value: string,\n secret: string,\n // `now` is injectable for the same reason verifySession's is: an age check\n // tested by waiting is a slow test that fails on a loaded machine.\n options: { maxAgeSeconds?: number; now?: () => number } = {},\n): Promise<string> {\n const payload = b64url(enc.encode(value));\n // The timestamp goes INSIDE the signed body, never beside it. An expiry the\n // holder can edit is not a limit. `~` is outside the base64url alphabet, so\n // `t<digits>~` cannot be confused with a payload that merely starts with \"t\".\n const body =\n options.maxAgeSeconds === undefined\n ? payload\n : `t${Math.floor((options.now ?? Date.now)() / 1000)}~${payload}`;\n const sig = new Uint8Array(await crypto.subtle.sign(\"HMAC\", await hmacKey(secret), enc.encode(body)));\n return `${body}.${b64url(sig)}`;\n}\n\nconst STAMPED = /^t(\\d+)~(.*)$/s;\n\n/**\n * Verify the signature, and — only if you ask — the age (components-F084.53).\n *\n * THE PREFIX ON THAT NUMBER IS NOT DECORATION. F-numbers are per-project, and\n * this comment ships in the .d.ts — so a consumer reading it in their editor\n * resolves the number against whatever board THEY know. broberg-id found this\n * by reading their own F084.53, which is \"no limit on password guesses\": a\n * different feature, in a different repo, behind the same string.\n *\n * WITHOUT `maxAgeSeconds` this is signature-only, exactly as before. That is\n * not laziness: the login-transaction cookie has lived on its browser `Max-Age`\n * since the package shipped, and making the check mandatory would invalidate\n * every cookie already in a user's browser — an instant logout for everyone.\n *\n * WITH `maxAgeSeconds`, a value the SERVER can date is refused once it is too\n * old. Reported by broberg-id: `Max-Age` is the client's claim about when a\n * cookie stopped being valid, and a client may simply not make that claim, so\n * the server used to accept a correctly-signed transaction value forever.\n *\n * THE MIXED CASE IS THE ONE THAT DECIDES WHETHER THIS IS WORTH ANYTHING: a\n * value signed by the OLD code (no timestamp) verified by the NEW code (with a\n * limit). That happens during every rollout, and it FAILS CLOSED — otherwise an\n * undated value would be the way around the very limit being added, and the\n * limit would only apply to those not trying to avoid it.\n */\nexport async function verifyValue(\n token: string | undefined | null,\n secret: string,\n options: { maxAgeSeconds?: number; now?: () => number } = {},\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\n const stamped = STAMPED.exec(body);\n if (options.maxAgeSeconds !== undefined) {\n if (!stamped) return null; // undated + a limit asked for → fail CLOSED\n const issuedAt = Number(stamped[1]);\n if ((options.now ?? Date.now)() / 1000 - issuedAt > options.maxAgeSeconds) return null;\n }\n return new TextDecoder().decode(fromB64url(stamped?.[2] ?? 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 * Server-side tokens for calling Broberg ID on the user's behalf (F095.1).\n *\n * The login already RECEIVES an access token and a refresh token; before 0.9.0\n * ssoRoutes dropped both, so an app could not call BID's app API (the account\n * page: read and change your own name and picture) as the user.\n *\n * ── WHY NOT IN THE COOKIE ────────────────────────────────────────────────\n * The session cookie is SIGNED, not encrypted, and the browser can read it. A\n * refresh token is the longest-lived, highest-value credential in the whole\n * flow: whoever holds it can mint access tokens until it is revoked. So the\n * tokens live server-side, keyed by a random `sid` that is the only thing the\n * cookie carries. A stolen cookie still needs the server to be useful.\n */\n\nexport interface TokenSet {\n /** The BID subject the tokens belong to — lets «Log ud overalt» find them. */\n sub: string;\n accessToken: string;\n refreshToken?: string;\n /** Unix seconds. */\n expiresAt: number;\n}\n\n/**\n * Where the tokens live. The APP provides it in production, backed by\n * something every instance shares (SQLite on one machine, a database or Redis\n * on several) — the same reason BackchannelStore has no in-memory default.\n */\nexport interface TokenStore {\n get(sid: string): Promise<TokenSet | undefined>;\n set(sid: string, tokens: TokenSet): Promise<void>;\n delete(sid: string): Promise<void>;\n /** Drop every token set for `sub` — what «Log ud overalt» calls. */\n deleteSub(sub: string): Promise<void>;\n}\n\nlet warned = false;\n\n/**\n * An in-process TokenStore. Fine for tests and a single dev process. In\n * production it LOSES every user's tokens on a restart (they must log in again\n * before the account page works) and is not shared between machines — so it\n * says so, once, when created.\n */\nexport function memoryTokenStore(options: { quiet?: boolean } = {}): TokenStore & { readonly size: number } {\n if (!options.quiet && !warned) {\n warned = true;\n console.warn(\n \"@broberg/sso: memoryTokenStore() keeps tokens in this process only — they are lost on restart and not shared \" +\n \"between instances. Give ssoRoutes a TokenStore backed by your database in production.\",\n );\n }\n const rows = new Map<string, TokenSet>();\n return {\n get size() {\n return rows.size;\n },\n async get(sid) {\n return rows.get(sid);\n },\n async set(sid, tokens) {\n rows.set(sid, { ...tokens });\n },\n async delete(sid) {\n rows.delete(sid);\n },\n async deleteSub(sub) {\n for (const [sid, t] of rows) if (t.sub === sub) rows.delete(sid);\n },\n };\n}\n\n/** When the access token expires: `expires_in` if BID sent it, else the JWT's exp, else 5 minutes. */\nexport function expiresAtFrom(expiresIn: number | undefined, accessToken: string, now: number): number {\n if (typeof expiresIn === \"number\" && expiresIn > 0) return now + Math.floor(expiresIn);\n const parts = accessToken.split(\".\");\n if (parts.length === 3) {\n try {\n const json = JSON.parse(atob(parts[1]!.replace(/-/g, \"+\").replace(/_/g, \"/\"))) as { exp?: unknown };\n if (typeof json.exp === \"number\" && json.exp > now) return json.exp;\n } catch {\n /* opaque token: fall through */\n }\n }\n return now + 300;\n}\n\n/** A random session id: 128 bits, URL-safe. */\nexport function newSid(): string {\n const b = crypto.getRandomValues(new Uint8Array(16));\n let s = \"\";\n for (const x of b) s += String.fromCharCode(x);\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n"]}
|
|
@@ -197,6 +197,22 @@ var SsoInsufficientScopeError = class extends SsoAppApiError {
|
|
|
197
197
|
}
|
|
198
198
|
scope;
|
|
199
199
|
};
|
|
200
|
+
var SsoTokenInactiveError = class extends SsoError {
|
|
201
|
+
constructor(message, reason) {
|
|
202
|
+
super(message);
|
|
203
|
+
this.reason = reason;
|
|
204
|
+
this.name = "SsoTokenInactiveError";
|
|
205
|
+
}
|
|
206
|
+
reason;
|
|
207
|
+
};
|
|
208
|
+
var SsoIntrospectionUnavailableError = class extends SsoError {
|
|
209
|
+
constructor(message, status) {
|
|
210
|
+
super(message);
|
|
211
|
+
this.status = status;
|
|
212
|
+
this.name = "SsoIntrospectionUnavailableError";
|
|
213
|
+
}
|
|
214
|
+
status;
|
|
215
|
+
};
|
|
200
216
|
var SsoAvatarRejectedError = class extends SsoError {
|
|
201
217
|
constructor(message, reason) {
|
|
202
218
|
super(message);
|
|
@@ -470,8 +486,77 @@ function createSsoClient(config, options = {}) {
|
|
|
470
486
|
const unverifiedClaims = Object.keys(info).filter((k) => k !== "sub" && info[k] !== void 0 && !(k in claims)).sort();
|
|
471
487
|
return { claims: { ...info, ...claims, sub: claims.sub }, unverifiedClaims };
|
|
472
488
|
}
|
|
489
|
+
const introspectCache = /* @__PURE__ */ new Map();
|
|
490
|
+
async function introspectToken(token, options2) {
|
|
491
|
+
const now = options2.now ?? Date.now;
|
|
492
|
+
const key = Array.from(
|
|
493
|
+
new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token))),
|
|
494
|
+
(b) => b.toString(16).padStart(2, "0")
|
|
495
|
+
).join("");
|
|
496
|
+
const hit = introspectCache.get(key);
|
|
497
|
+
if (hit && now() < hit.until) return hit.result;
|
|
498
|
+
introspectCache.delete(key);
|
|
499
|
+
let endpoint;
|
|
500
|
+
try {
|
|
501
|
+
endpoint = (await discovery()).introspection_endpoint ?? new URL("/oauth2/introspect", config.issuer).toString();
|
|
502
|
+
} catch (e) {
|
|
503
|
+
throw new SsoIntrospectionUnavailableError(`could not read ${config.issuer}'s discovery document: ${e instanceof Error ? e.message : String(e)}`, null);
|
|
504
|
+
}
|
|
505
|
+
if (!config.clientSecret) {
|
|
506
|
+
throw new SsoIntrospectionUnavailableError("introspection needs this app's client secret (SSO_CLIENT_SECRET) \u2014 a public client cannot introspect", null);
|
|
507
|
+
}
|
|
508
|
+
const basic = btoa(`${encodeURIComponent(config.clientId)}:${encodeURIComponent(config.clientSecret)}`);
|
|
509
|
+
const ctrl = new AbortController();
|
|
510
|
+
let timer;
|
|
511
|
+
const timedOut = new Promise((_, reject) => {
|
|
512
|
+
timer = setTimeout(() => {
|
|
513
|
+
reject(new SsoIntrospectionUnavailableError(`${endpoint} did not answer within ${timeoutMs} ms`, null));
|
|
514
|
+
ctrl.abort();
|
|
515
|
+
}, timeoutMs);
|
|
516
|
+
});
|
|
517
|
+
let body;
|
|
518
|
+
try {
|
|
519
|
+
const res = await Promise.race([
|
|
520
|
+
fetchImpl(endpoint, {
|
|
521
|
+
method: "POST",
|
|
522
|
+
headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
523
|
+
body: new URLSearchParams({ token, token_type_hint: "access_token" }),
|
|
524
|
+
signal: ctrl.signal
|
|
525
|
+
}),
|
|
526
|
+
timedOut
|
|
527
|
+
]);
|
|
528
|
+
if (!res.ok) {
|
|
529
|
+
throw new SsoIntrospectionUnavailableError(
|
|
530
|
+
`${endpoint} answered ${res.status}${res.status === 401 ? " \u2014 this app's client id/secret was refused for introspection" : ""}`,
|
|
531
|
+
res.status
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
const parsed = await Promise.race([res.json().catch(() => null), timedOut]);
|
|
535
|
+
if (!parsed || typeof parsed !== "object") throw new SsoIntrospectionUnavailableError(`${endpoint} answered ${res.status} with a body that is not a JSON object`, res.status);
|
|
536
|
+
body = parsed;
|
|
537
|
+
} catch (e) {
|
|
538
|
+
if (e instanceof SsoIntrospectionUnavailableError) throw e;
|
|
539
|
+
throw new SsoIntrospectionUnavailableError(`${endpoint} could not be reached: ${e instanceof Error ? e.message : String(e)}`, null);
|
|
540
|
+
} finally {
|
|
541
|
+
clearTimeout(timer);
|
|
542
|
+
}
|
|
543
|
+
if (body.active !== true) throw new SsoTokenInactiveError("the token is not active at BID", "inactive");
|
|
544
|
+
const clientId = typeof body.client_id === "string" ? body.client_id : "";
|
|
545
|
+
if (clientId !== options2.requireClientId) {
|
|
546
|
+
throw new SsoTokenInactiveError(`the token was issued to client \xAB${clientId || "(none)"}\xBB, not \xAB${options2.requireClientId}\xBB`, "wrong_client");
|
|
547
|
+
}
|
|
548
|
+
const exp = typeof body.exp === "number" ? body.exp : 0;
|
|
549
|
+
if (exp * 1e3 <= now()) throw new SsoTokenInactiveError("the token has expired", "expired");
|
|
550
|
+
if (typeof body.sub !== "string" || body.sub === "") {
|
|
551
|
+
throw new SsoIntrospectionUnavailableError(`${endpoint} said active but named no subject`, 200);
|
|
552
|
+
}
|
|
553
|
+
const result = { active: true, sub: body.sub, clientId, scope: typeof body.scope === "string" ? body.scope : "", exp };
|
|
554
|
+
if (options2.cacheMs && options2.cacheMs > 0) introspectCache.set(key, { result, until: Math.min(now() + options2.cacheMs, exp * 1e3) });
|
|
555
|
+
return result;
|
|
556
|
+
}
|
|
473
557
|
return {
|
|
474
558
|
discovery,
|
|
559
|
+
introspectToken,
|
|
475
560
|
get jwks() {
|
|
476
561
|
if (!jwksCache) throw new SsoError("the key cache is created on first verification");
|
|
477
562
|
return jwksCache;
|
|
@@ -815,7 +900,9 @@ exports.SsoAvatarRejectedError = SsoAvatarRejectedError;
|
|
|
815
900
|
exports.SsoConfigError = SsoConfigError;
|
|
816
901
|
exports.SsoError = SsoError;
|
|
817
902
|
exports.SsoInsufficientScopeError = SsoInsufficientScopeError;
|
|
903
|
+
exports.SsoIntrospectionUnavailableError = SsoIntrospectionUnavailableError;
|
|
818
904
|
exports.SsoReauthError = SsoReauthError;
|
|
905
|
+
exports.SsoTokenInactiveError = SsoTokenInactiveError;
|
|
819
906
|
exports.cookieHeader = cookieHeader;
|
|
820
907
|
exports.createJwksCache = createJwksCache;
|
|
821
908
|
exports.createSsoClient = createSsoClient;
|
|
@@ -829,5 +916,5 @@ exports.signSession = signSession;
|
|
|
829
916
|
exports.signValue = signValue;
|
|
830
917
|
exports.verifySession = verifySession;
|
|
831
918
|
exports.verifyValue = verifyValue;
|
|
832
|
-
//# sourceMappingURL=chunk-
|
|
833
|
-
//# sourceMappingURL=chunk-
|
|
919
|
+
//# sourceMappingURL=chunk-QYMEISTU.cjs.map
|
|
920
|
+
//# sourceMappingURL=chunk-QYMEISTU.cjs.map
|