@orangecheck/auth-core 2.9.0 → 2.11.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/dist/index.d.mts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +51 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +51 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -13,6 +13,7 @@ interface RevocationCheck {
|
|
|
13
13
|
interface RevocationCheckOptions {
|
|
14
14
|
url?: string;
|
|
15
15
|
token?: string;
|
|
16
|
+
headers?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
16
17
|
ttlMs?: number;
|
|
17
18
|
maxStaleMs?: number;
|
|
18
19
|
timeoutMs?: number;
|
|
@@ -23,6 +24,12 @@ interface RevocationCheckOptions {
|
|
|
23
24
|
}
|
|
24
25
|
declare function revocationKey(kind: "jti" | "sub", value: string): Promise<string>;
|
|
25
26
|
declare function createRevocationCheck(opts?: RevocationCheckOptions): RevocationCheck;
|
|
27
|
+
interface IntegratorRevocationOptions extends Omit<RevocationCheckOptions, "url" | "token" | "headers" | "developing"> {
|
|
28
|
+
projectKey?: string;
|
|
29
|
+
signingSecret?: string;
|
|
30
|
+
meOrigin?: string;
|
|
31
|
+
}
|
|
32
|
+
declare function integratorRevocationCheck(opts?: IntegratorRevocationOptions): RevocationCheck | null;
|
|
26
33
|
declare function familyRevocationCheck(): RevocationCheck;
|
|
27
34
|
declare function _resetFamilyRevocationCheckForTests(): void;
|
|
28
35
|
|
|
@@ -128,6 +135,7 @@ declare function resolveSessionFromRequest(headers: IncomingRequestHeaders, cfg:
|
|
|
128
135
|
interface VerifyOcOptions {
|
|
129
136
|
issuer?: string;
|
|
130
137
|
jwksCacheTtlMs?: number;
|
|
138
|
+
jwksMaxStaleMs?: number;
|
|
131
139
|
audience?: AudienceRule;
|
|
132
140
|
}
|
|
133
141
|
declare function verifyOcToken(token: string, options?: VerifyOcOptions): Promise<SessionPayload | null>;
|
|
@@ -138,4 +146,4 @@ interface SessionRequestHeaders {
|
|
|
138
146
|
declare function getOcSession(headers: SessionRequestHeaders | Headers, options?: VerifyOcOptions): Promise<SessionPayload | null>;
|
|
139
147
|
declare function _resetJwksCachesForTests(): void;
|
|
140
148
|
|
|
141
|
-
export { type AudienceRule, type AuthKey, type BondClaim, type CookieOptions, DEFAULT_ISSUER, DISPLAY_IDENTITY_KINDS, type DisplayIdentity, type DisplayIdentityKind, type IncomingRequestHeaders, JWT_ALG, type ResolveSessionResult, type RevocableClaims, type RevocationCheck, type RevocationCheckOptions, type RevocationStatus, SESSION_COOKIE, type SessionPayload, type SessionRequestHeaders, type SignConfig, TAB_SESSION_HEADER, type VerifyConfig, type VerifyOcOptions, _resetFamilyRevocationCheckForTests, _resetJwksCachesForTests, audienceSatisfied, bondMeets, clearSessionCookieHeader, createRevocationCheck, familyRevocationCheck, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readBondClaim, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, revocationKey, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
|
149
|
+
export { type AudienceRule, type AuthKey, type BondClaim, type CookieOptions, DEFAULT_ISSUER, DISPLAY_IDENTITY_KINDS, type DisplayIdentity, type DisplayIdentityKind, type IncomingRequestHeaders, type IntegratorRevocationOptions, JWT_ALG, type ResolveSessionResult, type RevocableClaims, type RevocationCheck, type RevocationCheckOptions, type RevocationStatus, SESSION_COOKIE, type SessionPayload, type SessionRequestHeaders, type SignConfig, TAB_SESSION_HEADER, type VerifyConfig, type VerifyOcOptions, _resetFamilyRevocationCheckForTests, _resetJwksCachesForTests, audienceSatisfied, bondMeets, clearSessionCookieHeader, createRevocationCheck, familyRevocationCheck, getOcSession, integratorRevocationCheck, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readBondClaim, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, revocationKey, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ interface RevocationCheck {
|
|
|
13
13
|
interface RevocationCheckOptions {
|
|
14
14
|
url?: string;
|
|
15
15
|
token?: string;
|
|
16
|
+
headers?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
16
17
|
ttlMs?: number;
|
|
17
18
|
maxStaleMs?: number;
|
|
18
19
|
timeoutMs?: number;
|
|
@@ -23,6 +24,12 @@ interface RevocationCheckOptions {
|
|
|
23
24
|
}
|
|
24
25
|
declare function revocationKey(kind: "jti" | "sub", value: string): Promise<string>;
|
|
25
26
|
declare function createRevocationCheck(opts?: RevocationCheckOptions): RevocationCheck;
|
|
27
|
+
interface IntegratorRevocationOptions extends Omit<RevocationCheckOptions, "url" | "token" | "headers" | "developing"> {
|
|
28
|
+
projectKey?: string;
|
|
29
|
+
signingSecret?: string;
|
|
30
|
+
meOrigin?: string;
|
|
31
|
+
}
|
|
32
|
+
declare function integratorRevocationCheck(opts?: IntegratorRevocationOptions): RevocationCheck | null;
|
|
26
33
|
declare function familyRevocationCheck(): RevocationCheck;
|
|
27
34
|
declare function _resetFamilyRevocationCheckForTests(): void;
|
|
28
35
|
|
|
@@ -128,6 +135,7 @@ declare function resolveSessionFromRequest(headers: IncomingRequestHeaders, cfg:
|
|
|
128
135
|
interface VerifyOcOptions {
|
|
129
136
|
issuer?: string;
|
|
130
137
|
jwksCacheTtlMs?: number;
|
|
138
|
+
jwksMaxStaleMs?: number;
|
|
131
139
|
audience?: AudienceRule;
|
|
132
140
|
}
|
|
133
141
|
declare function verifyOcToken(token: string, options?: VerifyOcOptions): Promise<SessionPayload | null>;
|
|
@@ -138,4 +146,4 @@ interface SessionRequestHeaders {
|
|
|
138
146
|
declare function getOcSession(headers: SessionRequestHeaders | Headers, options?: VerifyOcOptions): Promise<SessionPayload | null>;
|
|
139
147
|
declare function _resetJwksCachesForTests(): void;
|
|
140
148
|
|
|
141
|
-
export { type AudienceRule, type AuthKey, type BondClaim, type CookieOptions, DEFAULT_ISSUER, DISPLAY_IDENTITY_KINDS, type DisplayIdentity, type DisplayIdentityKind, type IncomingRequestHeaders, JWT_ALG, type ResolveSessionResult, type RevocableClaims, type RevocationCheck, type RevocationCheckOptions, type RevocationStatus, SESSION_COOKIE, type SessionPayload, type SessionRequestHeaders, type SignConfig, TAB_SESSION_HEADER, type VerifyConfig, type VerifyOcOptions, _resetFamilyRevocationCheckForTests, _resetJwksCachesForTests, audienceSatisfied, bondMeets, clearSessionCookieHeader, createRevocationCheck, familyRevocationCheck, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readBondClaim, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, revocationKey, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
|
149
|
+
export { type AudienceRule, type AuthKey, type BondClaim, type CookieOptions, DEFAULT_ISSUER, DISPLAY_IDENTITY_KINDS, type DisplayIdentity, type DisplayIdentityKind, type IncomingRequestHeaders, type IntegratorRevocationOptions, JWT_ALG, type ResolveSessionResult, type RevocableClaims, type RevocationCheck, type RevocationCheckOptions, type RevocationStatus, SESSION_COOKIE, type SessionPayload, type SessionRequestHeaders, type SignConfig, TAB_SESSION_HEADER, type VerifyConfig, type VerifyOcOptions, _resetFamilyRevocationCheckForTests, _resetJwksCachesForTests, audienceSatisfied, bondMeets, clearSessionCookieHeader, createRevocationCheck, familyRevocationCheck, getOcSession, integratorRevocationCheck, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readBondClaim, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, revocationKey, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ function createRevocationCheck(opts = {}) {
|
|
|
39
39
|
const issuer = (env("OC_AUTH_ISSUER") ?? "https://ochk.io").replace(/\/$/, "");
|
|
40
40
|
const url = opts.url ?? env("OC_REVOCATION_FEED_URL") ?? `${issuer}/api/auth/revocations`;
|
|
41
41
|
const token = opts.token ?? env("OC_REVOCATION_FEED_TOKEN")?.trim();
|
|
42
|
+
const signer = opts.headers;
|
|
42
43
|
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
43
44
|
const maxStaleMs = opts.maxStaleMs ?? DEFAULT_MAX_STALE_MS;
|
|
44
45
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
@@ -59,8 +60,9 @@ function createRevocationCheck(opts = {}) {
|
|
|
59
60
|
const ctl = new AbortController();
|
|
60
61
|
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
61
62
|
try {
|
|
63
|
+
const credential = signer ? await signer() : { authorization: `Bearer ${token}` };
|
|
62
64
|
const res = await doFetch(url, {
|
|
63
|
-
headers: {
|
|
65
|
+
headers: { ...credential, accept: "application/json" },
|
|
64
66
|
signal: ctl.signal
|
|
65
67
|
});
|
|
66
68
|
if (!res.ok) throw new Error(`revocation feed answered ${res.status}`);
|
|
@@ -84,7 +86,7 @@ function createRevocationCheck(opts = {}) {
|
|
|
84
86
|
return snapshot;
|
|
85
87
|
}
|
|
86
88
|
async function status(claims) {
|
|
87
|
-
if (!token) {
|
|
89
|
+
if (!token && !signer) {
|
|
88
90
|
if (developing) return "live";
|
|
89
91
|
warn("OC_REVOCATION_FEED_TOKEN is not set; sessions cannot be checked for revocation");
|
|
90
92
|
return "unverified";
|
|
@@ -112,6 +114,36 @@ function createRevocationCheck(opts = {}) {
|
|
|
112
114
|
}
|
|
113
115
|
};
|
|
114
116
|
}
|
|
117
|
+
async function hmacHex(secret, data) {
|
|
118
|
+
const enc = new TextEncoder();
|
|
119
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
120
|
+
"raw",
|
|
121
|
+
enc.encode(secret),
|
|
122
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
123
|
+
false,
|
|
124
|
+
["sign"]
|
|
125
|
+
);
|
|
126
|
+
const mac = new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", key, enc.encode(data)));
|
|
127
|
+
return Array.from(mac, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
128
|
+
}
|
|
129
|
+
function integratorRevocationCheck(opts = {}) {
|
|
130
|
+
const { projectKey: pk, signingSecret, meOrigin, ...rest } = opts;
|
|
131
|
+
const projectKey = (pk ?? env("OC_PROJECT_KEY"))?.trim();
|
|
132
|
+
const secret = (signingSecret ?? env("OC_SIGNING_SECRET"))?.trim();
|
|
133
|
+
if (!projectKey || !secret) return null;
|
|
134
|
+
const origin = (meOrigin ?? env("OC_ME_ORIGIN") ?? "https://me.ochk.io").replace(/\/$/, "");
|
|
135
|
+
const query = `project_key=${encodeURIComponent(projectKey)}`;
|
|
136
|
+
const now = rest.now ?? Date.now;
|
|
137
|
+
return createRevocationCheck({
|
|
138
|
+
...rest,
|
|
139
|
+
url: `${origin}/api/integrator/revocations?${query}`,
|
|
140
|
+
headers: async () => {
|
|
141
|
+
const t = Math.floor(now() / 1e3);
|
|
142
|
+
return { "x-oc-signature": `t=${t},v1=${await hmacHex(secret, `${t}.${query}`)}` };
|
|
143
|
+
},
|
|
144
|
+
developing: false
|
|
145
|
+
});
|
|
146
|
+
}
|
|
115
147
|
var SHARED = /* @__PURE__ */ Symbol.for("@orangecheck/auth-core/revocation-check/v1");
|
|
116
148
|
function familyRevocationCheck() {
|
|
117
149
|
const g = globalThis;
|
|
@@ -304,11 +336,12 @@ async function resolveSessionFromRequest(headers, cfg) {
|
|
|
304
336
|
return { ok: false, reason: "no_session" };
|
|
305
337
|
}
|
|
306
338
|
var DEFAULT_JWKS_TTL_MS = 60 * 60 * 1e3;
|
|
339
|
+
var DEFAULT_JWKS_MAX_STALE_MS = 6 * 60 * 60 * 1e3;
|
|
307
340
|
var jwksCache = /* @__PURE__ */ new Map();
|
|
308
341
|
var inflight = /* @__PURE__ */ new Map();
|
|
309
342
|
var lastForcedRefresh = /* @__PURE__ */ new Map();
|
|
310
343
|
var FORCED_REFRESH_MIN_INTERVAL_MS = 6e4;
|
|
311
|
-
async function fetchJwks(issuer, ttlMs) {
|
|
344
|
+
async function fetchJwks(issuer, ttlMs, maxStaleMs = DEFAULT_JWKS_MAX_STALE_MS) {
|
|
312
345
|
const cached = jwksCache.get(issuer);
|
|
313
346
|
if (cached && Date.now() - cached.fetchedAt < ttlMs) return cached;
|
|
314
347
|
const existing = inflight.get(issuer);
|
|
@@ -329,19 +362,24 @@ async function fetchJwks(issuer, ttlMs) {
|
|
|
329
362
|
);
|
|
330
363
|
}
|
|
331
364
|
const keys = {};
|
|
365
|
+
const retiredAt = {};
|
|
332
366
|
for (const k of json.keys) {
|
|
333
367
|
if (typeof k.kid !== "string") continue;
|
|
334
368
|
try {
|
|
335
369
|
keys[k.kid] = await jose.importJWK(k, JWT_ALG);
|
|
336
370
|
} catch {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (k.status === "retired") {
|
|
374
|
+
retiredAt[k.kid] = typeof k.retired_at === "number" ? k.retired_at : 0;
|
|
337
375
|
}
|
|
338
376
|
}
|
|
339
|
-
const entry = { keys, fetchedAt: Date.now() };
|
|
377
|
+
const entry = { keys, retiredAt, fetchedAt: Date.now() };
|
|
340
378
|
jwksCache.set(issuer, entry);
|
|
341
379
|
return entry;
|
|
342
380
|
} catch (err) {
|
|
343
381
|
const stale = jwksCache.get(issuer);
|
|
344
|
-
if (stale) return stale;
|
|
382
|
+
if (stale && Date.now() - stale.fetchedAt < maxStaleMs) return stale;
|
|
345
383
|
throw err;
|
|
346
384
|
}
|
|
347
385
|
})().finally(() => {
|
|
@@ -369,9 +407,10 @@ function assertSafeIssuer(issuer) {
|
|
|
369
407
|
async function verifyOcToken(token, options = {}) {
|
|
370
408
|
const issuer = options.issuer ?? DEFAULT_ISSUER;
|
|
371
409
|
const ttl = options.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;
|
|
410
|
+
const maxStale = options.jwksMaxStaleMs ?? DEFAULT_JWKS_MAX_STALE_MS;
|
|
372
411
|
try {
|
|
373
412
|
assertSafeIssuer(issuer);
|
|
374
|
-
let entry = await fetchJwks(issuer, ttl);
|
|
413
|
+
let entry = await fetchJwks(issuer, ttl, maxStale);
|
|
375
414
|
const headerB64 = token.split(".")[0];
|
|
376
415
|
if (!headerB64) return null;
|
|
377
416
|
const headerJson = typeof Buffer !== "undefined" ? Buffer.from(headerB64, "base64url").toString("utf8") : new TextDecoder().decode(base64UrlDecode(headerB64));
|
|
@@ -383,8 +422,7 @@ async function verifyOcToken(token, options = {}) {
|
|
|
383
422
|
const last = lastForcedRefresh.get(issuer) ?? 0;
|
|
384
423
|
if (now - last >= FORCED_REFRESH_MIN_INTERVAL_MS) {
|
|
385
424
|
lastForcedRefresh.set(issuer, now);
|
|
386
|
-
|
|
387
|
-
entry = await fetchJwks(issuer, ttl);
|
|
425
|
+
entry = await fetchJwks(issuer, 0, maxStale);
|
|
388
426
|
key = entry.keys[header.kid];
|
|
389
427
|
}
|
|
390
428
|
}
|
|
@@ -395,6 +433,10 @@ async function verifyOcToken(token, options = {}) {
|
|
|
395
433
|
});
|
|
396
434
|
const p = res.payload;
|
|
397
435
|
if (!p.sub || !p.did_oc || !p.jti) return null;
|
|
436
|
+
const retiredAt = entry.retiredAt[header.kid];
|
|
437
|
+
if (retiredAt !== void 0 && !(typeof p.iat === "number" && p.iat < retiredAt)) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
398
440
|
if (!audienceSatisfied(p.aud, options.audience)) return null;
|
|
399
441
|
return p;
|
|
400
442
|
} catch {
|
|
@@ -434,6 +476,7 @@ exports.clearSessionCookieHeader = clearSessionCookieHeader;
|
|
|
434
476
|
exports.createRevocationCheck = createRevocationCheck;
|
|
435
477
|
exports.familyRevocationCheck = familyRevocationCheck;
|
|
436
478
|
exports.getOcSession = getOcSession;
|
|
479
|
+
exports.integratorRevocationCheck = integratorRevocationCheck;
|
|
437
480
|
exports.loadPrivateKey = loadPrivateKey;
|
|
438
481
|
exports.loadPublicKey = loadPublicKey;
|
|
439
482
|
exports.parsePublicJwk = parsePublicJwk;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/revocation.ts","../src/index.ts"],"names":["inflight","importJWK","SignJWT","jwtVerify"],"mappings":"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","file":"index.js","sourcesContent":["// ─── Family-wide session revocation ─────────────────────────────────────\n//\n// A session token verifies by signature alone, so a site that checks only the\n// signature honours a token for its whole life, even after it was signed out,\n// replaced, or its account deleted. The auth host publishes the sessions that\n// ended at `<issuer>/api/auth/revocations`, to sites holding a feed token. This\n// check fetches that list at most once per `ttlMs` per process and answers from\n// memory, so a request never waits on the host while the copy is fresh.\n//\n// Entries are keyed by a truncated hash, so the list names no account:\n// - a token, by `ocrev1:jti:<jti>`: that token is ended.\n// - an account, by `ocrev1:sub:<sub>`, with a cutoff: every token of that\n// account issued before it (`iat < cutoff`) is ended.\n\n/** The claims a revocation decision reads. A verified session payload has them all. */\nexport interface RevocableClaims {\n jti?: unknown;\n sub?: unknown;\n iat?: unknown;\n}\n\n/**\n * `live`: not ended, per a copy of the list fresh enough to vouch for it.\n * `revoked`: ended. `unverified`: not known to be ended, but the copy is too\n * old (or was never fetched) to say so for a request that changes state.\n */\nexport type RevocationStatus = \"live\" | \"revoked\" | \"unverified\";\n\nexport interface RevocationCheck {\n status(claims: RevocableClaims): Promise<RevocationStatus>;\n /**\n * Whether to refuse the session for a request with this method: always when\n * revoked, and for an unsafe method (or none given) when unverified. Reads\n * keep working through an outage of the auth host; writes stop.\n */\n refuses(claims: RevocableClaims, method?: string): Promise<boolean>;\n}\n\nexport interface RevocationCheckOptions {\n /** Default `${OC_AUTH_ISSUER ?? \"https://ochk.io\"}/api/auth/revocations`. */\n url?: string;\n /** This site's feed token. Default `OC_REVOCATION_FEED_TOKEN`. */\n token?: string;\n /** How long a copy is used before fetching again. Default 60 s. */\n ttlMs?: number;\n /** Past this age a copy cannot vouch for a write. Default 10 min. */\n maxStaleMs?: number;\n /** One fetch's time limit. Default 3 s. */\n timeoutMs?: number;\n /** After a failed fetch, how long to wait before the next. Default 10 s. */\n retryMs?: number;\n /**\n * With no token: true answers `live` (local development and tests, where no\n * feed exists); false answers `unverified`, so writes are refused. Default:\n * true unless NODE_ENV is production.\n */\n developing?: boolean;\n fetch?: typeof fetch;\n now?: () => number;\n}\n\ninterface Snapshot {\n tokens: Set<string>;\n accounts: Map<string, number>;\n fetchedAt: number;\n}\n\nconst SAFE_METHODS = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\nconst DEFAULT_TTL_MS = 60_000;\nconst DEFAULT_MAX_STALE_MS = 10 * 60_000;\nconst DEFAULT_TIMEOUT_MS = 3_000;\nconst DEFAULT_RETRY_MS = 10_000;\n\nfunction env(name: string): string | undefined {\n return typeof process !== \"undefined\" ? process.env?.[name] : undefined;\n}\n\n/**\n * The feed key for a token (`kind: \"jti\"`) or an account (`kind: \"sub\"`):\n * base64url of the first 16 bytes of sha256(`ocrev1:<kind>:<value>`). The\n * auth host derives the same; its tests share these vectors.\n */\nexport async function revocationKey(\n kind: \"jti\" | \"sub\",\n value: string,\n): Promise<string> {\n const digest = await globalThis.crypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(`ocrev1:${kind}:${value}`),\n );\n let bin = \"\";\n for (const b of new Uint8Array(digest).subarray(0, 16)) bin += String.fromCharCode(b);\n return btoa(bin).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nfunction parseFeed(body: unknown, fetchedAt: number): Snapshot {\n const b = body as { v?: unknown; tokens?: unknown; accounts?: unknown } | null;\n if (!b || b.v !== 1 || !Array.isArray(b.tokens) || typeof b.accounts !== \"object\" || !b.accounts) {\n throw new Error(\"revocation feed: unexpected body\");\n }\n const tokens = new Set<string>();\n for (const t of b.tokens) if (typeof t === \"string\") tokens.add(t);\n const accounts = new Map<string, number>();\n for (const [k, v] of Object.entries(b.accounts as Record<string, unknown>)) {\n if (typeof v === \"number\" && Number.isFinite(v)) accounts.set(k, v);\n }\n return { tokens, accounts, fetchedAt };\n}\n\n/**\n * A revocation check with its own cached copy of the feed. Create one per\n * process and share it (fluid compute keeps module state across requests);\n * {@link familyRevocationCheck} is that one, configured from the environment.\n */\nexport function createRevocationCheck(opts: RevocationCheckOptions = {}): RevocationCheck {\n const issuer = (env(\"OC_AUTH_ISSUER\") ?? \"https://ochk.io\").replace(/\\/$/, \"\");\n const url = opts.url ?? env(\"OC_REVOCATION_FEED_URL\") ?? `${issuer}/api/auth/revocations`;\n const token = opts.token ?? env(\"OC_REVOCATION_FEED_TOKEN\")?.trim();\n const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;\n const maxStaleMs = opts.maxStaleMs ?? DEFAULT_MAX_STALE_MS;\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const retryMs = opts.retryMs ?? DEFAULT_RETRY_MS;\n const developing = opts.developing ?? env(\"NODE_ENV\") !== \"production\";\n const doFetch = opts.fetch ?? ((...a: Parameters<typeof fetch>) => fetch(...a));\n const now = opts.now ?? Date.now;\n\n let snapshot: Snapshot | null = null;\n let inflight: Promise<void> | null = null;\n let failedAt = -Infinity;\n let warned = false;\n const warn = (msg: string) => {\n if (warned) return;\n warned = true;\n console.warn(`[@orangecheck/auth-core] ${msg}`);\n };\n\n async function load(): Promise<void> {\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await doFetch(url, {\n headers: { authorization: `Bearer ${token}`, accept: \"application/json\" },\n signal: ctl.signal,\n });\n if (!res.ok) throw new Error(`revocation feed answered ${res.status}`);\n snapshot = parseFeed(await res.json(), now());\n warned = false;\n } finally {\n clearTimeout(timer);\n }\n }\n\n async function current(): Promise<Snapshot | null> {\n const t = now();\n if (snapshot && t - snapshot.fetchedAt < ttlMs) return snapshot;\n if (t - failedAt < retryMs) return snapshot;\n inflight ??= load()\n .catch((err: unknown) => {\n failedAt = now();\n warn(`${err instanceof Error ? err.message : String(err)}; using the last copy`);\n })\n .finally(() => {\n inflight = null;\n });\n await inflight;\n return snapshot;\n }\n\n async function status(claims: RevocableClaims): Promise<RevocationStatus> {\n if (!token) {\n if (developing) return \"live\";\n warn(\"OC_REVOCATION_FEED_TOKEN is not set; sessions cannot be checked for revocation\");\n return \"unverified\";\n }\n const snap = await current();\n if (!snap) return \"unverified\";\n // A token known to be ended stays ended, however old the copy.\n if (typeof claims.jti === \"string\" && snap.tokens.has(await revocationKey(\"jti\", claims.jti))) {\n return \"revoked\";\n }\n if (typeof claims.sub === \"string\" && snap.accounts.size > 0) {\n const cutoff = snap.accounts.get(await revocationKey(\"sub\", claims.sub));\n if (cutoff !== undefined && !(typeof claims.iat === \"number\" && claims.iat >= cutoff)) {\n return \"revoked\";\n }\n }\n return now() - snap.fetchedAt > maxStaleMs ? \"unverified\" : \"live\";\n }\n\n return {\n status,\n async refuses(claims, method) {\n const s = await status(claims);\n if (s === \"revoked\") return true;\n if (s === \"live\") return false;\n return !SAFE_METHODS.has((method ?? \"\").toUpperCase());\n },\n };\n}\n\nconst SHARED = Symbol.for(\"@orangecheck/auth-core/revocation-check/v1\");\n\n/**\n * The process-wide check, configured from the environment\n * (`OC_REVOCATION_FEED_TOKEN`, `OC_AUTH_ISSUER`). Every reader in a site\n * should use this one, so the site fetches the feed once per minute, not once\n * per reader.\n */\nexport function familyRevocationCheck(): RevocationCheck {\n const g = globalThis as { [SHARED]?: RevocationCheck };\n return (g[SHARED] ??= createRevocationCheck());\n}\n\n/** Tests only: drop the process-wide check so the next call reads the environment again. */\nexport function _resetFamilyRevocationCheckForTests(): void {\n delete (globalThis as { [SHARED]?: RevocationCheck })[SHARED];\n}\n","/**\n * @orangecheck/auth-core\n *\n * Cross-subdomain oc_session JWT verification + cookie helpers.\n *\n * The auth HOST (whoever owns sign-in) holds a private key and issues\n * tokens with `signSession()`. Every CONSUMER subdomain holds only the\n * public JWK and verifies tokens locally with `verifySessionToken()` —\n * no network round-trip, no shared secret.\n *\n * The ochk.io ecosystem picks Ed25519 (EdDSA) with JWKs distributed\n * via environment variable. The public key is also published at\n * `/.well-known/jwks.json` on the auth host for dynamic discovery.\n */\n\nimport {\n importJWK,\n jwtVerify,\n SignJWT,\n type CryptoKey,\n type JWTPayload,\n type KeyObject,\n} from \"jose\";\n\nexport const JWT_ALG = \"EdDSA\" as const;\nexport const SESSION_COOKIE = \"oc_session\" as const;\nexport const DEFAULT_ISSUER = \"https://ochk.io\" as const;\n\nexport type AuthKey = CryptoKey | KeyObject;\n\n/**\n * The kinds of identity a user can surface as their account-badge\n * label. `did` is the canonical `did:oc` identifier (always available\n * and the ultimate fallback); the other three are linked identities\n * the user may or may not have.\n */\nexport const DISPLAY_IDENTITY_KINDS = [\"did\", \"btc\", \"email\", \"nostr\"] as const;\nexport type DisplayIdentityKind = (typeof DISPLAY_IDENTITY_KINDS)[number];\n\n/**\n * The identity a user has chosen to display in their account badge —\n * carried as a JWT claim so every `.ochk.io` subdomain (and any\n * integrator reading the session) renders the same label with no\n * network round-trip, exactly like `name` / `npub`.\n *\n * `kind` is which identity; `value` is the full, renderable value (the\n * `did:oc`, the Bitcoin address, the email, or the npub). Only the\n * *promoted* identity's value is carried — never the whole identity\n * set — so an unrelated email never enters the token.\n */\nexport interface DisplayIdentity {\n kind: DisplayIdentityKind;\n value: string;\n}\n\n/**\n * A bond (oc-attest-protocol BOND.md): one unspent coin the account pledged.\n * The token carries floors, never the coin, so an app learns that a coin of\n * at least `sats_floor` sats, unspent at least `days_floor` days, backs this\n * account, and nothing about which coin it is.\n */\nexport interface BondClaim {\n v: 1;\n /** Largest of 1000, 10000, 100000, 1000000, 10000000 the coin meets; 0 for none. */\n sats_floor: number;\n /** Largest of 7, 30, 90, 365 days the coin has been unspent; 0 for none. */\n days_floor: number;\n /** Block height at which the issuer last found the coin unspent. */\n height: number;\n /** Unix seconds of that check. */\n checked_at: number;\n /**\n * Same for the same coin at your site, unrelated anywhere else, and keyed by the\n * issuer so it doesn't reveal the coin. *.ochk.io sites share the family session,\n * so they share one. Keep one account per nullifier; block it to block the coin.\n */\n nullifier: string;\n}\n\nexport interface SessionPayload extends JWTPayload {\n sub: string;\n /**\n * Opaque public-facing identifier · `did:oc:<32-hex>`. The sole\n * canonical user identifier post auth-refactor. Stable across\n * linking events. Per AUTH-REFACTOR-PLAN.md §2.1.\n */\n did_oc: string;\n jti: string;\n /**\n * Optional display name set by the user via the auth host's profile\n * surface. When present, consumer subdomains can render it in their\n * header chip without a network round-trip. Re-minted on signin and\n * whenever the user updates their profile.\n */\n name?: string | null;\n /** Optional Nostr npub set by the user. Same lifecycle as `name`. */\n npub?: string | null;\n /**\n * Slug of the federation this user is bound to (their \"home\"\n * federation). Multi-federation routing reads this; v1 has one live\n * federation so it's set on first signin and rarely changes. Absent\n * on tokens minted before this field shipped, and on accounts that\n * have never bound to a federation. Consumers fall back to the\n * directory default at /api/federations when absent.\n */\n home_federation?: string | null;\n /**\n * Discriminator recording where this user is on the custody-state\n * graph. Three legitimate states:\n *\n * - 'fedimint_threshold' — federation custody via the OC-introduced\n * federation (default for email-OTP signups; OC's threshold\n * guardians hold key shares).\n * - 'fedimint_client' — user moved their balance to a Fedimint\n * federation they picked themselves (still federation custody,\n * but on a federation OC didn't introduce).\n * - 'bip322' — full self-custody. The user signs with\n * their own Bitcoin key. OC has zero authority over their funds.\n *\n * Graduation is the product thesis. This field is the structural\n * primitive that records progress along the custody-state graph;\n * transitions are recorded as anchored \"rebind\" envelopes. Absent\n * on tokens minted before the field shipped — consumers should\n * treat undefined as 'fedimint_threshold' for email-OTP identities\n * and 'bip322' for BIP-322 identities (the default-by-construction\n * mapping).\n */\n signing_method?: \"fedimint_threshold\" | \"fedimint_client\" | \"bip322\" | null;\n /**\n * Historical did_ocs consolidated INTO this account via the\n * /api/auth/link/btc transfer-under-dual-proof flow. Consumers that\n * aggregate per-user data should union queries across\n * `[did_oc, ...merged_from]`. Absent / empty means \"no absorbed\n * accounts.\" Order is not significant.\n */\n merged_from?: string[];\n /**\n * Unix seconds when the user last successfully completed a WebAuthn\n * step-up assertion on the auth host. Consumer subdomains gate\n * sensitive actions on `(now - step_up_at) < their_window` —\n * typically 5 minutes. Absent on JWTs minted before WebAuthn\n * shipped, or on tokens minted by ordinary signin (only set by\n * /api/auth/webauthn/assertion/verify on success). Use\n * `verifyStepUpClaim()` to read it safely.\n */\n step_up_at?: number;\n /**\n * Unix seconds when the user last successfully completed an inline\n * sudo-mode re-authentication on the auth host (email-OTP for\n * email-primary identities, BIP-322 challenge for btc-primary\n * identities). Gates auth-graph-mutating operations — register an\n * additional WebAuthn credential, revoke one, link a new identity,\n * change recovery method — without forcing a full sign-out/sign-in.\n * Same shape and freshness window posture as `step_up_at`; the two\n * are independent claims (sudo proves \"you typed your password\n * again,\" step-up proves \"you tapped your hardware key\"). Read it\n * with `verifySudoClaim()`.\n */\n sudo_at?: number;\n /**\n * Optional best-effort owner flag · set by the auth host at signin\n * time when the new did_oc matches OWNER_OC_ADDRESSES. Surfaced\n * through to clients via `useOcSession().account.isOwner` so the\n * family-switcher and other low-stakes UX can render owner-only\n * affordances (e.g. an analytics.ochk.io entry).\n *\n * **NOT A SECURITY BOUNDARY.** Sensitive surfaces still re-check\n * the live `OWNER_OC_ADDRESSES` env against `session.did_oc` on\n * every request (analytics.ochk.io's requireOwner does this).\n * If an owner is removed from the env, their JWT may still carry\n * `is_owner: true` for up to 30 days — the UX hint goes stale,\n * but every gated action they attempt re-fails server-side.\n *\n * Treat absence as `false`. Set only on first signin or re-issue;\n * never mutated mid-session.\n */\n is_owner?: boolean;\n /**\n * The identity the user has chosen to show in their account badge\n * — the collapsed label every family site (and any integrator)\n * renders instead of the raw `did:oc`. Baked into the JWT so the\n * choice is consistent across every `.ochk.io` subdomain with no\n * round-trip, the same posture as `name` / `npub`.\n *\n * Re-resolved by the auth host on every mint: an explicit user\n * promotion wins; absent that, it defaults to the account's\n * sign-in identity (BIP-322 accounts → their Bitcoin address,\n * federation accounts → their email). Absent / null on tokens\n * minted before this field shipped — consumers fall back to the\n * `did_oc`. Read it safely with `resolveDisplayIdentity()`.\n */\n display_identity?: DisplayIdentity | null;\n /**\n * Present when the account is backed by a bond. Absent means no bond, not\n * a failed check. Read it with `readBondClaim()` / `bondMeets()`.\n */\n oc_bond?: BondClaim;\n}\n\n/** The token's bond claim when it is well-formed, else null. */\nexport function readBondClaim(payload: SessionPayload): BondClaim | null {\n const b = payload.oc_bond as Partial<BondClaim> | undefined;\n if (!b || typeof b !== \"object\" || b.v !== 1) return null;\n const nums = [b.sats_floor, b.days_floor, b.height, b.checked_at];\n if (!nums.every((n) => typeof n === \"number\" && Number.isFinite(n) && n >= 0)) return null;\n if (typeof b.nullifier !== \"string\" || !/^[0-9a-f]{64}$/.test(b.nullifier)) return null;\n return b as BondClaim;\n}\n\n/**\n * True when the account is backed by a coin whose floors reach `minSats`\n * and `minDays`, and the issuer checked it within\n * `maxAgeSecs` (default: no limit). Compare against the floor ladder: a\n * `minSats` of 50000 is met by a `sats_floor` of 100000, not 10000.\n */\nexport function bondMeets(\n payload: SessionPayload,\n need: { minSats?: number; minDays?: number; maxAgeSecs?: number },\n): boolean {\n const b = readBondClaim(payload);\n if (!b) return false;\n if (b.sats_floor < (need.minSats ?? 0)) return false;\n if (b.days_floor < (need.minDays ?? 0)) return false;\n if (need.maxAgeSecs !== undefined) {\n const age = Math.floor(Date.now() / 1000) - b.checked_at;\n if (age < 0 || age > need.maxAgeSecs) return false;\n }\n return true;\n}\n\n/**\n * Returns true when the JWT carries a fresh `step_up_at` claim —\n * i.e. the user successfully completed a WebAuthn assertion within\n * the supplied freshness window (in seconds). Returns false for\n * tokens that lack the claim entirely, that carry a stale value,\n * that carry a value somehow in the future, or where the freshness\n * window is non-positive.\n *\n * Consumers gating sensitive actions read this in two places:\n *\n * - Client side · before calling the action, to decide whether to\n * trigger `useStepUpAuth()`. Skip the prompt if already fresh.\n * - Server side · in the route handler, AFTER verifying the JWT,\n * to enforce the policy regardless of what the client did.\n *\n * The freshness window is consumer-chosen — typical default is 300s\n * (5 min); higher-value spends might use 60s.\n */\nexport function verifyStepUpClaim(\n payload: SessionPayload,\n opts: { max_age_secs: number },\n): boolean {\n if (typeof payload.step_up_at !== \"number\") return false;\n if (!Number.isFinite(payload.step_up_at)) return false;\n if (opts.max_age_secs <= 0) return false;\n const ageSec = Math.floor(Date.now() / 1000) - payload.step_up_at;\n return ageSec >= 0 && ageSec < opts.max_age_secs;\n}\n\n/**\n * Returns true when the JWT carries a fresh `sudo_at` claim — i.e. the\n * user successfully re-authenticated inline (email-OTP or BIP-322\n * challenge) within the supplied freshness window. Independent of\n * `step_up_at` · sudo is \"you proved your primary identity again,\"\n * step-up is \"you tapped your hardware key.\"\n *\n * Returns false for tokens that lack the claim, that carry a stale\n * value, that carry a future-dated value (clock skew or malicious\n * mint), or where the freshness window is non-positive.\n *\n * Use this to gate auth-graph-mutating operations on the auth host —\n * adding a second hardware key, linking a new identity, generating\n * recovery codes, changing recovery method. Default window is 5\n * minutes; tighten per-operation as needed.\n */\nexport function verifySudoClaim(\n payload: SessionPayload,\n opts: { max_age_secs: number },\n): boolean {\n if (typeof payload.sudo_at !== \"number\") return false;\n if (!Number.isFinite(payload.sudo_at)) return false;\n if (opts.max_age_secs <= 0) return false;\n const ageSec = Math.floor(Date.now() / 1000) - payload.sudo_at;\n return ageSec >= 0 && ageSec < opts.max_age_secs;\n}\n\n/**\n * Resolve the account-badge identity for a session — total, never\n * throws, always returns a renderable `{ kind, value }`.\n *\n * Returns the JWT's `display_identity` claim when it is present and\n * well-formed; otherwise falls back to the canonical `did:oc`. Use\n * this everywhere you render the badge label rather than reading the\n * raw claim, so tokens minted before the field shipped (and tokens\n * carrying a malformed claim) degrade gracefully to the did.\n *\n * This is the single resolver both `<OcAccountMenu>` and integrators\n * building their own chip should call.\n */\nexport function resolveDisplayIdentity(\n payload: SessionPayload,\n): DisplayIdentity {\n const claim = payload.display_identity;\n if (\n claim &&\n typeof claim === \"object\" &&\n typeof claim.value === \"string\" &&\n claim.value.length > 0 &&\n (DISPLAY_IDENTITY_KINDS as readonly string[]).includes(claim.kind)\n ) {\n return { kind: claim.kind, value: claim.value };\n }\n return { kind: \"did\", value: payload.did_oc };\n}\n\n/**\n * Which audience a verifier accepts.\n *\n * omitted → a FAMILY session only: the token must carry no `aud`.\n * a string → a token bound to that audience (or one of several).\n * \"*\" → either. For the routes built to receive tokens that\n * integrators forward.\n *\n * Omitted is the strict default: a token bound to a relying party is that\n * party's credential and never a family sign-in. Tokens minted before\n * audiences existed carry no `aud`, so every existing session keeps\n * verifying exactly as before.\n */\nexport type AudienceRule = string | readonly string[] | \"*\";\n\n/** Pure · does the payload's `aud` satisfy the rule? */\nexport function audienceSatisfied(aud: unknown, rule: AudienceRule | undefined): boolean {\n const have = aud === undefined ? [] : Array.isArray(aud) ? aud : [aud];\n if (rule === \"*\") return true;\n if (rule === undefined) return have.length === 0;\n const want = typeof rule === \"string\" ? [rule] : rule;\n return have.some((a) => typeof a === \"string\" && want.includes(a));\n}\n\nexport interface VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 public key. */\n publicJwk: string;\n /** Expected `iss` claim. Tokens with a different issuer are rejected. */\n issuer?: string;\n /** Accepted `aud` · see AudienceRule. Omit for family sessions. */\n audience?: AudienceRule;\n}\n\nexport interface SignConfig extends VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 private key. */\n privateJwk: string;\n /** Short key id placed in the JWT header (`kid`). Must match the JWK. */\n kid: string;\n}\n\n// ─── JWK helpers ────────────────────────────────────────────────────────\n\nfunction decodeJwk(value: string): Record<string, unknown> {\n const json =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(value, \"base64url\").toString(\"utf8\")\n : new TextDecoder().decode(base64UrlDecode(value));\n const parsed = JSON.parse(json) as Record<string, unknown>;\n if (!parsed || typeof parsed !== \"object\") {\n throw new Error(\n \"[@orangecheck/auth-core] JWK env did not decode to an object\",\n );\n }\n return parsed;\n}\n\nfunction base64UrlDecode(input: string): Uint8Array {\n const pad = input.length % 4 === 0 ? \"\" : \"=\".repeat(4 - (input.length % 4));\n const b64 = (input + pad).replace(/-/g, \"+\").replace(/_/g, \"/\");\n const bin = typeof atob !== \"undefined\" ? atob(b64) : \"\";\n const bytes = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);\n return bytes;\n}\n\nlet publicKeyPromise: Promise<AuthKey> | null = null;\nlet publicKeyCache: string | null = null;\nexport function loadPublicKey(publicJwk: string): Promise<AuthKey> {\n if (!publicKeyPromise || publicKeyCache !== publicJwk) {\n publicKeyCache = publicJwk;\n publicKeyPromise = importJWK(\n decodeJwk(publicJwk),\n JWT_ALG,\n ) as Promise<AuthKey>;\n }\n return publicKeyPromise;\n}\n\nlet privateKeyPromise: Promise<AuthKey> | null = null;\nlet privateKeyCache: string | null = null;\nexport function loadPrivateKey(privateJwk: string): Promise<AuthKey> {\n if (!privateKeyPromise || privateKeyCache !== privateJwk) {\n privateKeyCache = privateJwk;\n privateKeyPromise = importJWK(\n decodeJwk(privateJwk),\n JWT_ALG,\n ) as Promise<AuthKey>;\n }\n return privateKeyPromise;\n}\n\n/** Parse the `OC_AUTH_PUBLIC_JWK` env var back into a JWK object. */\nexport function parsePublicJwk(publicJwk: string): Record<string, unknown> {\n return decodeJwk(publicJwk);\n}\n\n// ─── Sign / verify ──────────────────────────────────────────────────────\n\n/**\n * Mint a fresh oc_session JWT. Only called on the auth host (the only\n * place that holds the private key).\n *\n * The caller owns recording an ended token's `jti` for the revocation\n * feed that {@link createRevocationCheck} reads.\n */\nexport async function signSession(\n claims: {\n sub: string;\n did_oc: string;\n jti: string;\n name?: string | null;\n npub?: string | null;\n home_federation?: string | null;\n signing_method?: \"fedimint_threshold\" | \"fedimint_client\" | \"bip322\" | null;\n merged_from?: string[];\n step_up_at?: number;\n sudo_at?: number;\n display_identity?: DisplayIdentity | null;\n /** Bind the token to one relying party (e.g. `https://example.com`).\n * Omit for a family session. A bound token verifies only where that\n * audience is expected — never as a family sign-in. */\n aud?: string;\n },\n cfg: SignConfig,\n ttlSeconds: number,\n): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n // did_oc is the sole user identifier. The legacy `addr` shim that\n // mirrored did_oc was dropped in v2.0.0 along with the type field.\n const { aud, ...rest } = claims;\n const jwt = new SignJWT(rest)\n .setProtectedHeader({ alg: JWT_ALG, typ: \"JWT\", kid: cfg.kid })\n .setIssuer(cfg.issuer ?? DEFAULT_ISSUER)\n .setIssuedAt(now)\n .setExpirationTime(now + ttlSeconds);\n if (aud) jwt.setAudience(aud);\n return jwt.sign(await loadPrivateKey(cfg.privateJwk));\n}\n\n/**\n * Crypto-only JWT verify. Returns the payload on success, `null` on\n * any failure (bad signature, expired, wrong issuer, malformed).\n *\n * Safe for consumer subdomains — they only need the public JWK. A signed-out\n * token still verifies here; pass the payload to\n * {@link familyRevocationCheck} to refuse it.\n */\nexport async function verifySessionToken(\n token: string,\n cfg: VerifyConfig,\n): Promise<SessionPayload | null> {\n try {\n const res = await jwtVerify(token, await loadPublicKey(cfg.publicJwk), {\n algorithms: [JWT_ALG],\n issuer: cfg.issuer ?? DEFAULT_ISSUER,\n });\n const p = res.payload as SessionPayload;\n if (!p.sub || !p.did_oc || !p.jti) return null;\n if (!audienceSatisfied(p.aud, cfg.audience)) return null;\n return p;\n } catch {\n return null;\n }\n}\n\n// ─── Cookie helpers ─────────────────────────────────────────────────────\n\nexport interface CookieOptions {\n domain?: string;\n maxAge?: number;\n expires?: Date;\n path?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: \"Lax\" | \"Strict\" | \"None\";\n}\n\n/**\n * Serialize a cookie header value for `Set-Cookie`. Defaults match the\n * ochk.io session shape: HttpOnly, SameSite=Lax, Path=/, Secure in prod.\n */\nexport function serializeSessionCookie(\n token: string,\n opts: CookieOptions = {},\n): string {\n const parts: string[] = [`${SESSION_COOKIE}=${token}`];\n parts.push(`Path=${opts.path ?? \"/\"}`);\n if (opts.httpOnly ?? true) parts.push(\"HttpOnly\");\n parts.push(`SameSite=${opts.sameSite ?? \"Lax\"}`);\n if (opts.secure ?? true) parts.push(\"Secure\");\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n if (opts.maxAge != null) parts.push(`Max-Age=${opts.maxAge}`);\n if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);\n return parts.join(\"; \");\n}\n\n/** Clear the session cookie. Must match the Domain used when it was set. */\nexport function clearSessionCookieHeader(\n opts: Pick<CookieOptions, \"domain\" | \"path\" | \"secure\"> = {},\n): string {\n return serializeSessionCookie(\"\", {\n ...opts,\n maxAge: 0,\n expires: new Date(0),\n });\n}\n\n/**\n * Read the oc_session token out of a raw `Cookie:` header string.\n * Returns `null` if the cookie is missing or empty.\n */\nexport function readSessionCookie(\n cookieHeader: string | null | undefined,\n): string | null {\n if (!cookieHeader) return null;\n const prefix = `${SESSION_COOKIE}=`;\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim();\n if (trimmed.startsWith(prefix)) {\n const val = trimmed.slice(prefix.length);\n return val.length > 0 ? val : null;\n }\n }\n return null;\n}\n\n/**\n * Read EVERY oc_session value out of a raw `Cookie:` header string.\n * Multiple same-name cookies are legitimate (e.g. a stale host-scoped\n * cookie shadowing the `Domain=.ochk.io` one) — verification should\n * try each rather than trust ordering.\n */\nexport function readAllSessionCookies(\n cookieHeader: string | null | undefined,\n): string[] {\n if (!cookieHeader) return [];\n const prefix = `${SESSION_COOKIE}=`;\n const out: string[] = [];\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim();\n if (trimmed.startsWith(prefix)) {\n const val = trimmed.slice(prefix.length);\n if (val.length > 0) out.push(val);\n }\n }\n return out;\n}\n\n// ─── Per-tab session resolution (multi-account · tab pinning) ───────────\n//\n// One browser holds ONE shared `oc_session` cookie (the browser-wide\n// default account) but may be signed into N roster accounts, each with\n// its own concurrently-valid session JWT. A tab \"pins\" itself to one\n// account by holding that account's JWT in sessionStorage and sending\n// it on every API call as the `x-oc-tab-session` header. The header\n// value is a full session JWT — a *credential*, never a bare selector —\n// so servers verify it exactly like the cookie token and stay\n// stateless.\n\n/** Header carrying a tab-pinned session JWT. Lowercase (Node folds headers). */\nexport const TAB_SESSION_HEADER = \"x-oc-tab-session\" as const;\n\nexport type ResolveSessionResult =\n | { ok: true; payload: SessionPayload; via: \"tab\" | \"cookie\" }\n | { ok: false; reason: \"tab_invalid\" | \"no_session\" };\n\n/** Header bag shapes accepted by {@link resolveSessionFromRequest}. */\nexport type IncomingRequestHeaders =\n Headers | Record<string, string | string[] | undefined>;\n\nfunction headerValue(\n headers: IncomingRequestHeaders,\n name: string,\n): string | null {\n if (isHeaders(headers)) return headers.get(name);\n const raw = headers[name] ?? headers[name.toLowerCase()];\n if (Array.isArray(raw)) return raw[0] ?? null;\n return typeof raw === \"string\" ? raw : null;\n}\n\n/**\n * Resolve the EFFECTIVE session for a request — the per-tab choke\n * point every consumer's `readJwtSession` should delegate to.\n *\n * Precedence:\n * 1. `x-oc-tab-session` header, when present. **Fail-closed**: a\n * present-but-invalid tab token resolves to\n * `{ ok: false, reason: 'tab_invalid' }` rather than silently\n * falling back to the cookie — falling back would execute the\n * request as a DIFFERENT account than the tab is displaying,\n * which is precisely the bug per-tab pinning exists to prevent.\n * Clients clear their pin on 401 and re-resolve.\n * 2. Every `oc_session` cookie in the jar, first one that verifies.\n *\n * Crypto-only (signature + exp + iss via {@link verifySessionToken}); a\n * signed-out token still resolves, so pass the payload to\n * {@link familyRevocationCheck}. Never throws.\n */\nexport async function resolveSessionFromRequest(\n headers: IncomingRequestHeaders,\n cfg: VerifyConfig,\n): Promise<ResolveSessionResult> {\n const tabToken = headerValue(headers, TAB_SESSION_HEADER);\n if (tabToken) {\n const payload = await verifySessionToken(tabToken, cfg);\n if (payload) return { ok: true, payload, via: \"tab\" };\n return { ok: false, reason: \"tab_invalid\" };\n }\n const cookieHeader = headerValue(headers, \"cookie\");\n for (const token of readAllSessionCookies(cookieHeader)) {\n const payload = await verifySessionToken(token, cfg);\n if (payload) return { ok: true, payload, via: \"cookie\" };\n }\n return { ok: false, reason: \"no_session\" };\n}\n\n// ─── JWKS-aware verification (zero env vars · zero JWK handling) ─────────\n//\n// verifyOcToken / getOcSession are the integrator-facing verification\n// primitives. They lazy-fetch the auth host's published JWKS at\n// `<issuer>/.well-known/jwks.json` and cache it in-process. Integrators\n// install @orangecheck/me-client (which re-exports these), call\n// `verifyOcToken(token)` or `getOcSession(headers)`, and never see a\n// JWK string, an env var, or a base64 dance.\n\nconst DEFAULT_JWKS_TTL_MS = 60 * 60 * 1000; // 1 hour\n\ninterface JwksCacheEntry {\n keys: Record<string, AuthKey>;\n fetchedAt: number;\n}\n\nconst jwksCache = new Map<string, JwksCacheEntry>();\nconst inflight = new Map<string, Promise<JwksCacheEntry>>();\n\n/**\n * When each issuer last had its cache force-refreshed by an unrecognised\n * `kid`, and how long that path must wait before doing it again.\n *\n * Without this, a token carrying any unknown kid makes the verifier drop its\n * cache and fetch the JWKS again — so a stream of tokens with random kids turns\n * every integrator's server into a refetch amplifier pointed at the auth host,\n * and puts a network round-trip in front of each rejection. `inflight` only\n * collapses CONCURRENT fetches; sequential requests each got their own.\n *\n * A minute is far below any real rotation window, so a genuinely new key is\n * still picked up promptly — the bust exists for rotation, not for every\n * malformed token.\n */\nconst lastForcedRefresh = new Map<string, number>();\nconst FORCED_REFRESH_MIN_INTERVAL_MS = 60_000;\n\ninterface JwksJson {\n keys: Array<Record<string, unknown> & { kid?: string }>;\n}\n\nasync function fetchJwks(\n issuer: string,\n ttlMs: number,\n): Promise<JwksCacheEntry> {\n const cached = jwksCache.get(issuer);\n if (cached && Date.now() - cached.fetchedAt < ttlMs) return cached;\n const existing = inflight.get(issuer);\n if (existing) return existing;\n const p = (async (): Promise<JwksCacheEntry> => {\n try {\n const url = `${issuer.replace(/\\/$/, \"\")}/.well-known/jwks.json`;\n const res = await fetch(url, { headers: { Accept: \"application/json\" } });\n if (!res.ok) {\n throw new Error(\n `[@orangecheck/auth-core] JWKS fetch ${url} returned ${res.status}`,\n );\n }\n const json = (await res.json()) as JwksJson;\n if (!json || !Array.isArray(json.keys)) {\n throw new Error(\n `[@orangecheck/auth-core] JWKS at ${url} did not return {keys:[…]}`,\n );\n }\n const keys: Record<string, AuthKey> = {};\n for (const k of json.keys) {\n if (typeof k.kid !== \"string\") continue;\n try {\n keys[k.kid] = (await importJWK(k, JWT_ALG)) as AuthKey;\n } catch {\n /* skip keys that won't import for our alg */\n }\n }\n const entry: JwksCacheEntry = { keys, fetchedAt: Date.now() };\n jwksCache.set(issuer, entry);\n return entry;\n } catch (err) {\n // Stale-on-error: a transient JWKS outage shouldn't lock out\n // every consumer. If we have a cache entry — even an expired\n // one — keep using it until the next successful fetch. Throw\n // only when we've never seen a usable JWKS.\n const stale = jwksCache.get(issuer);\n if (stale) return stale;\n throw err;\n }\n })().finally(() => {\n inflight.delete(issuer);\n });\n inflight.set(issuer, p);\n return p;\n}\n\nfunction assertSafeIssuer(issuer: string): void {\n let url: URL;\n try {\n url = new URL(issuer);\n } catch {\n throw new Error(\n `[@orangecheck/auth-core] issuer must be an absolute URL, got ${issuer}`,\n );\n }\n // Allow http only for loopback / local dev. Any production-shaped\n // hostname (real DNS) is required to be https — JWKS fetched over\n // plaintext HTTP is trivially MITM-able and would defeat the whole\n // verification stack.\n const isLoopback =\n url.hostname === \"localhost\" ||\n url.hostname === \"127.0.0.1\" ||\n url.hostname === \"::1\";\n if (url.protocol !== \"https:\" && !isLoopback) {\n throw new Error(\n `[@orangecheck/auth-core] issuer must be https (got ${url.protocol}//${url.hostname}); plaintext JWKS is not safe`,\n );\n }\n}\n\nexport interface VerifyOcOptions {\n /** Auth host issuer. Defaults to https://ochk.io. */\n issuer?: string;\n /** JWKS cache TTL in ms. Defaults to 1 hour. Stale-on-error: if the\n * cache exists, verification still works during a transient outage. */\n jwksCacheTtlMs?: number;\n /** Accepted `aud` · see AudienceRule. An integrator verifying tokens from\n * the sign-in popup sets this to its own origin, e.g.\n * `\"https://example.com\"`, so a token issued to another site is refused. */\n audience?: AudienceRule;\n}\n\n/**\n * Verify a JWT issued by an OC auth host. Lazy-fetches the JWKS from\n * `<issuer>/.well-known/jwks.json`, picks the key whose `kid` matches\n * the token's protected header, and verifies the signature.\n *\n * Returns the payload on success, `null` on any failure (bad signature,\n * expired, wrong issuer, kid not in JWKS, malformed). Never throws.\n *\n * Integrators don't need to handle JWKs, env vars, or rotation — the\n * cache picks up new keys automatically when they appear in the JWKS\n * response. Stale tokens signed under a retired key continue to verify\n * as long as the retired key is still published in the JWKS (standard\n * key-rotation overlap window).\n */\nexport async function verifyOcToken(\n token: string,\n options: VerifyOcOptions = {},\n): Promise<SessionPayload | null> {\n const issuer = options.issuer ?? DEFAULT_ISSUER;\n const ttl = options.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;\n try {\n assertSafeIssuer(issuer);\n // Fast path: try the cache. If the kid isn't in the cache (e.g.\n // brand-new key just rotated in), fall through to a fresh fetch.\n let entry = await fetchJwks(issuer, ttl);\n const headerB64 = token.split(\".\")[0];\n if (!headerB64) return null;\n const headerJson =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(headerB64, \"base64url\").toString(\"utf8\")\n : new TextDecoder().decode(base64UrlDecode(headerB64));\n const header = JSON.parse(headerJson) as { kid?: string; alg?: string };\n if (!header.kid) return null;\n let key = entry.keys[header.kid];\n if (!key) {\n // Force a fresh JWKS fetch in case the integrator's cache is older\n // than the active key set on the auth host — but at most once a\n // minute per issuer, so an unknown kid cannot be used to drive\n // unbounded refetches (see lastForcedRefresh).\n const now = Date.now();\n const last = lastForcedRefresh.get(issuer) ?? 0;\n if (now - last >= FORCED_REFRESH_MIN_INTERVAL_MS) {\n lastForcedRefresh.set(issuer, now);\n jwksCache.delete(issuer);\n entry = await fetchJwks(issuer, ttl);\n key = entry.keys[header.kid];\n }\n }\n if (!key) return null;\n const res = await jwtVerify(token, key, {\n algorithms: [JWT_ALG],\n issuer,\n });\n const p = res.payload as SessionPayload;\n if (!p.sub || !p.did_oc || !p.jti) return null;\n if (!audienceSatisfied(p.aud, options.audience)) return null;\n return p;\n } catch {\n return null;\n }\n}\n\nexport interface SessionRequestHeaders {\n cookie?: string | null;\n authorization?: string | null;\n}\n\n/**\n * Verify the OC session for a request. Accepts either a plain object\n * with `cookie` / `authorization` properties (Express / Next.js / etc.)\n * or a Web Headers object (Hono / edge / Fetch API). Reads the session\n * from the Cookie header first; falls back to a `Authorization: Bearer\n * <token>` header so cross-domain integrators (different eTLD+1, no\n * .ochk.io cookie) can verify the same way.\n *\n * Returns `null` for unauthenticated, missing, or invalid requests.\n * Never throws.\n */\nexport async function getOcSession(\n headers: SessionRequestHeaders | Headers,\n options: VerifyOcOptions = {},\n): Promise<SessionPayload | null> {\n const cookie = isHeaders(headers)\n ? headers.get(\"cookie\")\n : (headers.cookie ?? null);\n const authorization = isHeaders(headers)\n ? headers.get(\"authorization\")\n : (headers.authorization ?? null);\n let token = readSessionCookie(cookie);\n if (!token && typeof authorization === \"string\") {\n const m = /^Bearer\\s+(.+)$/i.exec(authorization);\n if (m) token = m[1]!;\n }\n if (!token) return null;\n return verifyOcToken(token, options);\n}\n\nfunction isHeaders(value: unknown): value is Headers {\n return (\n typeof Headers !== \"undefined\" &&\n value !== null &&\n typeof value === \"object\" &&\n value instanceof Headers\n );\n}\n\n/**\n * Clear the module-level JWKS caches. Test-only — the caches are process-wide\n * by design (that is the point of them), which makes any test that touches\n * verification order-dependent without a reset.\n */\nexport function _resetJwksCachesForTests(): void {\n jwksCache.clear();\n inflight.clear();\n lastForcedRefresh.clear();\n}\n\nexport * from \"./revocation\";\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/revocation.ts","../src/index.ts"],"names":["inflight","importJWK","SignJWT","jwtVerify"],"mappings":"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","file":"index.js","sourcesContent":["// ─── Family-wide session revocation ─────────────────────────────────────\n//\n// A session token verifies by signature alone, so a site that checks only the\n// signature honours a token for its whole life, even after it was signed out,\n// replaced, or its account deleted. The auth host publishes the sessions that\n// ended at `<issuer>/api/auth/revocations`, to sites holding a feed token. This\n// check fetches that list at most once per `ttlMs` per process and answers from\n// memory, so a request never waits on the host while the copy is fresh.\n//\n// Entries are keyed by a truncated hash, so the list names no account:\n// - a token, by `ocrev1:jti:<jti>`: that token is ended.\n// - an account, by `ocrev1:sub:<sub>`, with a cutoff: every token of that\n// account issued before it (`iat < cutoff`) is ended.\n\n/** The claims a revocation decision reads. A verified session payload has them all. */\nexport interface RevocableClaims {\n jti?: unknown;\n sub?: unknown;\n iat?: unknown;\n}\n\n/**\n * `live`: not ended, per a copy of the list fresh enough to vouch for it.\n * `revoked`: ended. `unverified`: not known to be ended, but the copy is too\n * old (or was never fetched) to say so for a request that changes state.\n */\nexport type RevocationStatus = \"live\" | \"revoked\" | \"unverified\";\n\nexport interface RevocationCheck {\n status(claims: RevocableClaims): Promise<RevocationStatus>;\n /**\n * Whether to refuse the session for a request with this method: always when\n * revoked, and for an unsafe method (or none given) when unverified. Reads\n * keep working through an outage of the auth host; writes stop.\n */\n refuses(claims: RevocableClaims, method?: string): Promise<boolean>;\n}\n\nexport interface RevocationCheckOptions {\n /** Default `${OC_AUTH_ISSUER ?? \"https://ochk.io\"}/api/auth/revocations`. */\n url?: string;\n /** This site's feed token. Default `OC_REVOCATION_FEED_TOKEN`. */\n token?: string;\n /**\n * Headers for each fetch, in place of the feed token: an integrator's\n * request signed with its project secret ({@link integratorRevocationCheck}).\n */\n headers?: () => Record<string, string> | Promise<Record<string, string>>;\n /** How long a copy is used before fetching again. Default 60 s. */\n ttlMs?: number;\n /** Past this age a copy cannot vouch for a write. Default 10 min. */\n maxStaleMs?: number;\n /** One fetch's time limit. Default 3 s. */\n timeoutMs?: number;\n /** After a failed fetch, how long to wait before the next. Default 10 s. */\n retryMs?: number;\n /**\n * With no token: true answers `live` (local development and tests, where no\n * feed exists); false answers `unverified`, so writes are refused. Default:\n * true unless NODE_ENV is production.\n */\n developing?: boolean;\n fetch?: typeof fetch;\n now?: () => number;\n}\n\ninterface Snapshot {\n tokens: Set<string>;\n accounts: Map<string, number>;\n fetchedAt: number;\n}\n\nconst SAFE_METHODS = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\nconst DEFAULT_TTL_MS = 60_000;\nconst DEFAULT_MAX_STALE_MS = 10 * 60_000;\nconst DEFAULT_TIMEOUT_MS = 3_000;\nconst DEFAULT_RETRY_MS = 10_000;\n\nfunction env(name: string): string | undefined {\n return typeof process !== \"undefined\" ? process.env?.[name] : undefined;\n}\n\n/**\n * The feed key for a token (`kind: \"jti\"`) or an account (`kind: \"sub\"`):\n * base64url of the first 16 bytes of sha256(`ocrev1:<kind>:<value>`). The\n * auth host derives the same; its tests share these vectors.\n */\nexport async function revocationKey(\n kind: \"jti\" | \"sub\",\n value: string,\n): Promise<string> {\n const digest = await globalThis.crypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(`ocrev1:${kind}:${value}`),\n );\n let bin = \"\";\n for (const b of new Uint8Array(digest).subarray(0, 16)) bin += String.fromCharCode(b);\n return btoa(bin).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nfunction parseFeed(body: unknown, fetchedAt: number): Snapshot {\n const b = body as { v?: unknown; tokens?: unknown; accounts?: unknown } | null;\n if (!b || b.v !== 1 || !Array.isArray(b.tokens) || typeof b.accounts !== \"object\" || !b.accounts) {\n throw new Error(\"revocation feed: unexpected body\");\n }\n const tokens = new Set<string>();\n for (const t of b.tokens) if (typeof t === \"string\") tokens.add(t);\n const accounts = new Map<string, number>();\n for (const [k, v] of Object.entries(b.accounts as Record<string, unknown>)) {\n if (typeof v === \"number\" && Number.isFinite(v)) accounts.set(k, v);\n }\n return { tokens, accounts, fetchedAt };\n}\n\n/**\n * A revocation check with its own cached copy of the feed. Create one per\n * process and share it (fluid compute keeps module state across requests);\n * {@link familyRevocationCheck} is that one, configured from the environment.\n */\nexport function createRevocationCheck(opts: RevocationCheckOptions = {}): RevocationCheck {\n const issuer = (env(\"OC_AUTH_ISSUER\") ?? \"https://ochk.io\").replace(/\\/$/, \"\");\n const url = opts.url ?? env(\"OC_REVOCATION_FEED_URL\") ?? `${issuer}/api/auth/revocations`;\n const token = opts.token ?? env(\"OC_REVOCATION_FEED_TOKEN\")?.trim();\n const signer = opts.headers;\n const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;\n const maxStaleMs = opts.maxStaleMs ?? DEFAULT_MAX_STALE_MS;\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const retryMs = opts.retryMs ?? DEFAULT_RETRY_MS;\n const developing = opts.developing ?? env(\"NODE_ENV\") !== \"production\";\n const doFetch = opts.fetch ?? ((...a: Parameters<typeof fetch>) => fetch(...a));\n const now = opts.now ?? Date.now;\n\n let snapshot: Snapshot | null = null;\n let inflight: Promise<void> | null = null;\n let failedAt = -Infinity;\n let warned = false;\n const warn = (msg: string) => {\n if (warned) return;\n warned = true;\n console.warn(`[@orangecheck/auth-core] ${msg}`);\n };\n\n async function load(): Promise<void> {\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const credential = signer ? await signer() : { authorization: `Bearer ${token}` };\n const res = await doFetch(url, {\n headers: { ...credential, accept: \"application/json\" },\n signal: ctl.signal,\n });\n if (!res.ok) throw new Error(`revocation feed answered ${res.status}`);\n snapshot = parseFeed(await res.json(), now());\n warned = false;\n } finally {\n clearTimeout(timer);\n }\n }\n\n async function current(): Promise<Snapshot | null> {\n const t = now();\n if (snapshot && t - snapshot.fetchedAt < ttlMs) return snapshot;\n if (t - failedAt < retryMs) return snapshot;\n inflight ??= load()\n .catch((err: unknown) => {\n failedAt = now();\n warn(`${err instanceof Error ? err.message : String(err)}; using the last copy`);\n })\n .finally(() => {\n inflight = null;\n });\n await inflight;\n return snapshot;\n }\n\n async function status(claims: RevocableClaims): Promise<RevocationStatus> {\n if (!token && !signer) {\n if (developing) return \"live\";\n warn(\"OC_REVOCATION_FEED_TOKEN is not set; sessions cannot be checked for revocation\");\n return \"unverified\";\n }\n const snap = await current();\n if (!snap) return \"unverified\";\n // A token known to be ended stays ended, however old the copy.\n if (typeof claims.jti === \"string\" && snap.tokens.has(await revocationKey(\"jti\", claims.jti))) {\n return \"revoked\";\n }\n if (typeof claims.sub === \"string\" && snap.accounts.size > 0) {\n const cutoff = snap.accounts.get(await revocationKey(\"sub\", claims.sub));\n if (cutoff !== undefined && !(typeof claims.iat === \"number\" && claims.iat >= cutoff)) {\n return \"revoked\";\n }\n }\n return now() - snap.fetchedAt > maxStaleMs ? \"unverified\" : \"live\";\n }\n\n return {\n status,\n async refuses(claims, method) {\n const s = await status(claims);\n if (s === \"revoked\") return true;\n if (s === \"live\") return false;\n return !SAFE_METHODS.has((method ?? \"\").toUpperCase());\n },\n };\n}\n\nexport interface IntegratorRevocationOptions\n extends Omit<RevocationCheckOptions, \"url\" | \"token\" | \"headers\" | \"developing\"> {\n /** Default `OC_PROJECT_KEY`. */\n projectKey?: string;\n /** Default `OC_SIGNING_SECRET`. */\n signingSecret?: string;\n /** Default `OC_ME_ORIGIN`, else `https://me.ochk.io`. */\n meOrigin?: string;\n}\n\nasync function hmacHex(secret: string, data: string): Promise<string> {\n const enc = new TextEncoder();\n const key = await globalThis.crypto.subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const mac = new Uint8Array(await globalThis.crypto.subtle.sign(\"HMAC\", key, enc.encode(data)));\n return Array.from(mac, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/**\n * For an integrator's server: the account cutoffs (sign out everywhere, a\n * passkey removed, an account deleted) from me.ochk.io's\n * `/api/integrator/revocations`, fetched with a request signed by the project\n * secret, so a session the person ended everywhere is refused here too. Null\n * when the project key or secret is not set: a site that has not configured\n * it keeps verifying by signature alone rather than refusing writes.\n */\nexport function integratorRevocationCheck(\n opts: IntegratorRevocationOptions = {},\n): RevocationCheck | null {\n const { projectKey: pk, signingSecret, meOrigin, ...rest } = opts;\n const projectKey = (pk ?? env(\"OC_PROJECT_KEY\"))?.trim();\n const secret = (signingSecret ?? env(\"OC_SIGNING_SECRET\"))?.trim();\n if (!projectKey || !secret) return null;\n const origin = (meOrigin ?? env(\"OC_ME_ORIGIN\") ?? \"https://me.ochk.io\").replace(/\\/$/, \"\");\n const query = `project_key=${encodeURIComponent(projectKey)}`;\n const now = rest.now ?? Date.now;\n return createRevocationCheck({\n ...rest,\n url: `${origin}/api/integrator/revocations?${query}`,\n headers: async () => {\n const t = Math.floor(now() / 1000);\n return { \"x-oc-signature\": `t=${t},v1=${await hmacHex(secret, `${t}.${query}`)}` };\n },\n developing: false,\n });\n}\n\nconst SHARED = Symbol.for(\"@orangecheck/auth-core/revocation-check/v1\");\n\n/**\n * The process-wide check, configured from the environment\n * (`OC_REVOCATION_FEED_TOKEN`, `OC_AUTH_ISSUER`). Every reader in a site\n * should use this one, so the site fetches the feed once per minute, not once\n * per reader.\n */\nexport function familyRevocationCheck(): RevocationCheck {\n const g = globalThis as { [SHARED]?: RevocationCheck };\n return (g[SHARED] ??= createRevocationCheck());\n}\n\n/** Tests only: drop the process-wide check so the next call reads the environment again. */\nexport function _resetFamilyRevocationCheckForTests(): void {\n delete (globalThis as { [SHARED]?: RevocationCheck })[SHARED];\n}\n","/**\n * @orangecheck/auth-core\n *\n * Cross-subdomain oc_session JWT verification + cookie helpers.\n *\n * The auth HOST (whoever owns sign-in) holds a private key and issues\n * tokens with `signSession()`. Every CONSUMER subdomain holds only the\n * public JWK and verifies tokens locally with `verifySessionToken()` —\n * no network round-trip, no shared secret.\n *\n * The ochk.io ecosystem picks Ed25519 (EdDSA) with JWKs distributed\n * via environment variable. The public key is also published at\n * `/.well-known/jwks.json` on the auth host for dynamic discovery.\n */\n\nimport {\n importJWK,\n jwtVerify,\n SignJWT,\n type CryptoKey,\n type JWTPayload,\n type KeyObject,\n} from \"jose\";\n\nexport const JWT_ALG = \"EdDSA\" as const;\nexport const SESSION_COOKIE = \"oc_session\" as const;\nexport const DEFAULT_ISSUER = \"https://ochk.io\" as const;\n\nexport type AuthKey = CryptoKey | KeyObject;\n\n/**\n * The kinds of identity a user can surface as their account-badge\n * label. `did` is the canonical `did:oc` identifier (always available\n * and the ultimate fallback); the other three are linked identities\n * the user may or may not have.\n */\nexport const DISPLAY_IDENTITY_KINDS = [\"did\", \"btc\", \"email\", \"nostr\"] as const;\nexport type DisplayIdentityKind = (typeof DISPLAY_IDENTITY_KINDS)[number];\n\n/**\n * The identity a user has chosen to display in their account badge —\n * carried as a JWT claim so every `.ochk.io` subdomain (and any\n * integrator reading the session) renders the same label with no\n * network round-trip, exactly like `name` / `npub`.\n *\n * `kind` is which identity; `value` is the full, renderable value (the\n * `did:oc`, the Bitcoin address, the email, or the npub). Only the\n * *promoted* identity's value is carried — never the whole identity\n * set — so an unrelated email never enters the token.\n */\nexport interface DisplayIdentity {\n kind: DisplayIdentityKind;\n value: string;\n}\n\n/**\n * A bond (oc-attest-protocol BOND.md): one unspent coin the account pledged.\n * The token carries floors, never the coin, so an app learns that a coin of\n * at least `sats_floor` sats, unspent at least `days_floor` days, backs this\n * account, and nothing about which coin it is.\n */\nexport interface BondClaim {\n v: 1;\n /** Largest of 1000, 10000, 100000, 1000000, 10000000 the coin meets; 0 for none. */\n sats_floor: number;\n /** Largest of 7, 30, 90, 365 days the coin has been unspent; 0 for none. */\n days_floor: number;\n /** Block height at which the issuer last found the coin unspent. */\n height: number;\n /** Unix seconds of that check. */\n checked_at: number;\n /**\n * Same for the same coin at your site, unrelated anywhere else, and keyed by the\n * issuer so it doesn't reveal the coin. *.ochk.io sites share the family session,\n * so they share one. Keep one account per nullifier; block it to block the coin.\n */\n nullifier: string;\n}\n\nexport interface SessionPayload extends JWTPayload {\n sub: string;\n /**\n * Opaque public-facing identifier · `did:oc:<32-hex>`. The sole\n * canonical user identifier post auth-refactor. Stable across\n * linking events. Per AUTH-REFACTOR-PLAN.md §2.1.\n */\n did_oc: string;\n jti: string;\n /**\n * Optional display name set by the user via the auth host's profile\n * surface. When present, consumer subdomains can render it in their\n * header chip without a network round-trip. Re-minted on signin and\n * whenever the user updates their profile.\n */\n name?: string | null;\n /** Optional Nostr npub set by the user. Same lifecycle as `name`. */\n npub?: string | null;\n /**\n * @deprecated No longer minted. Where a person's sats sit is me.ochk.io's\n * to say, per federation, from its own ledger (`/api/me/balance`).\n */\n home_federation?: string | null;\n /**\n * Whether the account signs with its own Bitcoin key. The auth host\n * derives it at every mint from the account's verified linked\n * identities; no one can set it.\n *\n * - 'bip322' — a BIP-322-verified Bitcoin address is linked.\n * - 'fedimint_threshold' — none is, so the account holds no key here.\n * - 'fedimint_client' — reserved; never minted.\n *\n * It says who holds the signing key, never who holds the sats.\n */\n signing_method?: \"fedimint_threshold\" | \"fedimint_client\" | \"bip322\" | null;\n /**\n * Historical did_ocs consolidated INTO this account via the\n * /api/auth/link/btc transfer-under-dual-proof flow. Consumers that\n * aggregate per-user data should union queries across\n * `[did_oc, ...merged_from]`. Absent / empty means \"no absorbed\n * accounts.\" Order is not significant.\n */\n merged_from?: string[];\n /**\n * Unix seconds when the user last successfully completed a WebAuthn\n * step-up assertion on the auth host. Consumer subdomains gate\n * sensitive actions on `(now - step_up_at) < their_window` —\n * typically 5 minutes. Absent on JWTs minted before WebAuthn\n * shipped, or on tokens minted by ordinary signin (only set by\n * /api/auth/webauthn/assertion/verify on success). Use\n * `verifyStepUpClaim()` to read it safely.\n */\n step_up_at?: number;\n /**\n * Unix seconds when the user last successfully completed an inline\n * sudo-mode re-authentication on the auth host (email-OTP for\n * email-primary identities, BIP-322 challenge for btc-primary\n * identities). Gates auth-graph-mutating operations — register an\n * additional WebAuthn credential, revoke one, link a new identity,\n * change recovery method — without forcing a full sign-out/sign-in.\n * Same shape and freshness window posture as `step_up_at`; the two\n * are independent claims (sudo proves \"you typed your password\n * again,\" step-up proves \"you tapped your hardware key\"). Read it\n * with `verifySudoClaim()`.\n */\n sudo_at?: number;\n /**\n * Optional best-effort owner flag · set by the auth host at signin\n * time when the new did_oc matches OWNER_OC_ADDRESSES. Surfaced\n * through to clients via `useOcSession().account.isOwner` so the\n * family-switcher and other low-stakes UX can render owner-only\n * affordances (e.g. an analytics.ochk.io entry).\n *\n * **NOT A SECURITY BOUNDARY.** Sensitive surfaces still re-check\n * the live `OWNER_OC_ADDRESSES` env against `session.did_oc` on\n * every request (analytics.ochk.io's requireOwner does this).\n * If an owner is removed from the env, their JWT may still carry\n * `is_owner: true` for up to 30 days — the UX hint goes stale,\n * but every gated action they attempt re-fails server-side.\n *\n * Treat absence as `false`. Set only on first signin or re-issue;\n * never mutated mid-session.\n */\n is_owner?: boolean;\n /**\n * The identity the user has chosen to show in their account badge\n * — the collapsed label every family site (and any integrator)\n * renders instead of the raw `did:oc`. Baked into the JWT so the\n * choice is consistent across every `.ochk.io` subdomain with no\n * round-trip, the same posture as `name` / `npub`.\n *\n * Re-resolved by the auth host on every mint: an explicit user\n * promotion wins; absent that, it defaults to the account's\n * sign-in identity (BIP-322 accounts → their Bitcoin address,\n * federation accounts → their email). Absent / null on tokens\n * minted before this field shipped — consumers fall back to the\n * `did_oc`. Read it safely with `resolveDisplayIdentity()`.\n */\n display_identity?: DisplayIdentity | null;\n /**\n * Present when the account is backed by a bond. Absent means no bond, not\n * a failed check. Read it with `readBondClaim()` / `bondMeets()`.\n */\n oc_bond?: BondClaim;\n}\n\n/** The token's bond claim when it is well-formed, else null. */\nexport function readBondClaim(payload: SessionPayload): BondClaim | null {\n const b = payload.oc_bond as Partial<BondClaim> | undefined;\n if (!b || typeof b !== \"object\" || b.v !== 1) return null;\n const nums = [b.sats_floor, b.days_floor, b.height, b.checked_at];\n if (!nums.every((n) => typeof n === \"number\" && Number.isFinite(n) && n >= 0)) return null;\n if (typeof b.nullifier !== \"string\" || !/^[0-9a-f]{64}$/.test(b.nullifier)) return null;\n return b as BondClaim;\n}\n\n/**\n * True when the account is backed by a coin whose floors reach `minSats`\n * and `minDays`, and the issuer checked it within\n * `maxAgeSecs` (default: no limit). Compare against the floor ladder: a\n * `minSats` of 50000 is met by a `sats_floor` of 100000, not 10000.\n */\nexport function bondMeets(\n payload: SessionPayload,\n need: { minSats?: number; minDays?: number; maxAgeSecs?: number },\n): boolean {\n const b = readBondClaim(payload);\n if (!b) return false;\n if (b.sats_floor < (need.minSats ?? 0)) return false;\n if (b.days_floor < (need.minDays ?? 0)) return false;\n if (need.maxAgeSecs !== undefined) {\n const age = Math.floor(Date.now() / 1000) - b.checked_at;\n if (age < 0 || age > need.maxAgeSecs) return false;\n }\n return true;\n}\n\n/**\n * Returns true when the JWT carries a fresh `step_up_at` claim —\n * i.e. the user successfully completed a WebAuthn assertion within\n * the supplied freshness window (in seconds). Returns false for\n * tokens that lack the claim entirely, that carry a stale value,\n * that carry a value somehow in the future, or where the freshness\n * window is non-positive.\n *\n * Consumers gating sensitive actions read this in two places:\n *\n * - Client side · before calling the action, to decide whether to\n * trigger `useStepUpAuth()`. Skip the prompt if already fresh.\n * - Server side · in the route handler, AFTER verifying the JWT,\n * to enforce the policy regardless of what the client did.\n *\n * The freshness window is consumer-chosen — typical default is 300s\n * (5 min); higher-value spends might use 60s.\n */\nexport function verifyStepUpClaim(\n payload: SessionPayload,\n opts: { max_age_secs: number },\n): boolean {\n if (typeof payload.step_up_at !== \"number\") return false;\n if (!Number.isFinite(payload.step_up_at)) return false;\n if (opts.max_age_secs <= 0) return false;\n const ageSec = Math.floor(Date.now() / 1000) - payload.step_up_at;\n return ageSec >= 0 && ageSec < opts.max_age_secs;\n}\n\n/**\n * Returns true when the JWT carries a fresh `sudo_at` claim — i.e. the\n * user successfully re-authenticated inline (email-OTP or BIP-322\n * challenge) within the supplied freshness window. Independent of\n * `step_up_at` · sudo is \"you proved your primary identity again,\"\n * step-up is \"you tapped your hardware key.\"\n *\n * Returns false for tokens that lack the claim, that carry a stale\n * value, that carry a future-dated value (clock skew or malicious\n * mint), or where the freshness window is non-positive.\n *\n * Use this to gate auth-graph-mutating operations on the auth host —\n * adding a second hardware key, linking a new identity, generating\n * recovery codes, changing recovery method. Default window is 5\n * minutes; tighten per-operation as needed.\n */\nexport function verifySudoClaim(\n payload: SessionPayload,\n opts: { max_age_secs: number },\n): boolean {\n if (typeof payload.sudo_at !== \"number\") return false;\n if (!Number.isFinite(payload.sudo_at)) return false;\n if (opts.max_age_secs <= 0) return false;\n const ageSec = Math.floor(Date.now() / 1000) - payload.sudo_at;\n return ageSec >= 0 && ageSec < opts.max_age_secs;\n}\n\n/**\n * Resolve the account-badge identity for a session — total, never\n * throws, always returns a renderable `{ kind, value }`.\n *\n * Returns the JWT's `display_identity` claim when it is present and\n * well-formed; otherwise falls back to the canonical `did:oc`. Use\n * this everywhere you render the badge label rather than reading the\n * raw claim, so tokens minted before the field shipped (and tokens\n * carrying a malformed claim) degrade gracefully to the did.\n *\n * This is the single resolver both `<OcAccountMenu>` and integrators\n * building their own chip should call.\n */\nexport function resolveDisplayIdentity(\n payload: SessionPayload,\n): DisplayIdentity {\n const claim = payload.display_identity;\n if (\n claim &&\n typeof claim === \"object\" &&\n typeof claim.value === \"string\" &&\n claim.value.length > 0 &&\n (DISPLAY_IDENTITY_KINDS as readonly string[]).includes(claim.kind)\n ) {\n return { kind: claim.kind, value: claim.value };\n }\n return { kind: \"did\", value: payload.did_oc };\n}\n\n/**\n * Which audience a verifier accepts.\n *\n * omitted → a FAMILY session only: the token must carry no `aud`.\n * a string → a token bound to that audience (or one of several).\n * \"*\" → either. For the routes built to receive tokens that\n * integrators forward.\n *\n * Omitted is the strict default: a token bound to a relying party is that\n * party's credential and never a family sign-in. Tokens minted before\n * audiences existed carry no `aud`, so every existing session keeps\n * verifying exactly as before.\n */\nexport type AudienceRule = string | readonly string[] | \"*\";\n\n/** Pure · does the payload's `aud` satisfy the rule? */\nexport function audienceSatisfied(aud: unknown, rule: AudienceRule | undefined): boolean {\n const have = aud === undefined ? [] : Array.isArray(aud) ? aud : [aud];\n if (rule === \"*\") return true;\n if (rule === undefined) return have.length === 0;\n const want = typeof rule === \"string\" ? [rule] : rule;\n return have.some((a) => typeof a === \"string\" && want.includes(a));\n}\n\nexport interface VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 public key. */\n publicJwk: string;\n /** Expected `iss` claim. Tokens with a different issuer are rejected. */\n issuer?: string;\n /** Accepted `aud` · see AudienceRule. Omit for family sessions. */\n audience?: AudienceRule;\n}\n\nexport interface SignConfig extends VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 private key. */\n privateJwk: string;\n /** Short key id placed in the JWT header (`kid`). Must match the JWK. */\n kid: string;\n}\n\n// ─── JWK helpers ────────────────────────────────────────────────────────\n\nfunction decodeJwk(value: string): Record<string, unknown> {\n const json =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(value, \"base64url\").toString(\"utf8\")\n : new TextDecoder().decode(base64UrlDecode(value));\n const parsed = JSON.parse(json) as Record<string, unknown>;\n if (!parsed || typeof parsed !== \"object\") {\n throw new Error(\n \"[@orangecheck/auth-core] JWK env did not decode to an object\",\n );\n }\n return parsed;\n}\n\nfunction base64UrlDecode(input: string): Uint8Array {\n const pad = input.length % 4 === 0 ? \"\" : \"=\".repeat(4 - (input.length % 4));\n const b64 = (input + pad).replace(/-/g, \"+\").replace(/_/g, \"/\");\n const bin = typeof atob !== \"undefined\" ? atob(b64) : \"\";\n const bytes = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);\n return bytes;\n}\n\nlet publicKeyPromise: Promise<AuthKey> | null = null;\nlet publicKeyCache: string | null = null;\nexport function loadPublicKey(publicJwk: string): Promise<AuthKey> {\n if (!publicKeyPromise || publicKeyCache !== publicJwk) {\n publicKeyCache = publicJwk;\n publicKeyPromise = importJWK(\n decodeJwk(publicJwk),\n JWT_ALG,\n ) as Promise<AuthKey>;\n }\n return publicKeyPromise;\n}\n\nlet privateKeyPromise: Promise<AuthKey> | null = null;\nlet privateKeyCache: string | null = null;\nexport function loadPrivateKey(privateJwk: string): Promise<AuthKey> {\n if (!privateKeyPromise || privateKeyCache !== privateJwk) {\n privateKeyCache = privateJwk;\n privateKeyPromise = importJWK(\n decodeJwk(privateJwk),\n JWT_ALG,\n ) as Promise<AuthKey>;\n }\n return privateKeyPromise;\n}\n\n/** Parse the `OC_AUTH_PUBLIC_JWK` env var back into a JWK object. */\nexport function parsePublicJwk(publicJwk: string): Record<string, unknown> {\n return decodeJwk(publicJwk);\n}\n\n// ─── Sign / verify ──────────────────────────────────────────────────────\n\n/**\n * Mint a fresh oc_session JWT. Only called on the auth host (the only\n * place that holds the private key).\n *\n * The caller owns recording an ended token's `jti` for the revocation\n * feed that {@link createRevocationCheck} reads.\n */\nexport async function signSession(\n claims: {\n sub: string;\n did_oc: string;\n jti: string;\n name?: string | null;\n npub?: string | null;\n home_federation?: string | null;\n signing_method?: \"fedimint_threshold\" | \"fedimint_client\" | \"bip322\" | null;\n merged_from?: string[];\n step_up_at?: number;\n sudo_at?: number;\n display_identity?: DisplayIdentity | null;\n /** Bind the token to one relying party (e.g. `https://example.com`).\n * Omit for a family session. A bound token verifies only where that\n * audience is expected — never as a family sign-in. */\n aud?: string;\n },\n cfg: SignConfig,\n ttlSeconds: number,\n): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n // did_oc is the sole user identifier. The legacy `addr` shim that\n // mirrored did_oc was dropped in v2.0.0 along with the type field.\n const { aud, ...rest } = claims;\n const jwt = new SignJWT(rest)\n .setProtectedHeader({ alg: JWT_ALG, typ: \"JWT\", kid: cfg.kid })\n .setIssuer(cfg.issuer ?? DEFAULT_ISSUER)\n .setIssuedAt(now)\n .setExpirationTime(now + ttlSeconds);\n if (aud) jwt.setAudience(aud);\n return jwt.sign(await loadPrivateKey(cfg.privateJwk));\n}\n\n/**\n * Crypto-only JWT verify. Returns the payload on success, `null` on\n * any failure (bad signature, expired, wrong issuer, malformed).\n *\n * Safe for consumer subdomains — they only need the public JWK. A signed-out\n * token still verifies here; pass the payload to\n * {@link familyRevocationCheck} to refuse it.\n */\nexport async function verifySessionToken(\n token: string,\n cfg: VerifyConfig,\n): Promise<SessionPayload | null> {\n try {\n const res = await jwtVerify(token, await loadPublicKey(cfg.publicJwk), {\n algorithms: [JWT_ALG],\n issuer: cfg.issuer ?? DEFAULT_ISSUER,\n });\n const p = res.payload as SessionPayload;\n if (!p.sub || !p.did_oc || !p.jti) return null;\n if (!audienceSatisfied(p.aud, cfg.audience)) return null;\n return p;\n } catch {\n return null;\n }\n}\n\n// ─── Cookie helpers ─────────────────────────────────────────────────────\n\nexport interface CookieOptions {\n domain?: string;\n maxAge?: number;\n expires?: Date;\n path?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: \"Lax\" | \"Strict\" | \"None\";\n}\n\n/**\n * Serialize a cookie header value for `Set-Cookie`. Defaults match the\n * ochk.io session shape: HttpOnly, SameSite=Lax, Path=/, Secure in prod.\n */\nexport function serializeSessionCookie(\n token: string,\n opts: CookieOptions = {},\n): string {\n const parts: string[] = [`${SESSION_COOKIE}=${token}`];\n parts.push(`Path=${opts.path ?? \"/\"}`);\n if (opts.httpOnly ?? true) parts.push(\"HttpOnly\");\n parts.push(`SameSite=${opts.sameSite ?? \"Lax\"}`);\n if (opts.secure ?? true) parts.push(\"Secure\");\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n if (opts.maxAge != null) parts.push(`Max-Age=${opts.maxAge}`);\n if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);\n return parts.join(\"; \");\n}\n\n/** Clear the session cookie. Must match the Domain used when it was set. */\nexport function clearSessionCookieHeader(\n opts: Pick<CookieOptions, \"domain\" | \"path\" | \"secure\"> = {},\n): string {\n return serializeSessionCookie(\"\", {\n ...opts,\n maxAge: 0,\n expires: new Date(0),\n });\n}\n\n/**\n * Read the oc_session token out of a raw `Cookie:` header string.\n * Returns `null` if the cookie is missing or empty.\n */\nexport function readSessionCookie(\n cookieHeader: string | null | undefined,\n): string | null {\n if (!cookieHeader) return null;\n const prefix = `${SESSION_COOKIE}=`;\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim();\n if (trimmed.startsWith(prefix)) {\n const val = trimmed.slice(prefix.length);\n return val.length > 0 ? val : null;\n }\n }\n return null;\n}\n\n/**\n * Read EVERY oc_session value out of a raw `Cookie:` header string.\n * Multiple same-name cookies are legitimate (e.g. a stale host-scoped\n * cookie shadowing the `Domain=.ochk.io` one) — verification should\n * try each rather than trust ordering.\n */\nexport function readAllSessionCookies(\n cookieHeader: string | null | undefined,\n): string[] {\n if (!cookieHeader) return [];\n const prefix = `${SESSION_COOKIE}=`;\n const out: string[] = [];\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim();\n if (trimmed.startsWith(prefix)) {\n const val = trimmed.slice(prefix.length);\n if (val.length > 0) out.push(val);\n }\n }\n return out;\n}\n\n// ─── Per-tab session resolution (multi-account · tab pinning) ───────────\n//\n// One browser holds ONE shared `oc_session` cookie (the browser-wide\n// default account) but may be signed into N roster accounts, each with\n// its own concurrently-valid session JWT. A tab \"pins\" itself to one\n// account by holding that account's JWT in sessionStorage and sending\n// it on every API call as the `x-oc-tab-session` header. The header\n// value is a full session JWT — a *credential*, never a bare selector —\n// so servers verify it exactly like the cookie token and stay\n// stateless.\n\n/** Header carrying a tab-pinned session JWT. Lowercase (Node folds headers). */\nexport const TAB_SESSION_HEADER = \"x-oc-tab-session\" as const;\n\nexport type ResolveSessionResult =\n | { ok: true; payload: SessionPayload; via: \"tab\" | \"cookie\" }\n | { ok: false; reason: \"tab_invalid\" | \"no_session\" };\n\n/** Header bag shapes accepted by {@link resolveSessionFromRequest}. */\nexport type IncomingRequestHeaders =\n Headers | Record<string, string | string[] | undefined>;\n\nfunction headerValue(\n headers: IncomingRequestHeaders,\n name: string,\n): string | null {\n if (isHeaders(headers)) return headers.get(name);\n const raw = headers[name] ?? headers[name.toLowerCase()];\n if (Array.isArray(raw)) return raw[0] ?? null;\n return typeof raw === \"string\" ? raw : null;\n}\n\n/**\n * Resolve the EFFECTIVE session for a request — the per-tab choke\n * point every consumer's `readJwtSession` should delegate to.\n *\n * Precedence:\n * 1. `x-oc-tab-session` header, when present. **Fail-closed**: a\n * present-but-invalid tab token resolves to\n * `{ ok: false, reason: 'tab_invalid' }` rather than silently\n * falling back to the cookie — falling back would execute the\n * request as a DIFFERENT account than the tab is displaying,\n * which is precisely the bug per-tab pinning exists to prevent.\n * Clients clear their pin on 401 and re-resolve.\n * 2. Every `oc_session` cookie in the jar, first one that verifies.\n *\n * Crypto-only (signature + exp + iss via {@link verifySessionToken}); a\n * signed-out token still resolves, so pass the payload to\n * {@link familyRevocationCheck}. Never throws.\n */\nexport async function resolveSessionFromRequest(\n headers: IncomingRequestHeaders,\n cfg: VerifyConfig,\n): Promise<ResolveSessionResult> {\n const tabToken = headerValue(headers, TAB_SESSION_HEADER);\n if (tabToken) {\n const payload = await verifySessionToken(tabToken, cfg);\n if (payload) return { ok: true, payload, via: \"tab\" };\n return { ok: false, reason: \"tab_invalid\" };\n }\n const cookieHeader = headerValue(headers, \"cookie\");\n for (const token of readAllSessionCookies(cookieHeader)) {\n const payload = await verifySessionToken(token, cfg);\n if (payload) return { ok: true, payload, via: \"cookie\" };\n }\n return { ok: false, reason: \"no_session\" };\n}\n\n// ─── JWKS-aware verification (zero env vars · zero JWK handling) ─────────\n//\n// verifyOcToken / getOcSession are the integrator-facing verification\n// primitives. They lazy-fetch the auth host's published JWKS at\n// `<issuer>/.well-known/jwks.json` and cache it in-process. Integrators\n// install @orangecheck/me-client (which re-exports these), call\n// `verifyOcToken(token)` or `getOcSession(headers)`, and never see a\n// JWK string, an env var, or a base64 dance.\n\nconst DEFAULT_JWKS_TTL_MS = 60 * 60 * 1000; // 1 hour\n/** How old a key list may be and still stand in for one that can't be fetched. */\nconst DEFAULT_JWKS_MAX_STALE_MS = 6 * 60 * 60 * 1000; // 6 hours\n\ninterface JwksCacheEntry {\n keys: Record<string, AuthKey>;\n /** kid → unix seconds, for keys published as `status: \"retired\"`. */\n retiredAt: Record<string, number>;\n fetchedAt: number;\n}\n\nconst jwksCache = new Map<string, JwksCacheEntry>();\nconst inflight = new Map<string, Promise<JwksCacheEntry>>();\n\n/**\n * When each issuer last had its cache force-refreshed by an unrecognised\n * `kid`, and how long that path must wait before doing it again.\n *\n * Without this, a token carrying any unknown kid makes the verifier drop its\n * cache and fetch the JWKS again — so a stream of tokens with random kids turns\n * every integrator's server into a refetch amplifier pointed at the auth host,\n * and puts a network round-trip in front of each rejection. `inflight` only\n * collapses CONCURRENT fetches; sequential requests each got their own.\n *\n * A minute is far below any real rotation window, so a genuinely new key is\n * still picked up promptly — the bust exists for rotation, not for every\n * malformed token.\n */\nconst lastForcedRefresh = new Map<string, number>();\nconst FORCED_REFRESH_MIN_INTERVAL_MS = 60_000;\n\ninterface JwksJson {\n keys: Array<Record<string, unknown> & { kid?: string }>;\n}\n\nasync function fetchJwks(\n issuer: string,\n ttlMs: number,\n maxStaleMs: number = DEFAULT_JWKS_MAX_STALE_MS,\n): Promise<JwksCacheEntry> {\n const cached = jwksCache.get(issuer);\n if (cached && Date.now() - cached.fetchedAt < ttlMs) return cached;\n const existing = inflight.get(issuer);\n if (existing) return existing;\n const p = (async (): Promise<JwksCacheEntry> => {\n try {\n const url = `${issuer.replace(/\\/$/, \"\")}/.well-known/jwks.json`;\n const res = await fetch(url, { headers: { Accept: \"application/json\" } });\n if (!res.ok) {\n throw new Error(\n `[@orangecheck/auth-core] JWKS fetch ${url} returned ${res.status}`,\n );\n }\n const json = (await res.json()) as JwksJson;\n if (!json || !Array.isArray(json.keys)) {\n throw new Error(\n `[@orangecheck/auth-core] JWKS at ${url} did not return {keys:[…]}`,\n );\n }\n const keys: Record<string, AuthKey> = {};\n const retiredAt: Record<string, number> = {};\n for (const k of json.keys) {\n if (typeof k.kid !== \"string\") continue;\n try {\n keys[k.kid] = (await importJWK(k, JWT_ALG)) as AuthKey;\n } catch {\n /* skip keys that won't import for our alg */\n continue;\n }\n if (k.status === \"retired\") {\n // A retired key with no readable retirement time signs nothing.\n retiredAt[k.kid] =\n typeof k.retired_at === \"number\" ? k.retired_at : 0;\n }\n }\n const entry: JwksCacheEntry = { keys, retiredAt, fetchedAt: Date.now() };\n jwksCache.set(issuer, entry);\n return entry;\n } catch (err) {\n // Stale-on-error: a transient JWKS outage shouldn't lock out\n // every consumer, so an expired entry keeps answering — but only\n // until it is `maxStaleMs` old. A key the auth host removed must\n // stop verifying even while its key list can't be fetched.\n const stale = jwksCache.get(issuer);\n if (stale && Date.now() - stale.fetchedAt < maxStaleMs) return stale;\n throw err;\n }\n })().finally(() => {\n inflight.delete(issuer);\n });\n inflight.set(issuer, p);\n return p;\n}\n\nfunction assertSafeIssuer(issuer: string): void {\n let url: URL;\n try {\n url = new URL(issuer);\n } catch {\n throw new Error(\n `[@orangecheck/auth-core] issuer must be an absolute URL, got ${issuer}`,\n );\n }\n // Allow http only for loopback / local dev. Any production-shaped\n // hostname (real DNS) is required to be https — JWKS fetched over\n // plaintext HTTP is trivially MITM-able and would defeat the whole\n // verification stack.\n const isLoopback =\n url.hostname === \"localhost\" ||\n url.hostname === \"127.0.0.1\" ||\n url.hostname === \"::1\";\n if (url.protocol !== \"https:\" && !isLoopback) {\n throw new Error(\n `[@orangecheck/auth-core] issuer must be https (got ${url.protocol}//${url.hostname}); plaintext JWKS is not safe`,\n );\n }\n}\n\nexport interface VerifyOcOptions {\n /** Auth host issuer. Defaults to https://ochk.io. */\n issuer?: string;\n /** JWKS cache TTL in ms. Defaults to 1 hour. Stale-on-error: if the\n * cache exists, verification still works during a transient outage. */\n jwksCacheTtlMs?: number;\n /** How old a cached JWKS may be and still stand in during an outage.\n * Defaults to 6 hours; past it, verification fails closed. */\n jwksMaxStaleMs?: number;\n /** Accepted `aud` · see AudienceRule. An integrator verifying tokens from\n * the sign-in popup sets this to its own origin, e.g.\n * `\"https://example.com\"`, so a token issued to another site is refused. */\n audience?: AudienceRule;\n}\n\n/**\n * Verify a JWT issued by an OC auth host. Lazy-fetches the JWKS from\n * `<issuer>/.well-known/jwks.json`, picks the key whose `kid` matches\n * the token's protected header, and verifies the signature.\n *\n * Returns the payload on success, `null` on any failure (bad signature,\n * expired, wrong issuer, kid not in JWKS, malformed). Never throws.\n *\n * Integrators don't need to handle JWKs, env vars, or rotation — the\n * cache picks up new keys automatically when they appear in the JWKS\n * response. A key published with `status: \"retired\"` verifies only tokens\n * issued before its `retired_at`; a key removed from the JWKS stops\n * verifying once the cache refreshes, and an unreachable JWKS is stood in\n * for by the last one fetched for at most `jwksMaxStaleMs`.\n */\nexport async function verifyOcToken(\n token: string,\n options: VerifyOcOptions = {},\n): Promise<SessionPayload | null> {\n const issuer = options.issuer ?? DEFAULT_ISSUER;\n const ttl = options.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;\n const maxStale = options.jwksMaxStaleMs ?? DEFAULT_JWKS_MAX_STALE_MS;\n try {\n assertSafeIssuer(issuer);\n // Fast path: try the cache. If the kid isn't in the cache (e.g.\n // brand-new key just rotated in), fall through to a fresh fetch.\n let entry = await fetchJwks(issuer, ttl, maxStale);\n const headerB64 = token.split(\".\")[0];\n if (!headerB64) return null;\n const headerJson =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(headerB64, \"base64url\").toString(\"utf8\")\n : new TextDecoder().decode(base64UrlDecode(headerB64));\n const header = JSON.parse(headerJson) as { kid?: string; alg?: string };\n if (!header.kid) return null;\n let key = entry.keys[header.kid];\n if (!key) {\n // Force a fresh JWKS fetch in case the integrator's cache is older\n // than the active key set on the auth host — but at most once a\n // minute per issuer, so an unknown kid cannot be used to drive\n // unbounded refetches (see lastForcedRefresh).\n const now = Date.now();\n const last = lastForcedRefresh.get(issuer) ?? 0;\n if (now - last >= FORCED_REFRESH_MIN_INTERVAL_MS) {\n lastForcedRefresh.set(issuer, now);\n // TTL 0 refetches while keeping the cached list as the stand-in.\n entry = await fetchJwks(issuer, 0, maxStale);\n key = entry.keys[header.kid];\n }\n }\n if (!key) return null;\n const res = await jwtVerify(token, key, {\n algorithms: [JWT_ALG],\n issuer,\n });\n const p = res.payload as SessionPayload;\n if (!p.sub || !p.did_oc || !p.jti) return null;\n // A retired key vouches only for what it signed before it retired.\n const retiredAt = entry.retiredAt[header.kid];\n if (retiredAt !== undefined && !(typeof p.iat === \"number\" && p.iat < retiredAt)) {\n return null;\n }\n if (!audienceSatisfied(p.aud, options.audience)) return null;\n return p;\n } catch {\n return null;\n }\n}\n\nexport interface SessionRequestHeaders {\n cookie?: string | null;\n authorization?: string | null;\n}\n\n/**\n * Verify the OC session for a request. Accepts either a plain object\n * with `cookie` / `authorization` properties (Express / Next.js / etc.)\n * or a Web Headers object (Hono / edge / Fetch API). Reads the session\n * from the Cookie header first; falls back to a `Authorization: Bearer\n * <token>` header so cross-domain integrators (different eTLD+1, no\n * .ochk.io cookie) can verify the same way.\n *\n * Returns `null` for unauthenticated, missing, or invalid requests.\n * Never throws.\n */\nexport async function getOcSession(\n headers: SessionRequestHeaders | Headers,\n options: VerifyOcOptions = {},\n): Promise<SessionPayload | null> {\n const cookie = isHeaders(headers)\n ? headers.get(\"cookie\")\n : (headers.cookie ?? null);\n const authorization = isHeaders(headers)\n ? headers.get(\"authorization\")\n : (headers.authorization ?? null);\n let token = readSessionCookie(cookie);\n if (!token && typeof authorization === \"string\") {\n const m = /^Bearer\\s+(.+)$/i.exec(authorization);\n if (m) token = m[1]!;\n }\n if (!token) return null;\n return verifyOcToken(token, options);\n}\n\nfunction isHeaders(value: unknown): value is Headers {\n return (\n typeof Headers !== \"undefined\" &&\n value !== null &&\n typeof value === \"object\" &&\n value instanceof Headers\n );\n}\n\n/**\n * Clear the module-level JWKS caches. Test-only — the caches are process-wide\n * by design (that is the point of them), which makes any test that touches\n * verification order-dependent without a reset.\n */\nexport function _resetJwksCachesForTests(): void {\n jwksCache.clear();\n inflight.clear();\n lastForcedRefresh.clear();\n}\n\nexport * from \"./revocation\";\n"]}
|
package/dist/index.mjs
CHANGED
|
@@ -37,6 +37,7 @@ function createRevocationCheck(opts = {}) {
|
|
|
37
37
|
const issuer = (env("OC_AUTH_ISSUER") ?? "https://ochk.io").replace(/\/$/, "");
|
|
38
38
|
const url = opts.url ?? env("OC_REVOCATION_FEED_URL") ?? `${issuer}/api/auth/revocations`;
|
|
39
39
|
const token = opts.token ?? env("OC_REVOCATION_FEED_TOKEN")?.trim();
|
|
40
|
+
const signer = opts.headers;
|
|
40
41
|
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
41
42
|
const maxStaleMs = opts.maxStaleMs ?? DEFAULT_MAX_STALE_MS;
|
|
42
43
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
@@ -57,8 +58,9 @@ function createRevocationCheck(opts = {}) {
|
|
|
57
58
|
const ctl = new AbortController();
|
|
58
59
|
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
59
60
|
try {
|
|
61
|
+
const credential = signer ? await signer() : { authorization: `Bearer ${token}` };
|
|
60
62
|
const res = await doFetch(url, {
|
|
61
|
-
headers: {
|
|
63
|
+
headers: { ...credential, accept: "application/json" },
|
|
62
64
|
signal: ctl.signal
|
|
63
65
|
});
|
|
64
66
|
if (!res.ok) throw new Error(`revocation feed answered ${res.status}`);
|
|
@@ -82,7 +84,7 @@ function createRevocationCheck(opts = {}) {
|
|
|
82
84
|
return snapshot;
|
|
83
85
|
}
|
|
84
86
|
async function status(claims) {
|
|
85
|
-
if (!token) {
|
|
87
|
+
if (!token && !signer) {
|
|
86
88
|
if (developing) return "live";
|
|
87
89
|
warn("OC_REVOCATION_FEED_TOKEN is not set; sessions cannot be checked for revocation");
|
|
88
90
|
return "unverified";
|
|
@@ -110,6 +112,36 @@ function createRevocationCheck(opts = {}) {
|
|
|
110
112
|
}
|
|
111
113
|
};
|
|
112
114
|
}
|
|
115
|
+
async function hmacHex(secret, data) {
|
|
116
|
+
const enc = new TextEncoder();
|
|
117
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
118
|
+
"raw",
|
|
119
|
+
enc.encode(secret),
|
|
120
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
121
|
+
false,
|
|
122
|
+
["sign"]
|
|
123
|
+
);
|
|
124
|
+
const mac = new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", key, enc.encode(data)));
|
|
125
|
+
return Array.from(mac, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
126
|
+
}
|
|
127
|
+
function integratorRevocationCheck(opts = {}) {
|
|
128
|
+
const { projectKey: pk, signingSecret, meOrigin, ...rest } = opts;
|
|
129
|
+
const projectKey = (pk ?? env("OC_PROJECT_KEY"))?.trim();
|
|
130
|
+
const secret = (signingSecret ?? env("OC_SIGNING_SECRET"))?.trim();
|
|
131
|
+
if (!projectKey || !secret) return null;
|
|
132
|
+
const origin = (meOrigin ?? env("OC_ME_ORIGIN") ?? "https://me.ochk.io").replace(/\/$/, "");
|
|
133
|
+
const query = `project_key=${encodeURIComponent(projectKey)}`;
|
|
134
|
+
const now = rest.now ?? Date.now;
|
|
135
|
+
return createRevocationCheck({
|
|
136
|
+
...rest,
|
|
137
|
+
url: `${origin}/api/integrator/revocations?${query}`,
|
|
138
|
+
headers: async () => {
|
|
139
|
+
const t = Math.floor(now() / 1e3);
|
|
140
|
+
return { "x-oc-signature": `t=${t},v1=${await hmacHex(secret, `${t}.${query}`)}` };
|
|
141
|
+
},
|
|
142
|
+
developing: false
|
|
143
|
+
});
|
|
144
|
+
}
|
|
113
145
|
var SHARED = /* @__PURE__ */ Symbol.for("@orangecheck/auth-core/revocation-check/v1");
|
|
114
146
|
function familyRevocationCheck() {
|
|
115
147
|
const g = globalThis;
|
|
@@ -302,11 +334,12 @@ async function resolveSessionFromRequest(headers, cfg) {
|
|
|
302
334
|
return { ok: false, reason: "no_session" };
|
|
303
335
|
}
|
|
304
336
|
var DEFAULT_JWKS_TTL_MS = 60 * 60 * 1e3;
|
|
337
|
+
var DEFAULT_JWKS_MAX_STALE_MS = 6 * 60 * 60 * 1e3;
|
|
305
338
|
var jwksCache = /* @__PURE__ */ new Map();
|
|
306
339
|
var inflight = /* @__PURE__ */ new Map();
|
|
307
340
|
var lastForcedRefresh = /* @__PURE__ */ new Map();
|
|
308
341
|
var FORCED_REFRESH_MIN_INTERVAL_MS = 6e4;
|
|
309
|
-
async function fetchJwks(issuer, ttlMs) {
|
|
342
|
+
async function fetchJwks(issuer, ttlMs, maxStaleMs = DEFAULT_JWKS_MAX_STALE_MS) {
|
|
310
343
|
const cached = jwksCache.get(issuer);
|
|
311
344
|
if (cached && Date.now() - cached.fetchedAt < ttlMs) return cached;
|
|
312
345
|
const existing = inflight.get(issuer);
|
|
@@ -327,19 +360,24 @@ async function fetchJwks(issuer, ttlMs) {
|
|
|
327
360
|
);
|
|
328
361
|
}
|
|
329
362
|
const keys = {};
|
|
363
|
+
const retiredAt = {};
|
|
330
364
|
for (const k of json.keys) {
|
|
331
365
|
if (typeof k.kid !== "string") continue;
|
|
332
366
|
try {
|
|
333
367
|
keys[k.kid] = await importJWK(k, JWT_ALG);
|
|
334
368
|
} catch {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (k.status === "retired") {
|
|
372
|
+
retiredAt[k.kid] = typeof k.retired_at === "number" ? k.retired_at : 0;
|
|
335
373
|
}
|
|
336
374
|
}
|
|
337
|
-
const entry = { keys, fetchedAt: Date.now() };
|
|
375
|
+
const entry = { keys, retiredAt, fetchedAt: Date.now() };
|
|
338
376
|
jwksCache.set(issuer, entry);
|
|
339
377
|
return entry;
|
|
340
378
|
} catch (err) {
|
|
341
379
|
const stale = jwksCache.get(issuer);
|
|
342
|
-
if (stale) return stale;
|
|
380
|
+
if (stale && Date.now() - stale.fetchedAt < maxStaleMs) return stale;
|
|
343
381
|
throw err;
|
|
344
382
|
}
|
|
345
383
|
})().finally(() => {
|
|
@@ -367,9 +405,10 @@ function assertSafeIssuer(issuer) {
|
|
|
367
405
|
async function verifyOcToken(token, options = {}) {
|
|
368
406
|
const issuer = options.issuer ?? DEFAULT_ISSUER;
|
|
369
407
|
const ttl = options.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;
|
|
408
|
+
const maxStale = options.jwksMaxStaleMs ?? DEFAULT_JWKS_MAX_STALE_MS;
|
|
370
409
|
try {
|
|
371
410
|
assertSafeIssuer(issuer);
|
|
372
|
-
let entry = await fetchJwks(issuer, ttl);
|
|
411
|
+
let entry = await fetchJwks(issuer, ttl, maxStale);
|
|
373
412
|
const headerB64 = token.split(".")[0];
|
|
374
413
|
if (!headerB64) return null;
|
|
375
414
|
const headerJson = typeof Buffer !== "undefined" ? Buffer.from(headerB64, "base64url").toString("utf8") : new TextDecoder().decode(base64UrlDecode(headerB64));
|
|
@@ -381,8 +420,7 @@ async function verifyOcToken(token, options = {}) {
|
|
|
381
420
|
const last = lastForcedRefresh.get(issuer) ?? 0;
|
|
382
421
|
if (now - last >= FORCED_REFRESH_MIN_INTERVAL_MS) {
|
|
383
422
|
lastForcedRefresh.set(issuer, now);
|
|
384
|
-
|
|
385
|
-
entry = await fetchJwks(issuer, ttl);
|
|
423
|
+
entry = await fetchJwks(issuer, 0, maxStale);
|
|
386
424
|
key = entry.keys[header.kid];
|
|
387
425
|
}
|
|
388
426
|
}
|
|
@@ -393,6 +431,10 @@ async function verifyOcToken(token, options = {}) {
|
|
|
393
431
|
});
|
|
394
432
|
const p = res.payload;
|
|
395
433
|
if (!p.sub || !p.did_oc || !p.jti) return null;
|
|
434
|
+
const retiredAt = entry.retiredAt[header.kid];
|
|
435
|
+
if (retiredAt !== void 0 && !(typeof p.iat === "number" && p.iat < retiredAt)) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
396
438
|
if (!audienceSatisfied(p.aud, options.audience)) return null;
|
|
397
439
|
return p;
|
|
398
440
|
} catch {
|
|
@@ -419,6 +461,6 @@ function _resetJwksCachesForTests() {
|
|
|
419
461
|
lastForcedRefresh.clear();
|
|
420
462
|
}
|
|
421
463
|
|
|
422
|
-
export { DEFAULT_ISSUER, DISPLAY_IDENTITY_KINDS, JWT_ALG, SESSION_COOKIE, TAB_SESSION_HEADER, _resetFamilyRevocationCheckForTests, _resetJwksCachesForTests, audienceSatisfied, bondMeets, clearSessionCookieHeader, createRevocationCheck, familyRevocationCheck, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readBondClaim, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, revocationKey, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
|
464
|
+
export { DEFAULT_ISSUER, DISPLAY_IDENTITY_KINDS, JWT_ALG, SESSION_COOKIE, TAB_SESSION_HEADER, _resetFamilyRevocationCheckForTests, _resetJwksCachesForTests, audienceSatisfied, bondMeets, clearSessionCookieHeader, createRevocationCheck, familyRevocationCheck, getOcSession, integratorRevocationCheck, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readBondClaim, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, revocationKey, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
|
423
465
|
//# sourceMappingURL=index.mjs.map
|
|
424
466
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/revocation.ts","../src/index.ts"],"names":["inflight"],"mappings":"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","file":"index.mjs","sourcesContent":["// ─── Family-wide session revocation ─────────────────────────────────────\n//\n// A session token verifies by signature alone, so a site that checks only the\n// signature honours a token for its whole life, even after it was signed out,\n// replaced, or its account deleted. The auth host publishes the sessions that\n// ended at `<issuer>/api/auth/revocations`, to sites holding a feed token. This\n// check fetches that list at most once per `ttlMs` per process and answers from\n// memory, so a request never waits on the host while the copy is fresh.\n//\n// Entries are keyed by a truncated hash, so the list names no account:\n// - a token, by `ocrev1:jti:<jti>`: that token is ended.\n// - an account, by `ocrev1:sub:<sub>`, with a cutoff: every token of that\n// account issued before it (`iat < cutoff`) is ended.\n\n/** The claims a revocation decision reads. A verified session payload has them all. */\nexport interface RevocableClaims {\n jti?: unknown;\n sub?: unknown;\n iat?: unknown;\n}\n\n/**\n * `live`: not ended, per a copy of the list fresh enough to vouch for it.\n * `revoked`: ended. `unverified`: not known to be ended, but the copy is too\n * old (or was never fetched) to say so for a request that changes state.\n */\nexport type RevocationStatus = \"live\" | \"revoked\" | \"unverified\";\n\nexport interface RevocationCheck {\n status(claims: RevocableClaims): Promise<RevocationStatus>;\n /**\n * Whether to refuse the session for a request with this method: always when\n * revoked, and for an unsafe method (or none given) when unverified. Reads\n * keep working through an outage of the auth host; writes stop.\n */\n refuses(claims: RevocableClaims, method?: string): Promise<boolean>;\n}\n\nexport interface RevocationCheckOptions {\n /** Default `${OC_AUTH_ISSUER ?? \"https://ochk.io\"}/api/auth/revocations`. */\n url?: string;\n /** This site's feed token. Default `OC_REVOCATION_FEED_TOKEN`. */\n token?: string;\n /** How long a copy is used before fetching again. Default 60 s. */\n ttlMs?: number;\n /** Past this age a copy cannot vouch for a write. Default 10 min. */\n maxStaleMs?: number;\n /** One fetch's time limit. Default 3 s. */\n timeoutMs?: number;\n /** After a failed fetch, how long to wait before the next. Default 10 s. */\n retryMs?: number;\n /**\n * With no token: true answers `live` (local development and tests, where no\n * feed exists); false answers `unverified`, so writes are refused. Default:\n * true unless NODE_ENV is production.\n */\n developing?: boolean;\n fetch?: typeof fetch;\n now?: () => number;\n}\n\ninterface Snapshot {\n tokens: Set<string>;\n accounts: Map<string, number>;\n fetchedAt: number;\n}\n\nconst SAFE_METHODS = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\nconst DEFAULT_TTL_MS = 60_000;\nconst DEFAULT_MAX_STALE_MS = 10 * 60_000;\nconst DEFAULT_TIMEOUT_MS = 3_000;\nconst DEFAULT_RETRY_MS = 10_000;\n\nfunction env(name: string): string | undefined {\n return typeof process !== \"undefined\" ? process.env?.[name] : undefined;\n}\n\n/**\n * The feed key for a token (`kind: \"jti\"`) or an account (`kind: \"sub\"`):\n * base64url of the first 16 bytes of sha256(`ocrev1:<kind>:<value>`). The\n * auth host derives the same; its tests share these vectors.\n */\nexport async function revocationKey(\n kind: \"jti\" | \"sub\",\n value: string,\n): Promise<string> {\n const digest = await globalThis.crypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(`ocrev1:${kind}:${value}`),\n );\n let bin = \"\";\n for (const b of new Uint8Array(digest).subarray(0, 16)) bin += String.fromCharCode(b);\n return btoa(bin).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nfunction parseFeed(body: unknown, fetchedAt: number): Snapshot {\n const b = body as { v?: unknown; tokens?: unknown; accounts?: unknown } | null;\n if (!b || b.v !== 1 || !Array.isArray(b.tokens) || typeof b.accounts !== \"object\" || !b.accounts) {\n throw new Error(\"revocation feed: unexpected body\");\n }\n const tokens = new Set<string>();\n for (const t of b.tokens) if (typeof t === \"string\") tokens.add(t);\n const accounts = new Map<string, number>();\n for (const [k, v] of Object.entries(b.accounts as Record<string, unknown>)) {\n if (typeof v === \"number\" && Number.isFinite(v)) accounts.set(k, v);\n }\n return { tokens, accounts, fetchedAt };\n}\n\n/**\n * A revocation check with its own cached copy of the feed. Create one per\n * process and share it (fluid compute keeps module state across requests);\n * {@link familyRevocationCheck} is that one, configured from the environment.\n */\nexport function createRevocationCheck(opts: RevocationCheckOptions = {}): RevocationCheck {\n const issuer = (env(\"OC_AUTH_ISSUER\") ?? \"https://ochk.io\").replace(/\\/$/, \"\");\n const url = opts.url ?? env(\"OC_REVOCATION_FEED_URL\") ?? `${issuer}/api/auth/revocations`;\n const token = opts.token ?? env(\"OC_REVOCATION_FEED_TOKEN\")?.trim();\n const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;\n const maxStaleMs = opts.maxStaleMs ?? DEFAULT_MAX_STALE_MS;\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const retryMs = opts.retryMs ?? DEFAULT_RETRY_MS;\n const developing = opts.developing ?? env(\"NODE_ENV\") !== \"production\";\n const doFetch = opts.fetch ?? ((...a: Parameters<typeof fetch>) => fetch(...a));\n const now = opts.now ?? Date.now;\n\n let snapshot: Snapshot | null = null;\n let inflight: Promise<void> | null = null;\n let failedAt = -Infinity;\n let warned = false;\n const warn = (msg: string) => {\n if (warned) return;\n warned = true;\n console.warn(`[@orangecheck/auth-core] ${msg}`);\n };\n\n async function load(): Promise<void> {\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const res = await doFetch(url, {\n headers: { authorization: `Bearer ${token}`, accept: \"application/json\" },\n signal: ctl.signal,\n });\n if (!res.ok) throw new Error(`revocation feed answered ${res.status}`);\n snapshot = parseFeed(await res.json(), now());\n warned = false;\n } finally {\n clearTimeout(timer);\n }\n }\n\n async function current(): Promise<Snapshot | null> {\n const t = now();\n if (snapshot && t - snapshot.fetchedAt < ttlMs) return snapshot;\n if (t - failedAt < retryMs) return snapshot;\n inflight ??= load()\n .catch((err: unknown) => {\n failedAt = now();\n warn(`${err instanceof Error ? err.message : String(err)}; using the last copy`);\n })\n .finally(() => {\n inflight = null;\n });\n await inflight;\n return snapshot;\n }\n\n async function status(claims: RevocableClaims): Promise<RevocationStatus> {\n if (!token) {\n if (developing) return \"live\";\n warn(\"OC_REVOCATION_FEED_TOKEN is not set; sessions cannot be checked for revocation\");\n return \"unverified\";\n }\n const snap = await current();\n if (!snap) return \"unverified\";\n // A token known to be ended stays ended, however old the copy.\n if (typeof claims.jti === \"string\" && snap.tokens.has(await revocationKey(\"jti\", claims.jti))) {\n return \"revoked\";\n }\n if (typeof claims.sub === \"string\" && snap.accounts.size > 0) {\n const cutoff = snap.accounts.get(await revocationKey(\"sub\", claims.sub));\n if (cutoff !== undefined && !(typeof claims.iat === \"number\" && claims.iat >= cutoff)) {\n return \"revoked\";\n }\n }\n return now() - snap.fetchedAt > maxStaleMs ? \"unverified\" : \"live\";\n }\n\n return {\n status,\n async refuses(claims, method) {\n const s = await status(claims);\n if (s === \"revoked\") return true;\n if (s === \"live\") return false;\n return !SAFE_METHODS.has((method ?? \"\").toUpperCase());\n },\n };\n}\n\nconst SHARED = Symbol.for(\"@orangecheck/auth-core/revocation-check/v1\");\n\n/**\n * The process-wide check, configured from the environment\n * (`OC_REVOCATION_FEED_TOKEN`, `OC_AUTH_ISSUER`). Every reader in a site\n * should use this one, so the site fetches the feed once per minute, not once\n * per reader.\n */\nexport function familyRevocationCheck(): RevocationCheck {\n const g = globalThis as { [SHARED]?: RevocationCheck };\n return (g[SHARED] ??= createRevocationCheck());\n}\n\n/** Tests only: drop the process-wide check so the next call reads the environment again. */\nexport function _resetFamilyRevocationCheckForTests(): void {\n delete (globalThis as { [SHARED]?: RevocationCheck })[SHARED];\n}\n","/**\n * @orangecheck/auth-core\n *\n * Cross-subdomain oc_session JWT verification + cookie helpers.\n *\n * The auth HOST (whoever owns sign-in) holds a private key and issues\n * tokens with `signSession()`. Every CONSUMER subdomain holds only the\n * public JWK and verifies tokens locally with `verifySessionToken()` —\n * no network round-trip, no shared secret.\n *\n * The ochk.io ecosystem picks Ed25519 (EdDSA) with JWKs distributed\n * via environment variable. The public key is also published at\n * `/.well-known/jwks.json` on the auth host for dynamic discovery.\n */\n\nimport {\n importJWK,\n jwtVerify,\n SignJWT,\n type CryptoKey,\n type JWTPayload,\n type KeyObject,\n} from \"jose\";\n\nexport const JWT_ALG = \"EdDSA\" as const;\nexport const SESSION_COOKIE = \"oc_session\" as const;\nexport const DEFAULT_ISSUER = \"https://ochk.io\" as const;\n\nexport type AuthKey = CryptoKey | KeyObject;\n\n/**\n * The kinds of identity a user can surface as their account-badge\n * label. `did` is the canonical `did:oc` identifier (always available\n * and the ultimate fallback); the other three are linked identities\n * the user may or may not have.\n */\nexport const DISPLAY_IDENTITY_KINDS = [\"did\", \"btc\", \"email\", \"nostr\"] as const;\nexport type DisplayIdentityKind = (typeof DISPLAY_IDENTITY_KINDS)[number];\n\n/**\n * The identity a user has chosen to display in their account badge —\n * carried as a JWT claim so every `.ochk.io` subdomain (and any\n * integrator reading the session) renders the same label with no\n * network round-trip, exactly like `name` / `npub`.\n *\n * `kind` is which identity; `value` is the full, renderable value (the\n * `did:oc`, the Bitcoin address, the email, or the npub). Only the\n * *promoted* identity's value is carried — never the whole identity\n * set — so an unrelated email never enters the token.\n */\nexport interface DisplayIdentity {\n kind: DisplayIdentityKind;\n value: string;\n}\n\n/**\n * A bond (oc-attest-protocol BOND.md): one unspent coin the account pledged.\n * The token carries floors, never the coin, so an app learns that a coin of\n * at least `sats_floor` sats, unspent at least `days_floor` days, backs this\n * account, and nothing about which coin it is.\n */\nexport interface BondClaim {\n v: 1;\n /** Largest of 1000, 10000, 100000, 1000000, 10000000 the coin meets; 0 for none. */\n sats_floor: number;\n /** Largest of 7, 30, 90, 365 days the coin has been unspent; 0 for none. */\n days_floor: number;\n /** Block height at which the issuer last found the coin unspent. */\n height: number;\n /** Unix seconds of that check. */\n checked_at: number;\n /**\n * Same for the same coin at your site, unrelated anywhere else, and keyed by the\n * issuer so it doesn't reveal the coin. *.ochk.io sites share the family session,\n * so they share one. Keep one account per nullifier; block it to block the coin.\n */\n nullifier: string;\n}\n\nexport interface SessionPayload extends JWTPayload {\n sub: string;\n /**\n * Opaque public-facing identifier · `did:oc:<32-hex>`. The sole\n * canonical user identifier post auth-refactor. Stable across\n * linking events. Per AUTH-REFACTOR-PLAN.md §2.1.\n */\n did_oc: string;\n jti: string;\n /**\n * Optional display name set by the user via the auth host's profile\n * surface. When present, consumer subdomains can render it in their\n * header chip without a network round-trip. Re-minted on signin and\n * whenever the user updates their profile.\n */\n name?: string | null;\n /** Optional Nostr npub set by the user. Same lifecycle as `name`. */\n npub?: string | null;\n /**\n * Slug of the federation this user is bound to (their \"home\"\n * federation). Multi-federation routing reads this; v1 has one live\n * federation so it's set on first signin and rarely changes. Absent\n * on tokens minted before this field shipped, and on accounts that\n * have never bound to a federation. Consumers fall back to the\n * directory default at /api/federations when absent.\n */\n home_federation?: string | null;\n /**\n * Discriminator recording where this user is on the custody-state\n * graph. Three legitimate states:\n *\n * - 'fedimint_threshold' — federation custody via the OC-introduced\n * federation (default for email-OTP signups; OC's threshold\n * guardians hold key shares).\n * - 'fedimint_client' — user moved their balance to a Fedimint\n * federation they picked themselves (still federation custody,\n * but on a federation OC didn't introduce).\n * - 'bip322' — full self-custody. The user signs with\n * their own Bitcoin key. OC has zero authority over their funds.\n *\n * Graduation is the product thesis. This field is the structural\n * primitive that records progress along the custody-state graph;\n * transitions are recorded as anchored \"rebind\" envelopes. Absent\n * on tokens minted before the field shipped — consumers should\n * treat undefined as 'fedimint_threshold' for email-OTP identities\n * and 'bip322' for BIP-322 identities (the default-by-construction\n * mapping).\n */\n signing_method?: \"fedimint_threshold\" | \"fedimint_client\" | \"bip322\" | null;\n /**\n * Historical did_ocs consolidated INTO this account via the\n * /api/auth/link/btc transfer-under-dual-proof flow. Consumers that\n * aggregate per-user data should union queries across\n * `[did_oc, ...merged_from]`. Absent / empty means \"no absorbed\n * accounts.\" Order is not significant.\n */\n merged_from?: string[];\n /**\n * Unix seconds when the user last successfully completed a WebAuthn\n * step-up assertion on the auth host. Consumer subdomains gate\n * sensitive actions on `(now - step_up_at) < their_window` —\n * typically 5 minutes. Absent on JWTs minted before WebAuthn\n * shipped, or on tokens minted by ordinary signin (only set by\n * /api/auth/webauthn/assertion/verify on success). Use\n * `verifyStepUpClaim()` to read it safely.\n */\n step_up_at?: number;\n /**\n * Unix seconds when the user last successfully completed an inline\n * sudo-mode re-authentication on the auth host (email-OTP for\n * email-primary identities, BIP-322 challenge for btc-primary\n * identities). Gates auth-graph-mutating operations — register an\n * additional WebAuthn credential, revoke one, link a new identity,\n * change recovery method — without forcing a full sign-out/sign-in.\n * Same shape and freshness window posture as `step_up_at`; the two\n * are independent claims (sudo proves \"you typed your password\n * again,\" step-up proves \"you tapped your hardware key\"). Read it\n * with `verifySudoClaim()`.\n */\n sudo_at?: number;\n /**\n * Optional best-effort owner flag · set by the auth host at signin\n * time when the new did_oc matches OWNER_OC_ADDRESSES. Surfaced\n * through to clients via `useOcSession().account.isOwner` so the\n * family-switcher and other low-stakes UX can render owner-only\n * affordances (e.g. an analytics.ochk.io entry).\n *\n * **NOT A SECURITY BOUNDARY.** Sensitive surfaces still re-check\n * the live `OWNER_OC_ADDRESSES` env against `session.did_oc` on\n * every request (analytics.ochk.io's requireOwner does this).\n * If an owner is removed from the env, their JWT may still carry\n * `is_owner: true` for up to 30 days — the UX hint goes stale,\n * but every gated action they attempt re-fails server-side.\n *\n * Treat absence as `false`. Set only on first signin or re-issue;\n * never mutated mid-session.\n */\n is_owner?: boolean;\n /**\n * The identity the user has chosen to show in their account badge\n * — the collapsed label every family site (and any integrator)\n * renders instead of the raw `did:oc`. Baked into the JWT so the\n * choice is consistent across every `.ochk.io` subdomain with no\n * round-trip, the same posture as `name` / `npub`.\n *\n * Re-resolved by the auth host on every mint: an explicit user\n * promotion wins; absent that, it defaults to the account's\n * sign-in identity (BIP-322 accounts → their Bitcoin address,\n * federation accounts → their email). Absent / null on tokens\n * minted before this field shipped — consumers fall back to the\n * `did_oc`. Read it safely with `resolveDisplayIdentity()`.\n */\n display_identity?: DisplayIdentity | null;\n /**\n * Present when the account is backed by a bond. Absent means no bond, not\n * a failed check. Read it with `readBondClaim()` / `bondMeets()`.\n */\n oc_bond?: BondClaim;\n}\n\n/** The token's bond claim when it is well-formed, else null. */\nexport function readBondClaim(payload: SessionPayload): BondClaim | null {\n const b = payload.oc_bond as Partial<BondClaim> | undefined;\n if (!b || typeof b !== \"object\" || b.v !== 1) return null;\n const nums = [b.sats_floor, b.days_floor, b.height, b.checked_at];\n if (!nums.every((n) => typeof n === \"number\" && Number.isFinite(n) && n >= 0)) return null;\n if (typeof b.nullifier !== \"string\" || !/^[0-9a-f]{64}$/.test(b.nullifier)) return null;\n return b as BondClaim;\n}\n\n/**\n * True when the account is backed by a coin whose floors reach `minSats`\n * and `minDays`, and the issuer checked it within\n * `maxAgeSecs` (default: no limit). Compare against the floor ladder: a\n * `minSats` of 50000 is met by a `sats_floor` of 100000, not 10000.\n */\nexport function bondMeets(\n payload: SessionPayload,\n need: { minSats?: number; minDays?: number; maxAgeSecs?: number },\n): boolean {\n const b = readBondClaim(payload);\n if (!b) return false;\n if (b.sats_floor < (need.minSats ?? 0)) return false;\n if (b.days_floor < (need.minDays ?? 0)) return false;\n if (need.maxAgeSecs !== undefined) {\n const age = Math.floor(Date.now() / 1000) - b.checked_at;\n if (age < 0 || age > need.maxAgeSecs) return false;\n }\n return true;\n}\n\n/**\n * Returns true when the JWT carries a fresh `step_up_at` claim —\n * i.e. the user successfully completed a WebAuthn assertion within\n * the supplied freshness window (in seconds). Returns false for\n * tokens that lack the claim entirely, that carry a stale value,\n * that carry a value somehow in the future, or where the freshness\n * window is non-positive.\n *\n * Consumers gating sensitive actions read this in two places:\n *\n * - Client side · before calling the action, to decide whether to\n * trigger `useStepUpAuth()`. Skip the prompt if already fresh.\n * - Server side · in the route handler, AFTER verifying the JWT,\n * to enforce the policy regardless of what the client did.\n *\n * The freshness window is consumer-chosen — typical default is 300s\n * (5 min); higher-value spends might use 60s.\n */\nexport function verifyStepUpClaim(\n payload: SessionPayload,\n opts: { max_age_secs: number },\n): boolean {\n if (typeof payload.step_up_at !== \"number\") return false;\n if (!Number.isFinite(payload.step_up_at)) return false;\n if (opts.max_age_secs <= 0) return false;\n const ageSec = Math.floor(Date.now() / 1000) - payload.step_up_at;\n return ageSec >= 0 && ageSec < opts.max_age_secs;\n}\n\n/**\n * Returns true when the JWT carries a fresh `sudo_at` claim — i.e. the\n * user successfully re-authenticated inline (email-OTP or BIP-322\n * challenge) within the supplied freshness window. Independent of\n * `step_up_at` · sudo is \"you proved your primary identity again,\"\n * step-up is \"you tapped your hardware key.\"\n *\n * Returns false for tokens that lack the claim, that carry a stale\n * value, that carry a future-dated value (clock skew or malicious\n * mint), or where the freshness window is non-positive.\n *\n * Use this to gate auth-graph-mutating operations on the auth host —\n * adding a second hardware key, linking a new identity, generating\n * recovery codes, changing recovery method. Default window is 5\n * minutes; tighten per-operation as needed.\n */\nexport function verifySudoClaim(\n payload: SessionPayload,\n opts: { max_age_secs: number },\n): boolean {\n if (typeof payload.sudo_at !== \"number\") return false;\n if (!Number.isFinite(payload.sudo_at)) return false;\n if (opts.max_age_secs <= 0) return false;\n const ageSec = Math.floor(Date.now() / 1000) - payload.sudo_at;\n return ageSec >= 0 && ageSec < opts.max_age_secs;\n}\n\n/**\n * Resolve the account-badge identity for a session — total, never\n * throws, always returns a renderable `{ kind, value }`.\n *\n * Returns the JWT's `display_identity` claim when it is present and\n * well-formed; otherwise falls back to the canonical `did:oc`. Use\n * this everywhere you render the badge label rather than reading the\n * raw claim, so tokens minted before the field shipped (and tokens\n * carrying a malformed claim) degrade gracefully to the did.\n *\n * This is the single resolver both `<OcAccountMenu>` and integrators\n * building their own chip should call.\n */\nexport function resolveDisplayIdentity(\n payload: SessionPayload,\n): DisplayIdentity {\n const claim = payload.display_identity;\n if (\n claim &&\n typeof claim === \"object\" &&\n typeof claim.value === \"string\" &&\n claim.value.length > 0 &&\n (DISPLAY_IDENTITY_KINDS as readonly string[]).includes(claim.kind)\n ) {\n return { kind: claim.kind, value: claim.value };\n }\n return { kind: \"did\", value: payload.did_oc };\n}\n\n/**\n * Which audience a verifier accepts.\n *\n * omitted → a FAMILY session only: the token must carry no `aud`.\n * a string → a token bound to that audience (or one of several).\n * \"*\" → either. For the routes built to receive tokens that\n * integrators forward.\n *\n * Omitted is the strict default: a token bound to a relying party is that\n * party's credential and never a family sign-in. Tokens minted before\n * audiences existed carry no `aud`, so every existing session keeps\n * verifying exactly as before.\n */\nexport type AudienceRule = string | readonly string[] | \"*\";\n\n/** Pure · does the payload's `aud` satisfy the rule? */\nexport function audienceSatisfied(aud: unknown, rule: AudienceRule | undefined): boolean {\n const have = aud === undefined ? [] : Array.isArray(aud) ? aud : [aud];\n if (rule === \"*\") return true;\n if (rule === undefined) return have.length === 0;\n const want = typeof rule === \"string\" ? [rule] : rule;\n return have.some((a) => typeof a === \"string\" && want.includes(a));\n}\n\nexport interface VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 public key. */\n publicJwk: string;\n /** Expected `iss` claim. Tokens with a different issuer are rejected. */\n issuer?: string;\n /** Accepted `aud` · see AudienceRule. Omit for family sessions. */\n audience?: AudienceRule;\n}\n\nexport interface SignConfig extends VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 private key. */\n privateJwk: string;\n /** Short key id placed in the JWT header (`kid`). Must match the JWK. */\n kid: string;\n}\n\n// ─── JWK helpers ────────────────────────────────────────────────────────\n\nfunction decodeJwk(value: string): Record<string, unknown> {\n const json =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(value, \"base64url\").toString(\"utf8\")\n : new TextDecoder().decode(base64UrlDecode(value));\n const parsed = JSON.parse(json) as Record<string, unknown>;\n if (!parsed || typeof parsed !== \"object\") {\n throw new Error(\n \"[@orangecheck/auth-core] JWK env did not decode to an object\",\n );\n }\n return parsed;\n}\n\nfunction base64UrlDecode(input: string): Uint8Array {\n const pad = input.length % 4 === 0 ? \"\" : \"=\".repeat(4 - (input.length % 4));\n const b64 = (input + pad).replace(/-/g, \"+\").replace(/_/g, \"/\");\n const bin = typeof atob !== \"undefined\" ? atob(b64) : \"\";\n const bytes = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);\n return bytes;\n}\n\nlet publicKeyPromise: Promise<AuthKey> | null = null;\nlet publicKeyCache: string | null = null;\nexport function loadPublicKey(publicJwk: string): Promise<AuthKey> {\n if (!publicKeyPromise || publicKeyCache !== publicJwk) {\n publicKeyCache = publicJwk;\n publicKeyPromise = importJWK(\n decodeJwk(publicJwk),\n JWT_ALG,\n ) as Promise<AuthKey>;\n }\n return publicKeyPromise;\n}\n\nlet privateKeyPromise: Promise<AuthKey> | null = null;\nlet privateKeyCache: string | null = null;\nexport function loadPrivateKey(privateJwk: string): Promise<AuthKey> {\n if (!privateKeyPromise || privateKeyCache !== privateJwk) {\n privateKeyCache = privateJwk;\n privateKeyPromise = importJWK(\n decodeJwk(privateJwk),\n JWT_ALG,\n ) as Promise<AuthKey>;\n }\n return privateKeyPromise;\n}\n\n/** Parse the `OC_AUTH_PUBLIC_JWK` env var back into a JWK object. */\nexport function parsePublicJwk(publicJwk: string): Record<string, unknown> {\n return decodeJwk(publicJwk);\n}\n\n// ─── Sign / verify ──────────────────────────────────────────────────────\n\n/**\n * Mint a fresh oc_session JWT. Only called on the auth host (the only\n * place that holds the private key).\n *\n * The caller owns recording an ended token's `jti` for the revocation\n * feed that {@link createRevocationCheck} reads.\n */\nexport async function signSession(\n claims: {\n sub: string;\n did_oc: string;\n jti: string;\n name?: string | null;\n npub?: string | null;\n home_federation?: string | null;\n signing_method?: \"fedimint_threshold\" | \"fedimint_client\" | \"bip322\" | null;\n merged_from?: string[];\n step_up_at?: number;\n sudo_at?: number;\n display_identity?: DisplayIdentity | null;\n /** Bind the token to one relying party (e.g. `https://example.com`).\n * Omit for a family session. A bound token verifies only where that\n * audience is expected — never as a family sign-in. */\n aud?: string;\n },\n cfg: SignConfig,\n ttlSeconds: number,\n): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n // did_oc is the sole user identifier. The legacy `addr` shim that\n // mirrored did_oc was dropped in v2.0.0 along with the type field.\n const { aud, ...rest } = claims;\n const jwt = new SignJWT(rest)\n .setProtectedHeader({ alg: JWT_ALG, typ: \"JWT\", kid: cfg.kid })\n .setIssuer(cfg.issuer ?? DEFAULT_ISSUER)\n .setIssuedAt(now)\n .setExpirationTime(now + ttlSeconds);\n if (aud) jwt.setAudience(aud);\n return jwt.sign(await loadPrivateKey(cfg.privateJwk));\n}\n\n/**\n * Crypto-only JWT verify. Returns the payload on success, `null` on\n * any failure (bad signature, expired, wrong issuer, malformed).\n *\n * Safe for consumer subdomains — they only need the public JWK. A signed-out\n * token still verifies here; pass the payload to\n * {@link familyRevocationCheck} to refuse it.\n */\nexport async function verifySessionToken(\n token: string,\n cfg: VerifyConfig,\n): Promise<SessionPayload | null> {\n try {\n const res = await jwtVerify(token, await loadPublicKey(cfg.publicJwk), {\n algorithms: [JWT_ALG],\n issuer: cfg.issuer ?? DEFAULT_ISSUER,\n });\n const p = res.payload as SessionPayload;\n if (!p.sub || !p.did_oc || !p.jti) return null;\n if (!audienceSatisfied(p.aud, cfg.audience)) return null;\n return p;\n } catch {\n return null;\n }\n}\n\n// ─── Cookie helpers ─────────────────────────────────────────────────────\n\nexport interface CookieOptions {\n domain?: string;\n maxAge?: number;\n expires?: Date;\n path?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: \"Lax\" | \"Strict\" | \"None\";\n}\n\n/**\n * Serialize a cookie header value for `Set-Cookie`. Defaults match the\n * ochk.io session shape: HttpOnly, SameSite=Lax, Path=/, Secure in prod.\n */\nexport function serializeSessionCookie(\n token: string,\n opts: CookieOptions = {},\n): string {\n const parts: string[] = [`${SESSION_COOKIE}=${token}`];\n parts.push(`Path=${opts.path ?? \"/\"}`);\n if (opts.httpOnly ?? true) parts.push(\"HttpOnly\");\n parts.push(`SameSite=${opts.sameSite ?? \"Lax\"}`);\n if (opts.secure ?? true) parts.push(\"Secure\");\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n if (opts.maxAge != null) parts.push(`Max-Age=${opts.maxAge}`);\n if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);\n return parts.join(\"; \");\n}\n\n/** Clear the session cookie. Must match the Domain used when it was set. */\nexport function clearSessionCookieHeader(\n opts: Pick<CookieOptions, \"domain\" | \"path\" | \"secure\"> = {},\n): string {\n return serializeSessionCookie(\"\", {\n ...opts,\n maxAge: 0,\n expires: new Date(0),\n });\n}\n\n/**\n * Read the oc_session token out of a raw `Cookie:` header string.\n * Returns `null` if the cookie is missing or empty.\n */\nexport function readSessionCookie(\n cookieHeader: string | null | undefined,\n): string | null {\n if (!cookieHeader) return null;\n const prefix = `${SESSION_COOKIE}=`;\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim();\n if (trimmed.startsWith(prefix)) {\n const val = trimmed.slice(prefix.length);\n return val.length > 0 ? val : null;\n }\n }\n return null;\n}\n\n/**\n * Read EVERY oc_session value out of a raw `Cookie:` header string.\n * Multiple same-name cookies are legitimate (e.g. a stale host-scoped\n * cookie shadowing the `Domain=.ochk.io` one) — verification should\n * try each rather than trust ordering.\n */\nexport function readAllSessionCookies(\n cookieHeader: string | null | undefined,\n): string[] {\n if (!cookieHeader) return [];\n const prefix = `${SESSION_COOKIE}=`;\n const out: string[] = [];\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim();\n if (trimmed.startsWith(prefix)) {\n const val = trimmed.slice(prefix.length);\n if (val.length > 0) out.push(val);\n }\n }\n return out;\n}\n\n// ─── Per-tab session resolution (multi-account · tab pinning) ───────────\n//\n// One browser holds ONE shared `oc_session` cookie (the browser-wide\n// default account) but may be signed into N roster accounts, each with\n// its own concurrently-valid session JWT. A tab \"pins\" itself to one\n// account by holding that account's JWT in sessionStorage and sending\n// it on every API call as the `x-oc-tab-session` header. The header\n// value is a full session JWT — a *credential*, never a bare selector —\n// so servers verify it exactly like the cookie token and stay\n// stateless.\n\n/** Header carrying a tab-pinned session JWT. Lowercase (Node folds headers). */\nexport const TAB_SESSION_HEADER = \"x-oc-tab-session\" as const;\n\nexport type ResolveSessionResult =\n | { ok: true; payload: SessionPayload; via: \"tab\" | \"cookie\" }\n | { ok: false; reason: \"tab_invalid\" | \"no_session\" };\n\n/** Header bag shapes accepted by {@link resolveSessionFromRequest}. */\nexport type IncomingRequestHeaders =\n Headers | Record<string, string | string[] | undefined>;\n\nfunction headerValue(\n headers: IncomingRequestHeaders,\n name: string,\n): string | null {\n if (isHeaders(headers)) return headers.get(name);\n const raw = headers[name] ?? headers[name.toLowerCase()];\n if (Array.isArray(raw)) return raw[0] ?? null;\n return typeof raw === \"string\" ? raw : null;\n}\n\n/**\n * Resolve the EFFECTIVE session for a request — the per-tab choke\n * point every consumer's `readJwtSession` should delegate to.\n *\n * Precedence:\n * 1. `x-oc-tab-session` header, when present. **Fail-closed**: a\n * present-but-invalid tab token resolves to\n * `{ ok: false, reason: 'tab_invalid' }` rather than silently\n * falling back to the cookie — falling back would execute the\n * request as a DIFFERENT account than the tab is displaying,\n * which is precisely the bug per-tab pinning exists to prevent.\n * Clients clear their pin on 401 and re-resolve.\n * 2. Every `oc_session` cookie in the jar, first one that verifies.\n *\n * Crypto-only (signature + exp + iss via {@link verifySessionToken}); a\n * signed-out token still resolves, so pass the payload to\n * {@link familyRevocationCheck}. Never throws.\n */\nexport async function resolveSessionFromRequest(\n headers: IncomingRequestHeaders,\n cfg: VerifyConfig,\n): Promise<ResolveSessionResult> {\n const tabToken = headerValue(headers, TAB_SESSION_HEADER);\n if (tabToken) {\n const payload = await verifySessionToken(tabToken, cfg);\n if (payload) return { ok: true, payload, via: \"tab\" };\n return { ok: false, reason: \"tab_invalid\" };\n }\n const cookieHeader = headerValue(headers, \"cookie\");\n for (const token of readAllSessionCookies(cookieHeader)) {\n const payload = await verifySessionToken(token, cfg);\n if (payload) return { ok: true, payload, via: \"cookie\" };\n }\n return { ok: false, reason: \"no_session\" };\n}\n\n// ─── JWKS-aware verification (zero env vars · zero JWK handling) ─────────\n//\n// verifyOcToken / getOcSession are the integrator-facing verification\n// primitives. They lazy-fetch the auth host's published JWKS at\n// `<issuer>/.well-known/jwks.json` and cache it in-process. Integrators\n// install @orangecheck/me-client (which re-exports these), call\n// `verifyOcToken(token)` or `getOcSession(headers)`, and never see a\n// JWK string, an env var, or a base64 dance.\n\nconst DEFAULT_JWKS_TTL_MS = 60 * 60 * 1000; // 1 hour\n\ninterface JwksCacheEntry {\n keys: Record<string, AuthKey>;\n fetchedAt: number;\n}\n\nconst jwksCache = new Map<string, JwksCacheEntry>();\nconst inflight = new Map<string, Promise<JwksCacheEntry>>();\n\n/**\n * When each issuer last had its cache force-refreshed by an unrecognised\n * `kid`, and how long that path must wait before doing it again.\n *\n * Without this, a token carrying any unknown kid makes the verifier drop its\n * cache and fetch the JWKS again — so a stream of tokens with random kids turns\n * every integrator's server into a refetch amplifier pointed at the auth host,\n * and puts a network round-trip in front of each rejection. `inflight` only\n * collapses CONCURRENT fetches; sequential requests each got their own.\n *\n * A minute is far below any real rotation window, so a genuinely new key is\n * still picked up promptly — the bust exists for rotation, not for every\n * malformed token.\n */\nconst lastForcedRefresh = new Map<string, number>();\nconst FORCED_REFRESH_MIN_INTERVAL_MS = 60_000;\n\ninterface JwksJson {\n keys: Array<Record<string, unknown> & { kid?: string }>;\n}\n\nasync function fetchJwks(\n issuer: string,\n ttlMs: number,\n): Promise<JwksCacheEntry> {\n const cached = jwksCache.get(issuer);\n if (cached && Date.now() - cached.fetchedAt < ttlMs) return cached;\n const existing = inflight.get(issuer);\n if (existing) return existing;\n const p = (async (): Promise<JwksCacheEntry> => {\n try {\n const url = `${issuer.replace(/\\/$/, \"\")}/.well-known/jwks.json`;\n const res = await fetch(url, { headers: { Accept: \"application/json\" } });\n if (!res.ok) {\n throw new Error(\n `[@orangecheck/auth-core] JWKS fetch ${url} returned ${res.status}`,\n );\n }\n const json = (await res.json()) as JwksJson;\n if (!json || !Array.isArray(json.keys)) {\n throw new Error(\n `[@orangecheck/auth-core] JWKS at ${url} did not return {keys:[…]}`,\n );\n }\n const keys: Record<string, AuthKey> = {};\n for (const k of json.keys) {\n if (typeof k.kid !== \"string\") continue;\n try {\n keys[k.kid] = (await importJWK(k, JWT_ALG)) as AuthKey;\n } catch {\n /* skip keys that won't import for our alg */\n }\n }\n const entry: JwksCacheEntry = { keys, fetchedAt: Date.now() };\n jwksCache.set(issuer, entry);\n return entry;\n } catch (err) {\n // Stale-on-error: a transient JWKS outage shouldn't lock out\n // every consumer. If we have a cache entry — even an expired\n // one — keep using it until the next successful fetch. Throw\n // only when we've never seen a usable JWKS.\n const stale = jwksCache.get(issuer);\n if (stale) return stale;\n throw err;\n }\n })().finally(() => {\n inflight.delete(issuer);\n });\n inflight.set(issuer, p);\n return p;\n}\n\nfunction assertSafeIssuer(issuer: string): void {\n let url: URL;\n try {\n url = new URL(issuer);\n } catch {\n throw new Error(\n `[@orangecheck/auth-core] issuer must be an absolute URL, got ${issuer}`,\n );\n }\n // Allow http only for loopback / local dev. Any production-shaped\n // hostname (real DNS) is required to be https — JWKS fetched over\n // plaintext HTTP is trivially MITM-able and would defeat the whole\n // verification stack.\n const isLoopback =\n url.hostname === \"localhost\" ||\n url.hostname === \"127.0.0.1\" ||\n url.hostname === \"::1\";\n if (url.protocol !== \"https:\" && !isLoopback) {\n throw new Error(\n `[@orangecheck/auth-core] issuer must be https (got ${url.protocol}//${url.hostname}); plaintext JWKS is not safe`,\n );\n }\n}\n\nexport interface VerifyOcOptions {\n /** Auth host issuer. Defaults to https://ochk.io. */\n issuer?: string;\n /** JWKS cache TTL in ms. Defaults to 1 hour. Stale-on-error: if the\n * cache exists, verification still works during a transient outage. */\n jwksCacheTtlMs?: number;\n /** Accepted `aud` · see AudienceRule. An integrator verifying tokens from\n * the sign-in popup sets this to its own origin, e.g.\n * `\"https://example.com\"`, so a token issued to another site is refused. */\n audience?: AudienceRule;\n}\n\n/**\n * Verify a JWT issued by an OC auth host. Lazy-fetches the JWKS from\n * `<issuer>/.well-known/jwks.json`, picks the key whose `kid` matches\n * the token's protected header, and verifies the signature.\n *\n * Returns the payload on success, `null` on any failure (bad signature,\n * expired, wrong issuer, kid not in JWKS, malformed). Never throws.\n *\n * Integrators don't need to handle JWKs, env vars, or rotation — the\n * cache picks up new keys automatically when they appear in the JWKS\n * response. Stale tokens signed under a retired key continue to verify\n * as long as the retired key is still published in the JWKS (standard\n * key-rotation overlap window).\n */\nexport async function verifyOcToken(\n token: string,\n options: VerifyOcOptions = {},\n): Promise<SessionPayload | null> {\n const issuer = options.issuer ?? DEFAULT_ISSUER;\n const ttl = options.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;\n try {\n assertSafeIssuer(issuer);\n // Fast path: try the cache. If the kid isn't in the cache (e.g.\n // brand-new key just rotated in), fall through to a fresh fetch.\n let entry = await fetchJwks(issuer, ttl);\n const headerB64 = token.split(\".\")[0];\n if (!headerB64) return null;\n const headerJson =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(headerB64, \"base64url\").toString(\"utf8\")\n : new TextDecoder().decode(base64UrlDecode(headerB64));\n const header = JSON.parse(headerJson) as { kid?: string; alg?: string };\n if (!header.kid) return null;\n let key = entry.keys[header.kid];\n if (!key) {\n // Force a fresh JWKS fetch in case the integrator's cache is older\n // than the active key set on the auth host — but at most once a\n // minute per issuer, so an unknown kid cannot be used to drive\n // unbounded refetches (see lastForcedRefresh).\n const now = Date.now();\n const last = lastForcedRefresh.get(issuer) ?? 0;\n if (now - last >= FORCED_REFRESH_MIN_INTERVAL_MS) {\n lastForcedRefresh.set(issuer, now);\n jwksCache.delete(issuer);\n entry = await fetchJwks(issuer, ttl);\n key = entry.keys[header.kid];\n }\n }\n if (!key) return null;\n const res = await jwtVerify(token, key, {\n algorithms: [JWT_ALG],\n issuer,\n });\n const p = res.payload as SessionPayload;\n if (!p.sub || !p.did_oc || !p.jti) return null;\n if (!audienceSatisfied(p.aud, options.audience)) return null;\n return p;\n } catch {\n return null;\n }\n}\n\nexport interface SessionRequestHeaders {\n cookie?: string | null;\n authorization?: string | null;\n}\n\n/**\n * Verify the OC session for a request. Accepts either a plain object\n * with `cookie` / `authorization` properties (Express / Next.js / etc.)\n * or a Web Headers object (Hono / edge / Fetch API). Reads the session\n * from the Cookie header first; falls back to a `Authorization: Bearer\n * <token>` header so cross-domain integrators (different eTLD+1, no\n * .ochk.io cookie) can verify the same way.\n *\n * Returns `null` for unauthenticated, missing, or invalid requests.\n * Never throws.\n */\nexport async function getOcSession(\n headers: SessionRequestHeaders | Headers,\n options: VerifyOcOptions = {},\n): Promise<SessionPayload | null> {\n const cookie = isHeaders(headers)\n ? headers.get(\"cookie\")\n : (headers.cookie ?? null);\n const authorization = isHeaders(headers)\n ? headers.get(\"authorization\")\n : (headers.authorization ?? null);\n let token = readSessionCookie(cookie);\n if (!token && typeof authorization === \"string\") {\n const m = /^Bearer\\s+(.+)$/i.exec(authorization);\n if (m) token = m[1]!;\n }\n if (!token) return null;\n return verifyOcToken(token, options);\n}\n\nfunction isHeaders(value: unknown): value is Headers {\n return (\n typeof Headers !== \"undefined\" &&\n value !== null &&\n typeof value === \"object\" &&\n value instanceof Headers\n );\n}\n\n/**\n * Clear the module-level JWKS caches. Test-only — the caches are process-wide\n * by design (that is the point of them), which makes any test that touches\n * verification order-dependent without a reset.\n */\nexport function _resetJwksCachesForTests(): void {\n jwksCache.clear();\n inflight.clear();\n lastForcedRefresh.clear();\n}\n\nexport * from \"./revocation\";\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/revocation.ts","../src/index.ts"],"names":["inflight"],"mappings":"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","file":"index.mjs","sourcesContent":["// ─── Family-wide session revocation ─────────────────────────────────────\n//\n// A session token verifies by signature alone, so a site that checks only the\n// signature honours a token for its whole life, even after it was signed out,\n// replaced, or its account deleted. The auth host publishes the sessions that\n// ended at `<issuer>/api/auth/revocations`, to sites holding a feed token. This\n// check fetches that list at most once per `ttlMs` per process and answers from\n// memory, so a request never waits on the host while the copy is fresh.\n//\n// Entries are keyed by a truncated hash, so the list names no account:\n// - a token, by `ocrev1:jti:<jti>`: that token is ended.\n// - an account, by `ocrev1:sub:<sub>`, with a cutoff: every token of that\n// account issued before it (`iat < cutoff`) is ended.\n\n/** The claims a revocation decision reads. A verified session payload has them all. */\nexport interface RevocableClaims {\n jti?: unknown;\n sub?: unknown;\n iat?: unknown;\n}\n\n/**\n * `live`: not ended, per a copy of the list fresh enough to vouch for it.\n * `revoked`: ended. `unverified`: not known to be ended, but the copy is too\n * old (or was never fetched) to say so for a request that changes state.\n */\nexport type RevocationStatus = \"live\" | \"revoked\" | \"unverified\";\n\nexport interface RevocationCheck {\n status(claims: RevocableClaims): Promise<RevocationStatus>;\n /**\n * Whether to refuse the session for a request with this method: always when\n * revoked, and for an unsafe method (or none given) when unverified. Reads\n * keep working through an outage of the auth host; writes stop.\n */\n refuses(claims: RevocableClaims, method?: string): Promise<boolean>;\n}\n\nexport interface RevocationCheckOptions {\n /** Default `${OC_AUTH_ISSUER ?? \"https://ochk.io\"}/api/auth/revocations`. */\n url?: string;\n /** This site's feed token. Default `OC_REVOCATION_FEED_TOKEN`. */\n token?: string;\n /**\n * Headers for each fetch, in place of the feed token: an integrator's\n * request signed with its project secret ({@link integratorRevocationCheck}).\n */\n headers?: () => Record<string, string> | Promise<Record<string, string>>;\n /** How long a copy is used before fetching again. Default 60 s. */\n ttlMs?: number;\n /** Past this age a copy cannot vouch for a write. Default 10 min. */\n maxStaleMs?: number;\n /** One fetch's time limit. Default 3 s. */\n timeoutMs?: number;\n /** After a failed fetch, how long to wait before the next. Default 10 s. */\n retryMs?: number;\n /**\n * With no token: true answers `live` (local development and tests, where no\n * feed exists); false answers `unverified`, so writes are refused. Default:\n * true unless NODE_ENV is production.\n */\n developing?: boolean;\n fetch?: typeof fetch;\n now?: () => number;\n}\n\ninterface Snapshot {\n tokens: Set<string>;\n accounts: Map<string, number>;\n fetchedAt: number;\n}\n\nconst SAFE_METHODS = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\nconst DEFAULT_TTL_MS = 60_000;\nconst DEFAULT_MAX_STALE_MS = 10 * 60_000;\nconst DEFAULT_TIMEOUT_MS = 3_000;\nconst DEFAULT_RETRY_MS = 10_000;\n\nfunction env(name: string): string | undefined {\n return typeof process !== \"undefined\" ? process.env?.[name] : undefined;\n}\n\n/**\n * The feed key for a token (`kind: \"jti\"`) or an account (`kind: \"sub\"`):\n * base64url of the first 16 bytes of sha256(`ocrev1:<kind>:<value>`). The\n * auth host derives the same; its tests share these vectors.\n */\nexport async function revocationKey(\n kind: \"jti\" | \"sub\",\n value: string,\n): Promise<string> {\n const digest = await globalThis.crypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(`ocrev1:${kind}:${value}`),\n );\n let bin = \"\";\n for (const b of new Uint8Array(digest).subarray(0, 16)) bin += String.fromCharCode(b);\n return btoa(bin).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nfunction parseFeed(body: unknown, fetchedAt: number): Snapshot {\n const b = body as { v?: unknown; tokens?: unknown; accounts?: unknown } | null;\n if (!b || b.v !== 1 || !Array.isArray(b.tokens) || typeof b.accounts !== \"object\" || !b.accounts) {\n throw new Error(\"revocation feed: unexpected body\");\n }\n const tokens = new Set<string>();\n for (const t of b.tokens) if (typeof t === \"string\") tokens.add(t);\n const accounts = new Map<string, number>();\n for (const [k, v] of Object.entries(b.accounts as Record<string, unknown>)) {\n if (typeof v === \"number\" && Number.isFinite(v)) accounts.set(k, v);\n }\n return { tokens, accounts, fetchedAt };\n}\n\n/**\n * A revocation check with its own cached copy of the feed. Create one per\n * process and share it (fluid compute keeps module state across requests);\n * {@link familyRevocationCheck} is that one, configured from the environment.\n */\nexport function createRevocationCheck(opts: RevocationCheckOptions = {}): RevocationCheck {\n const issuer = (env(\"OC_AUTH_ISSUER\") ?? \"https://ochk.io\").replace(/\\/$/, \"\");\n const url = opts.url ?? env(\"OC_REVOCATION_FEED_URL\") ?? `${issuer}/api/auth/revocations`;\n const token = opts.token ?? env(\"OC_REVOCATION_FEED_TOKEN\")?.trim();\n const signer = opts.headers;\n const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;\n const maxStaleMs = opts.maxStaleMs ?? DEFAULT_MAX_STALE_MS;\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const retryMs = opts.retryMs ?? DEFAULT_RETRY_MS;\n const developing = opts.developing ?? env(\"NODE_ENV\") !== \"production\";\n const doFetch = opts.fetch ?? ((...a: Parameters<typeof fetch>) => fetch(...a));\n const now = opts.now ?? Date.now;\n\n let snapshot: Snapshot | null = null;\n let inflight: Promise<void> | null = null;\n let failedAt = -Infinity;\n let warned = false;\n const warn = (msg: string) => {\n if (warned) return;\n warned = true;\n console.warn(`[@orangecheck/auth-core] ${msg}`);\n };\n\n async function load(): Promise<void> {\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n const credential = signer ? await signer() : { authorization: `Bearer ${token}` };\n const res = await doFetch(url, {\n headers: { ...credential, accept: \"application/json\" },\n signal: ctl.signal,\n });\n if (!res.ok) throw new Error(`revocation feed answered ${res.status}`);\n snapshot = parseFeed(await res.json(), now());\n warned = false;\n } finally {\n clearTimeout(timer);\n }\n }\n\n async function current(): Promise<Snapshot | null> {\n const t = now();\n if (snapshot && t - snapshot.fetchedAt < ttlMs) return snapshot;\n if (t - failedAt < retryMs) return snapshot;\n inflight ??= load()\n .catch((err: unknown) => {\n failedAt = now();\n warn(`${err instanceof Error ? err.message : String(err)}; using the last copy`);\n })\n .finally(() => {\n inflight = null;\n });\n await inflight;\n return snapshot;\n }\n\n async function status(claims: RevocableClaims): Promise<RevocationStatus> {\n if (!token && !signer) {\n if (developing) return \"live\";\n warn(\"OC_REVOCATION_FEED_TOKEN is not set; sessions cannot be checked for revocation\");\n return \"unverified\";\n }\n const snap = await current();\n if (!snap) return \"unverified\";\n // A token known to be ended stays ended, however old the copy.\n if (typeof claims.jti === \"string\" && snap.tokens.has(await revocationKey(\"jti\", claims.jti))) {\n return \"revoked\";\n }\n if (typeof claims.sub === \"string\" && snap.accounts.size > 0) {\n const cutoff = snap.accounts.get(await revocationKey(\"sub\", claims.sub));\n if (cutoff !== undefined && !(typeof claims.iat === \"number\" && claims.iat >= cutoff)) {\n return \"revoked\";\n }\n }\n return now() - snap.fetchedAt > maxStaleMs ? \"unverified\" : \"live\";\n }\n\n return {\n status,\n async refuses(claims, method) {\n const s = await status(claims);\n if (s === \"revoked\") return true;\n if (s === \"live\") return false;\n return !SAFE_METHODS.has((method ?? \"\").toUpperCase());\n },\n };\n}\n\nexport interface IntegratorRevocationOptions\n extends Omit<RevocationCheckOptions, \"url\" | \"token\" | \"headers\" | \"developing\"> {\n /** Default `OC_PROJECT_KEY`. */\n projectKey?: string;\n /** Default `OC_SIGNING_SECRET`. */\n signingSecret?: string;\n /** Default `OC_ME_ORIGIN`, else `https://me.ochk.io`. */\n meOrigin?: string;\n}\n\nasync function hmacHex(secret: string, data: string): Promise<string> {\n const enc = new TextEncoder();\n const key = await globalThis.crypto.subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const mac = new Uint8Array(await globalThis.crypto.subtle.sign(\"HMAC\", key, enc.encode(data)));\n return Array.from(mac, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/**\n * For an integrator's server: the account cutoffs (sign out everywhere, a\n * passkey removed, an account deleted) from me.ochk.io's\n * `/api/integrator/revocations`, fetched with a request signed by the project\n * secret, so a session the person ended everywhere is refused here too. Null\n * when the project key or secret is not set: a site that has not configured\n * it keeps verifying by signature alone rather than refusing writes.\n */\nexport function integratorRevocationCheck(\n opts: IntegratorRevocationOptions = {},\n): RevocationCheck | null {\n const { projectKey: pk, signingSecret, meOrigin, ...rest } = opts;\n const projectKey = (pk ?? env(\"OC_PROJECT_KEY\"))?.trim();\n const secret = (signingSecret ?? env(\"OC_SIGNING_SECRET\"))?.trim();\n if (!projectKey || !secret) return null;\n const origin = (meOrigin ?? env(\"OC_ME_ORIGIN\") ?? \"https://me.ochk.io\").replace(/\\/$/, \"\");\n const query = `project_key=${encodeURIComponent(projectKey)}`;\n const now = rest.now ?? Date.now;\n return createRevocationCheck({\n ...rest,\n url: `${origin}/api/integrator/revocations?${query}`,\n headers: async () => {\n const t = Math.floor(now() / 1000);\n return { \"x-oc-signature\": `t=${t},v1=${await hmacHex(secret, `${t}.${query}`)}` };\n },\n developing: false,\n });\n}\n\nconst SHARED = Symbol.for(\"@orangecheck/auth-core/revocation-check/v1\");\n\n/**\n * The process-wide check, configured from the environment\n * (`OC_REVOCATION_FEED_TOKEN`, `OC_AUTH_ISSUER`). Every reader in a site\n * should use this one, so the site fetches the feed once per minute, not once\n * per reader.\n */\nexport function familyRevocationCheck(): RevocationCheck {\n const g = globalThis as { [SHARED]?: RevocationCheck };\n return (g[SHARED] ??= createRevocationCheck());\n}\n\n/** Tests only: drop the process-wide check so the next call reads the environment again. */\nexport function _resetFamilyRevocationCheckForTests(): void {\n delete (globalThis as { [SHARED]?: RevocationCheck })[SHARED];\n}\n","/**\n * @orangecheck/auth-core\n *\n * Cross-subdomain oc_session JWT verification + cookie helpers.\n *\n * The auth HOST (whoever owns sign-in) holds a private key and issues\n * tokens with `signSession()`. Every CONSUMER subdomain holds only the\n * public JWK and verifies tokens locally with `verifySessionToken()` —\n * no network round-trip, no shared secret.\n *\n * The ochk.io ecosystem picks Ed25519 (EdDSA) with JWKs distributed\n * via environment variable. The public key is also published at\n * `/.well-known/jwks.json` on the auth host for dynamic discovery.\n */\n\nimport {\n importJWK,\n jwtVerify,\n SignJWT,\n type CryptoKey,\n type JWTPayload,\n type KeyObject,\n} from \"jose\";\n\nexport const JWT_ALG = \"EdDSA\" as const;\nexport const SESSION_COOKIE = \"oc_session\" as const;\nexport const DEFAULT_ISSUER = \"https://ochk.io\" as const;\n\nexport type AuthKey = CryptoKey | KeyObject;\n\n/**\n * The kinds of identity a user can surface as their account-badge\n * label. `did` is the canonical `did:oc` identifier (always available\n * and the ultimate fallback); the other three are linked identities\n * the user may or may not have.\n */\nexport const DISPLAY_IDENTITY_KINDS = [\"did\", \"btc\", \"email\", \"nostr\"] as const;\nexport type DisplayIdentityKind = (typeof DISPLAY_IDENTITY_KINDS)[number];\n\n/**\n * The identity a user has chosen to display in their account badge —\n * carried as a JWT claim so every `.ochk.io` subdomain (and any\n * integrator reading the session) renders the same label with no\n * network round-trip, exactly like `name` / `npub`.\n *\n * `kind` is which identity; `value` is the full, renderable value (the\n * `did:oc`, the Bitcoin address, the email, or the npub). Only the\n * *promoted* identity's value is carried — never the whole identity\n * set — so an unrelated email never enters the token.\n */\nexport interface DisplayIdentity {\n kind: DisplayIdentityKind;\n value: string;\n}\n\n/**\n * A bond (oc-attest-protocol BOND.md): one unspent coin the account pledged.\n * The token carries floors, never the coin, so an app learns that a coin of\n * at least `sats_floor` sats, unspent at least `days_floor` days, backs this\n * account, and nothing about which coin it is.\n */\nexport interface BondClaim {\n v: 1;\n /** Largest of 1000, 10000, 100000, 1000000, 10000000 the coin meets; 0 for none. */\n sats_floor: number;\n /** Largest of 7, 30, 90, 365 days the coin has been unspent; 0 for none. */\n days_floor: number;\n /** Block height at which the issuer last found the coin unspent. */\n height: number;\n /** Unix seconds of that check. */\n checked_at: number;\n /**\n * Same for the same coin at your site, unrelated anywhere else, and keyed by the\n * issuer so it doesn't reveal the coin. *.ochk.io sites share the family session,\n * so they share one. Keep one account per nullifier; block it to block the coin.\n */\n nullifier: string;\n}\n\nexport interface SessionPayload extends JWTPayload {\n sub: string;\n /**\n * Opaque public-facing identifier · `did:oc:<32-hex>`. The sole\n * canonical user identifier post auth-refactor. Stable across\n * linking events. Per AUTH-REFACTOR-PLAN.md §2.1.\n */\n did_oc: string;\n jti: string;\n /**\n * Optional display name set by the user via the auth host's profile\n * surface. When present, consumer subdomains can render it in their\n * header chip without a network round-trip. Re-minted on signin and\n * whenever the user updates their profile.\n */\n name?: string | null;\n /** Optional Nostr npub set by the user. Same lifecycle as `name`. */\n npub?: string | null;\n /**\n * @deprecated No longer minted. Where a person's sats sit is me.ochk.io's\n * to say, per federation, from its own ledger (`/api/me/balance`).\n */\n home_federation?: string | null;\n /**\n * Whether the account signs with its own Bitcoin key. The auth host\n * derives it at every mint from the account's verified linked\n * identities; no one can set it.\n *\n * - 'bip322' — a BIP-322-verified Bitcoin address is linked.\n * - 'fedimint_threshold' — none is, so the account holds no key here.\n * - 'fedimint_client' — reserved; never minted.\n *\n * It says who holds the signing key, never who holds the sats.\n */\n signing_method?: \"fedimint_threshold\" | \"fedimint_client\" | \"bip322\" | null;\n /**\n * Historical did_ocs consolidated INTO this account via the\n * /api/auth/link/btc transfer-under-dual-proof flow. Consumers that\n * aggregate per-user data should union queries across\n * `[did_oc, ...merged_from]`. Absent / empty means \"no absorbed\n * accounts.\" Order is not significant.\n */\n merged_from?: string[];\n /**\n * Unix seconds when the user last successfully completed a WebAuthn\n * step-up assertion on the auth host. Consumer subdomains gate\n * sensitive actions on `(now - step_up_at) < their_window` —\n * typically 5 minutes. Absent on JWTs minted before WebAuthn\n * shipped, or on tokens minted by ordinary signin (only set by\n * /api/auth/webauthn/assertion/verify on success). Use\n * `verifyStepUpClaim()` to read it safely.\n */\n step_up_at?: number;\n /**\n * Unix seconds when the user last successfully completed an inline\n * sudo-mode re-authentication on the auth host (email-OTP for\n * email-primary identities, BIP-322 challenge for btc-primary\n * identities). Gates auth-graph-mutating operations — register an\n * additional WebAuthn credential, revoke one, link a new identity,\n * change recovery method — without forcing a full sign-out/sign-in.\n * Same shape and freshness window posture as `step_up_at`; the two\n * are independent claims (sudo proves \"you typed your password\n * again,\" step-up proves \"you tapped your hardware key\"). Read it\n * with `verifySudoClaim()`.\n */\n sudo_at?: number;\n /**\n * Optional best-effort owner flag · set by the auth host at signin\n * time when the new did_oc matches OWNER_OC_ADDRESSES. Surfaced\n * through to clients via `useOcSession().account.isOwner` so the\n * family-switcher and other low-stakes UX can render owner-only\n * affordances (e.g. an analytics.ochk.io entry).\n *\n * **NOT A SECURITY BOUNDARY.** Sensitive surfaces still re-check\n * the live `OWNER_OC_ADDRESSES` env against `session.did_oc` on\n * every request (analytics.ochk.io's requireOwner does this).\n * If an owner is removed from the env, their JWT may still carry\n * `is_owner: true` for up to 30 days — the UX hint goes stale,\n * but every gated action they attempt re-fails server-side.\n *\n * Treat absence as `false`. Set only on first signin or re-issue;\n * never mutated mid-session.\n */\n is_owner?: boolean;\n /**\n * The identity the user has chosen to show in their account badge\n * — the collapsed label every family site (and any integrator)\n * renders instead of the raw `did:oc`. Baked into the JWT so the\n * choice is consistent across every `.ochk.io` subdomain with no\n * round-trip, the same posture as `name` / `npub`.\n *\n * Re-resolved by the auth host on every mint: an explicit user\n * promotion wins; absent that, it defaults to the account's\n * sign-in identity (BIP-322 accounts → their Bitcoin address,\n * federation accounts → their email). Absent / null on tokens\n * minted before this field shipped — consumers fall back to the\n * `did_oc`. Read it safely with `resolveDisplayIdentity()`.\n */\n display_identity?: DisplayIdentity | null;\n /**\n * Present when the account is backed by a bond. Absent means no bond, not\n * a failed check. Read it with `readBondClaim()` / `bondMeets()`.\n */\n oc_bond?: BondClaim;\n}\n\n/** The token's bond claim when it is well-formed, else null. */\nexport function readBondClaim(payload: SessionPayload): BondClaim | null {\n const b = payload.oc_bond as Partial<BondClaim> | undefined;\n if (!b || typeof b !== \"object\" || b.v !== 1) return null;\n const nums = [b.sats_floor, b.days_floor, b.height, b.checked_at];\n if (!nums.every((n) => typeof n === \"number\" && Number.isFinite(n) && n >= 0)) return null;\n if (typeof b.nullifier !== \"string\" || !/^[0-9a-f]{64}$/.test(b.nullifier)) return null;\n return b as BondClaim;\n}\n\n/**\n * True when the account is backed by a coin whose floors reach `minSats`\n * and `minDays`, and the issuer checked it within\n * `maxAgeSecs` (default: no limit). Compare against the floor ladder: a\n * `minSats` of 50000 is met by a `sats_floor` of 100000, not 10000.\n */\nexport function bondMeets(\n payload: SessionPayload,\n need: { minSats?: number; minDays?: number; maxAgeSecs?: number },\n): boolean {\n const b = readBondClaim(payload);\n if (!b) return false;\n if (b.sats_floor < (need.minSats ?? 0)) return false;\n if (b.days_floor < (need.minDays ?? 0)) return false;\n if (need.maxAgeSecs !== undefined) {\n const age = Math.floor(Date.now() / 1000) - b.checked_at;\n if (age < 0 || age > need.maxAgeSecs) return false;\n }\n return true;\n}\n\n/**\n * Returns true when the JWT carries a fresh `step_up_at` claim —\n * i.e. the user successfully completed a WebAuthn assertion within\n * the supplied freshness window (in seconds). Returns false for\n * tokens that lack the claim entirely, that carry a stale value,\n * that carry a value somehow in the future, or where the freshness\n * window is non-positive.\n *\n * Consumers gating sensitive actions read this in two places:\n *\n * - Client side · before calling the action, to decide whether to\n * trigger `useStepUpAuth()`. Skip the prompt if already fresh.\n * - Server side · in the route handler, AFTER verifying the JWT,\n * to enforce the policy regardless of what the client did.\n *\n * The freshness window is consumer-chosen — typical default is 300s\n * (5 min); higher-value spends might use 60s.\n */\nexport function verifyStepUpClaim(\n payload: SessionPayload,\n opts: { max_age_secs: number },\n): boolean {\n if (typeof payload.step_up_at !== \"number\") return false;\n if (!Number.isFinite(payload.step_up_at)) return false;\n if (opts.max_age_secs <= 0) return false;\n const ageSec = Math.floor(Date.now() / 1000) - payload.step_up_at;\n return ageSec >= 0 && ageSec < opts.max_age_secs;\n}\n\n/**\n * Returns true when the JWT carries a fresh `sudo_at` claim — i.e. the\n * user successfully re-authenticated inline (email-OTP or BIP-322\n * challenge) within the supplied freshness window. Independent of\n * `step_up_at` · sudo is \"you proved your primary identity again,\"\n * step-up is \"you tapped your hardware key.\"\n *\n * Returns false for tokens that lack the claim, that carry a stale\n * value, that carry a future-dated value (clock skew or malicious\n * mint), or where the freshness window is non-positive.\n *\n * Use this to gate auth-graph-mutating operations on the auth host —\n * adding a second hardware key, linking a new identity, generating\n * recovery codes, changing recovery method. Default window is 5\n * minutes; tighten per-operation as needed.\n */\nexport function verifySudoClaim(\n payload: SessionPayload,\n opts: { max_age_secs: number },\n): boolean {\n if (typeof payload.sudo_at !== \"number\") return false;\n if (!Number.isFinite(payload.sudo_at)) return false;\n if (opts.max_age_secs <= 0) return false;\n const ageSec = Math.floor(Date.now() / 1000) - payload.sudo_at;\n return ageSec >= 0 && ageSec < opts.max_age_secs;\n}\n\n/**\n * Resolve the account-badge identity for a session — total, never\n * throws, always returns a renderable `{ kind, value }`.\n *\n * Returns the JWT's `display_identity` claim when it is present and\n * well-formed; otherwise falls back to the canonical `did:oc`. Use\n * this everywhere you render the badge label rather than reading the\n * raw claim, so tokens minted before the field shipped (and tokens\n * carrying a malformed claim) degrade gracefully to the did.\n *\n * This is the single resolver both `<OcAccountMenu>` and integrators\n * building their own chip should call.\n */\nexport function resolveDisplayIdentity(\n payload: SessionPayload,\n): DisplayIdentity {\n const claim = payload.display_identity;\n if (\n claim &&\n typeof claim === \"object\" &&\n typeof claim.value === \"string\" &&\n claim.value.length > 0 &&\n (DISPLAY_IDENTITY_KINDS as readonly string[]).includes(claim.kind)\n ) {\n return { kind: claim.kind, value: claim.value };\n }\n return { kind: \"did\", value: payload.did_oc };\n}\n\n/**\n * Which audience a verifier accepts.\n *\n * omitted → a FAMILY session only: the token must carry no `aud`.\n * a string → a token bound to that audience (or one of several).\n * \"*\" → either. For the routes built to receive tokens that\n * integrators forward.\n *\n * Omitted is the strict default: a token bound to a relying party is that\n * party's credential and never a family sign-in. Tokens minted before\n * audiences existed carry no `aud`, so every existing session keeps\n * verifying exactly as before.\n */\nexport type AudienceRule = string | readonly string[] | \"*\";\n\n/** Pure · does the payload's `aud` satisfy the rule? */\nexport function audienceSatisfied(aud: unknown, rule: AudienceRule | undefined): boolean {\n const have = aud === undefined ? [] : Array.isArray(aud) ? aud : [aud];\n if (rule === \"*\") return true;\n if (rule === undefined) return have.length === 0;\n const want = typeof rule === \"string\" ? [rule] : rule;\n return have.some((a) => typeof a === \"string\" && want.includes(a));\n}\n\nexport interface VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 public key. */\n publicJwk: string;\n /** Expected `iss` claim. Tokens with a different issuer are rejected. */\n issuer?: string;\n /** Accepted `aud` · see AudienceRule. Omit for family sessions. */\n audience?: AudienceRule;\n}\n\nexport interface SignConfig extends VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 private key. */\n privateJwk: string;\n /** Short key id placed in the JWT header (`kid`). Must match the JWK. */\n kid: string;\n}\n\n// ─── JWK helpers ────────────────────────────────────────────────────────\n\nfunction decodeJwk(value: string): Record<string, unknown> {\n const json =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(value, \"base64url\").toString(\"utf8\")\n : new TextDecoder().decode(base64UrlDecode(value));\n const parsed = JSON.parse(json) as Record<string, unknown>;\n if (!parsed || typeof parsed !== \"object\") {\n throw new Error(\n \"[@orangecheck/auth-core] JWK env did not decode to an object\",\n );\n }\n return parsed;\n}\n\nfunction base64UrlDecode(input: string): Uint8Array {\n const pad = input.length % 4 === 0 ? \"\" : \"=\".repeat(4 - (input.length % 4));\n const b64 = (input + pad).replace(/-/g, \"+\").replace(/_/g, \"/\");\n const bin = typeof atob !== \"undefined\" ? atob(b64) : \"\";\n const bytes = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);\n return bytes;\n}\n\nlet publicKeyPromise: Promise<AuthKey> | null = null;\nlet publicKeyCache: string | null = null;\nexport function loadPublicKey(publicJwk: string): Promise<AuthKey> {\n if (!publicKeyPromise || publicKeyCache !== publicJwk) {\n publicKeyCache = publicJwk;\n publicKeyPromise = importJWK(\n decodeJwk(publicJwk),\n JWT_ALG,\n ) as Promise<AuthKey>;\n }\n return publicKeyPromise;\n}\n\nlet privateKeyPromise: Promise<AuthKey> | null = null;\nlet privateKeyCache: string | null = null;\nexport function loadPrivateKey(privateJwk: string): Promise<AuthKey> {\n if (!privateKeyPromise || privateKeyCache !== privateJwk) {\n privateKeyCache = privateJwk;\n privateKeyPromise = importJWK(\n decodeJwk(privateJwk),\n JWT_ALG,\n ) as Promise<AuthKey>;\n }\n return privateKeyPromise;\n}\n\n/** Parse the `OC_AUTH_PUBLIC_JWK` env var back into a JWK object. */\nexport function parsePublicJwk(publicJwk: string): Record<string, unknown> {\n return decodeJwk(publicJwk);\n}\n\n// ─── Sign / verify ──────────────────────────────────────────────────────\n\n/**\n * Mint a fresh oc_session JWT. Only called on the auth host (the only\n * place that holds the private key).\n *\n * The caller owns recording an ended token's `jti` for the revocation\n * feed that {@link createRevocationCheck} reads.\n */\nexport async function signSession(\n claims: {\n sub: string;\n did_oc: string;\n jti: string;\n name?: string | null;\n npub?: string | null;\n home_federation?: string | null;\n signing_method?: \"fedimint_threshold\" | \"fedimint_client\" | \"bip322\" | null;\n merged_from?: string[];\n step_up_at?: number;\n sudo_at?: number;\n display_identity?: DisplayIdentity | null;\n /** Bind the token to one relying party (e.g. `https://example.com`).\n * Omit for a family session. A bound token verifies only where that\n * audience is expected — never as a family sign-in. */\n aud?: string;\n },\n cfg: SignConfig,\n ttlSeconds: number,\n): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n // did_oc is the sole user identifier. The legacy `addr` shim that\n // mirrored did_oc was dropped in v2.0.0 along with the type field.\n const { aud, ...rest } = claims;\n const jwt = new SignJWT(rest)\n .setProtectedHeader({ alg: JWT_ALG, typ: \"JWT\", kid: cfg.kid })\n .setIssuer(cfg.issuer ?? DEFAULT_ISSUER)\n .setIssuedAt(now)\n .setExpirationTime(now + ttlSeconds);\n if (aud) jwt.setAudience(aud);\n return jwt.sign(await loadPrivateKey(cfg.privateJwk));\n}\n\n/**\n * Crypto-only JWT verify. Returns the payload on success, `null` on\n * any failure (bad signature, expired, wrong issuer, malformed).\n *\n * Safe for consumer subdomains — they only need the public JWK. A signed-out\n * token still verifies here; pass the payload to\n * {@link familyRevocationCheck} to refuse it.\n */\nexport async function verifySessionToken(\n token: string,\n cfg: VerifyConfig,\n): Promise<SessionPayload | null> {\n try {\n const res = await jwtVerify(token, await loadPublicKey(cfg.publicJwk), {\n algorithms: [JWT_ALG],\n issuer: cfg.issuer ?? DEFAULT_ISSUER,\n });\n const p = res.payload as SessionPayload;\n if (!p.sub || !p.did_oc || !p.jti) return null;\n if (!audienceSatisfied(p.aud, cfg.audience)) return null;\n return p;\n } catch {\n return null;\n }\n}\n\n// ─── Cookie helpers ─────────────────────────────────────────────────────\n\nexport interface CookieOptions {\n domain?: string;\n maxAge?: number;\n expires?: Date;\n path?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: \"Lax\" | \"Strict\" | \"None\";\n}\n\n/**\n * Serialize a cookie header value for `Set-Cookie`. Defaults match the\n * ochk.io session shape: HttpOnly, SameSite=Lax, Path=/, Secure in prod.\n */\nexport function serializeSessionCookie(\n token: string,\n opts: CookieOptions = {},\n): string {\n const parts: string[] = [`${SESSION_COOKIE}=${token}`];\n parts.push(`Path=${opts.path ?? \"/\"}`);\n if (opts.httpOnly ?? true) parts.push(\"HttpOnly\");\n parts.push(`SameSite=${opts.sameSite ?? \"Lax\"}`);\n if (opts.secure ?? true) parts.push(\"Secure\");\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n if (opts.maxAge != null) parts.push(`Max-Age=${opts.maxAge}`);\n if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);\n return parts.join(\"; \");\n}\n\n/** Clear the session cookie. Must match the Domain used when it was set. */\nexport function clearSessionCookieHeader(\n opts: Pick<CookieOptions, \"domain\" | \"path\" | \"secure\"> = {},\n): string {\n return serializeSessionCookie(\"\", {\n ...opts,\n maxAge: 0,\n expires: new Date(0),\n });\n}\n\n/**\n * Read the oc_session token out of a raw `Cookie:` header string.\n * Returns `null` if the cookie is missing or empty.\n */\nexport function readSessionCookie(\n cookieHeader: string | null | undefined,\n): string | null {\n if (!cookieHeader) return null;\n const prefix = `${SESSION_COOKIE}=`;\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim();\n if (trimmed.startsWith(prefix)) {\n const val = trimmed.slice(prefix.length);\n return val.length > 0 ? val : null;\n }\n }\n return null;\n}\n\n/**\n * Read EVERY oc_session value out of a raw `Cookie:` header string.\n * Multiple same-name cookies are legitimate (e.g. a stale host-scoped\n * cookie shadowing the `Domain=.ochk.io` one) — verification should\n * try each rather than trust ordering.\n */\nexport function readAllSessionCookies(\n cookieHeader: string | null | undefined,\n): string[] {\n if (!cookieHeader) return [];\n const prefix = `${SESSION_COOKIE}=`;\n const out: string[] = [];\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim();\n if (trimmed.startsWith(prefix)) {\n const val = trimmed.slice(prefix.length);\n if (val.length > 0) out.push(val);\n }\n }\n return out;\n}\n\n// ─── Per-tab session resolution (multi-account · tab pinning) ───────────\n//\n// One browser holds ONE shared `oc_session` cookie (the browser-wide\n// default account) but may be signed into N roster accounts, each with\n// its own concurrently-valid session JWT. A tab \"pins\" itself to one\n// account by holding that account's JWT in sessionStorage and sending\n// it on every API call as the `x-oc-tab-session` header. The header\n// value is a full session JWT — a *credential*, never a bare selector —\n// so servers verify it exactly like the cookie token and stay\n// stateless.\n\n/** Header carrying a tab-pinned session JWT. Lowercase (Node folds headers). */\nexport const TAB_SESSION_HEADER = \"x-oc-tab-session\" as const;\n\nexport type ResolveSessionResult =\n | { ok: true; payload: SessionPayload; via: \"tab\" | \"cookie\" }\n | { ok: false; reason: \"tab_invalid\" | \"no_session\" };\n\n/** Header bag shapes accepted by {@link resolveSessionFromRequest}. */\nexport type IncomingRequestHeaders =\n Headers | Record<string, string | string[] | undefined>;\n\nfunction headerValue(\n headers: IncomingRequestHeaders,\n name: string,\n): string | null {\n if (isHeaders(headers)) return headers.get(name);\n const raw = headers[name] ?? headers[name.toLowerCase()];\n if (Array.isArray(raw)) return raw[0] ?? null;\n return typeof raw === \"string\" ? raw : null;\n}\n\n/**\n * Resolve the EFFECTIVE session for a request — the per-tab choke\n * point every consumer's `readJwtSession` should delegate to.\n *\n * Precedence:\n * 1. `x-oc-tab-session` header, when present. **Fail-closed**: a\n * present-but-invalid tab token resolves to\n * `{ ok: false, reason: 'tab_invalid' }` rather than silently\n * falling back to the cookie — falling back would execute the\n * request as a DIFFERENT account than the tab is displaying,\n * which is precisely the bug per-tab pinning exists to prevent.\n * Clients clear their pin on 401 and re-resolve.\n * 2. Every `oc_session` cookie in the jar, first one that verifies.\n *\n * Crypto-only (signature + exp + iss via {@link verifySessionToken}); a\n * signed-out token still resolves, so pass the payload to\n * {@link familyRevocationCheck}. Never throws.\n */\nexport async function resolveSessionFromRequest(\n headers: IncomingRequestHeaders,\n cfg: VerifyConfig,\n): Promise<ResolveSessionResult> {\n const tabToken = headerValue(headers, TAB_SESSION_HEADER);\n if (tabToken) {\n const payload = await verifySessionToken(tabToken, cfg);\n if (payload) return { ok: true, payload, via: \"tab\" };\n return { ok: false, reason: \"tab_invalid\" };\n }\n const cookieHeader = headerValue(headers, \"cookie\");\n for (const token of readAllSessionCookies(cookieHeader)) {\n const payload = await verifySessionToken(token, cfg);\n if (payload) return { ok: true, payload, via: \"cookie\" };\n }\n return { ok: false, reason: \"no_session\" };\n}\n\n// ─── JWKS-aware verification (zero env vars · zero JWK handling) ─────────\n//\n// verifyOcToken / getOcSession are the integrator-facing verification\n// primitives. They lazy-fetch the auth host's published JWKS at\n// `<issuer>/.well-known/jwks.json` and cache it in-process. Integrators\n// install @orangecheck/me-client (which re-exports these), call\n// `verifyOcToken(token)` or `getOcSession(headers)`, and never see a\n// JWK string, an env var, or a base64 dance.\n\nconst DEFAULT_JWKS_TTL_MS = 60 * 60 * 1000; // 1 hour\n/** How old a key list may be and still stand in for one that can't be fetched. */\nconst DEFAULT_JWKS_MAX_STALE_MS = 6 * 60 * 60 * 1000; // 6 hours\n\ninterface JwksCacheEntry {\n keys: Record<string, AuthKey>;\n /** kid → unix seconds, for keys published as `status: \"retired\"`. */\n retiredAt: Record<string, number>;\n fetchedAt: number;\n}\n\nconst jwksCache = new Map<string, JwksCacheEntry>();\nconst inflight = new Map<string, Promise<JwksCacheEntry>>();\n\n/**\n * When each issuer last had its cache force-refreshed by an unrecognised\n * `kid`, and how long that path must wait before doing it again.\n *\n * Without this, a token carrying any unknown kid makes the verifier drop its\n * cache and fetch the JWKS again — so a stream of tokens with random kids turns\n * every integrator's server into a refetch amplifier pointed at the auth host,\n * and puts a network round-trip in front of each rejection. `inflight` only\n * collapses CONCURRENT fetches; sequential requests each got their own.\n *\n * A minute is far below any real rotation window, so a genuinely new key is\n * still picked up promptly — the bust exists for rotation, not for every\n * malformed token.\n */\nconst lastForcedRefresh = new Map<string, number>();\nconst FORCED_REFRESH_MIN_INTERVAL_MS = 60_000;\n\ninterface JwksJson {\n keys: Array<Record<string, unknown> & { kid?: string }>;\n}\n\nasync function fetchJwks(\n issuer: string,\n ttlMs: number,\n maxStaleMs: number = DEFAULT_JWKS_MAX_STALE_MS,\n): Promise<JwksCacheEntry> {\n const cached = jwksCache.get(issuer);\n if (cached && Date.now() - cached.fetchedAt < ttlMs) return cached;\n const existing = inflight.get(issuer);\n if (existing) return existing;\n const p = (async (): Promise<JwksCacheEntry> => {\n try {\n const url = `${issuer.replace(/\\/$/, \"\")}/.well-known/jwks.json`;\n const res = await fetch(url, { headers: { Accept: \"application/json\" } });\n if (!res.ok) {\n throw new Error(\n `[@orangecheck/auth-core] JWKS fetch ${url} returned ${res.status}`,\n );\n }\n const json = (await res.json()) as JwksJson;\n if (!json || !Array.isArray(json.keys)) {\n throw new Error(\n `[@orangecheck/auth-core] JWKS at ${url} did not return {keys:[…]}`,\n );\n }\n const keys: Record<string, AuthKey> = {};\n const retiredAt: Record<string, number> = {};\n for (const k of json.keys) {\n if (typeof k.kid !== \"string\") continue;\n try {\n keys[k.kid] = (await importJWK(k, JWT_ALG)) as AuthKey;\n } catch {\n /* skip keys that won't import for our alg */\n continue;\n }\n if (k.status === \"retired\") {\n // A retired key with no readable retirement time signs nothing.\n retiredAt[k.kid] =\n typeof k.retired_at === \"number\" ? k.retired_at : 0;\n }\n }\n const entry: JwksCacheEntry = { keys, retiredAt, fetchedAt: Date.now() };\n jwksCache.set(issuer, entry);\n return entry;\n } catch (err) {\n // Stale-on-error: a transient JWKS outage shouldn't lock out\n // every consumer, so an expired entry keeps answering — but only\n // until it is `maxStaleMs` old. A key the auth host removed must\n // stop verifying even while its key list can't be fetched.\n const stale = jwksCache.get(issuer);\n if (stale && Date.now() - stale.fetchedAt < maxStaleMs) return stale;\n throw err;\n }\n })().finally(() => {\n inflight.delete(issuer);\n });\n inflight.set(issuer, p);\n return p;\n}\n\nfunction assertSafeIssuer(issuer: string): void {\n let url: URL;\n try {\n url = new URL(issuer);\n } catch {\n throw new Error(\n `[@orangecheck/auth-core] issuer must be an absolute URL, got ${issuer}`,\n );\n }\n // Allow http only for loopback / local dev. Any production-shaped\n // hostname (real DNS) is required to be https — JWKS fetched over\n // plaintext HTTP is trivially MITM-able and would defeat the whole\n // verification stack.\n const isLoopback =\n url.hostname === \"localhost\" ||\n url.hostname === \"127.0.0.1\" ||\n url.hostname === \"::1\";\n if (url.protocol !== \"https:\" && !isLoopback) {\n throw new Error(\n `[@orangecheck/auth-core] issuer must be https (got ${url.protocol}//${url.hostname}); plaintext JWKS is not safe`,\n );\n }\n}\n\nexport interface VerifyOcOptions {\n /** Auth host issuer. Defaults to https://ochk.io. */\n issuer?: string;\n /** JWKS cache TTL in ms. Defaults to 1 hour. Stale-on-error: if the\n * cache exists, verification still works during a transient outage. */\n jwksCacheTtlMs?: number;\n /** How old a cached JWKS may be and still stand in during an outage.\n * Defaults to 6 hours; past it, verification fails closed. */\n jwksMaxStaleMs?: number;\n /** Accepted `aud` · see AudienceRule. An integrator verifying tokens from\n * the sign-in popup sets this to its own origin, e.g.\n * `\"https://example.com\"`, so a token issued to another site is refused. */\n audience?: AudienceRule;\n}\n\n/**\n * Verify a JWT issued by an OC auth host. Lazy-fetches the JWKS from\n * `<issuer>/.well-known/jwks.json`, picks the key whose `kid` matches\n * the token's protected header, and verifies the signature.\n *\n * Returns the payload on success, `null` on any failure (bad signature,\n * expired, wrong issuer, kid not in JWKS, malformed). Never throws.\n *\n * Integrators don't need to handle JWKs, env vars, or rotation — the\n * cache picks up new keys automatically when they appear in the JWKS\n * response. A key published with `status: \"retired\"` verifies only tokens\n * issued before its `retired_at`; a key removed from the JWKS stops\n * verifying once the cache refreshes, and an unreachable JWKS is stood in\n * for by the last one fetched for at most `jwksMaxStaleMs`.\n */\nexport async function verifyOcToken(\n token: string,\n options: VerifyOcOptions = {},\n): Promise<SessionPayload | null> {\n const issuer = options.issuer ?? DEFAULT_ISSUER;\n const ttl = options.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;\n const maxStale = options.jwksMaxStaleMs ?? DEFAULT_JWKS_MAX_STALE_MS;\n try {\n assertSafeIssuer(issuer);\n // Fast path: try the cache. If the kid isn't in the cache (e.g.\n // brand-new key just rotated in), fall through to a fresh fetch.\n let entry = await fetchJwks(issuer, ttl, maxStale);\n const headerB64 = token.split(\".\")[0];\n if (!headerB64) return null;\n const headerJson =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(headerB64, \"base64url\").toString(\"utf8\")\n : new TextDecoder().decode(base64UrlDecode(headerB64));\n const header = JSON.parse(headerJson) as { kid?: string; alg?: string };\n if (!header.kid) return null;\n let key = entry.keys[header.kid];\n if (!key) {\n // Force a fresh JWKS fetch in case the integrator's cache is older\n // than the active key set on the auth host — but at most once a\n // minute per issuer, so an unknown kid cannot be used to drive\n // unbounded refetches (see lastForcedRefresh).\n const now = Date.now();\n const last = lastForcedRefresh.get(issuer) ?? 0;\n if (now - last >= FORCED_REFRESH_MIN_INTERVAL_MS) {\n lastForcedRefresh.set(issuer, now);\n // TTL 0 refetches while keeping the cached list as the stand-in.\n entry = await fetchJwks(issuer, 0, maxStale);\n key = entry.keys[header.kid];\n }\n }\n if (!key) return null;\n const res = await jwtVerify(token, key, {\n algorithms: [JWT_ALG],\n issuer,\n });\n const p = res.payload as SessionPayload;\n if (!p.sub || !p.did_oc || !p.jti) return null;\n // A retired key vouches only for what it signed before it retired.\n const retiredAt = entry.retiredAt[header.kid];\n if (retiredAt !== undefined && !(typeof p.iat === \"number\" && p.iat < retiredAt)) {\n return null;\n }\n if (!audienceSatisfied(p.aud, options.audience)) return null;\n return p;\n } catch {\n return null;\n }\n}\n\nexport interface SessionRequestHeaders {\n cookie?: string | null;\n authorization?: string | null;\n}\n\n/**\n * Verify the OC session for a request. Accepts either a plain object\n * with `cookie` / `authorization` properties (Express / Next.js / etc.)\n * or a Web Headers object (Hono / edge / Fetch API). Reads the session\n * from the Cookie header first; falls back to a `Authorization: Bearer\n * <token>` header so cross-domain integrators (different eTLD+1, no\n * .ochk.io cookie) can verify the same way.\n *\n * Returns `null` for unauthenticated, missing, or invalid requests.\n * Never throws.\n */\nexport async function getOcSession(\n headers: SessionRequestHeaders | Headers,\n options: VerifyOcOptions = {},\n): Promise<SessionPayload | null> {\n const cookie = isHeaders(headers)\n ? headers.get(\"cookie\")\n : (headers.cookie ?? null);\n const authorization = isHeaders(headers)\n ? headers.get(\"authorization\")\n : (headers.authorization ?? null);\n let token = readSessionCookie(cookie);\n if (!token && typeof authorization === \"string\") {\n const m = /^Bearer\\s+(.+)$/i.exec(authorization);\n if (m) token = m[1]!;\n }\n if (!token) return null;\n return verifyOcToken(token, options);\n}\n\nfunction isHeaders(value: unknown): value is Headers {\n return (\n typeof Headers !== \"undefined\" &&\n value !== null &&\n typeof value === \"object\" &&\n value instanceof Headers\n );\n}\n\n/**\n * Clear the module-level JWKS caches. Test-only — the caches are process-wide\n * by design (that is the point of them), which makes any test that touches\n * verification order-dependent without a reset.\n */\nexport function _resetJwksCachesForTests(): void {\n jwksCache.clear();\n inflight.clear();\n lastForcedRefresh.clear();\n}\n\nexport * from \"./revocation\";\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangecheck/auth-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "OrangeCheck session JWT primitives. Verifies oc_session tokens against the auth host's published JWKS — no env vars, no JWK handling. Re-exported by @orangecheck/me-client for integrators; also used by family consumer subdomains directly.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"orangecheck",
|