@orangecheck/auth-core 2.8.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -5
- package/dist/index.d.mts +28 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +139 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +136 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@orangecheck/auth-core`
|
|
2
2
|
|
|
3
|
-
> **Full reference:** [
|
|
3
|
+
> **Full reference:** [ochk.io/docs/sdk/auth-core](https://ochk.io/docs/sdk/auth-core) — auto-generated from the TypeScript source on every release.
|
|
4
4
|
> Hand-written prose below is the high-level overview; the docs site is the source of truth for every export, type, and signature.
|
|
5
5
|
|
|
6
6
|
|
|
@@ -18,7 +18,7 @@ No React, no DB, no framework. Usable from Node, Edge, or browser.
|
|
|
18
18
|
|
|
19
19
|
There is **one auth host** (by convention `https://ochk.io`) that holds the
|
|
20
20
|
private JWK and issues tokens. Every **consumer subdomain** (`attest.ochk.io`,
|
|
21
|
-
`lock.ochk.io`, `
|
|
21
|
+
`lock.ochk.io`, `stamp.ochk.io`, …) only holds the public JWK and verifies
|
|
22
22
|
locally — no network round-trip.
|
|
23
23
|
|
|
24
24
|
The session cookie is set with `Domain=.ochk.io`, so once a user signs in on
|
|
@@ -80,15 +80,28 @@ if (!payload) return res.status(401).end();
|
|
|
80
80
|
// payload.did_oc is the opaque user identifier (did:oc:<32-hex>).
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
A signed-out token still verifies. Family sites also ask the auth host's list
|
|
84
|
+
of ended sessions, cached for a minute per process:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { familyRevocationCheck } from '@orangecheck/auth-core';
|
|
88
|
+
|
|
89
|
+
// Reads OC_REVOCATION_FEED_TOKEN. Refuses a revoked session, and refuses
|
|
90
|
+
// writes (not reads) while the list cannot be refreshed for 10 minutes.
|
|
91
|
+
if (await familyRevocationCheck().refuses(payload, req.method)) {
|
|
92
|
+
return res.status(401).end();
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
83
96
|
## Guarantees
|
|
84
97
|
|
|
85
98
|
- **No shared secret.** Consumer apps cannot forge sessions — only the auth
|
|
86
99
|
host can, because only it holds the private key.
|
|
87
100
|
- **Offline-verifiable.** Given the public JWK, any process can verify a
|
|
88
101
|
token — no database, no auth server round-trip.
|
|
89
|
-
- **
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
- **No call per request.** Verification is crypto only. The revocation
|
|
103
|
+
check fetches the auth host's list at most once per minute per process
|
|
104
|
+
and answers from memory.
|
|
92
105
|
|
|
93
106
|
## License
|
|
94
107
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { CryptoKey, KeyObject, JWTPayload } from 'jose';
|
|
2
2
|
|
|
3
|
+
interface RevocableClaims {
|
|
4
|
+
jti?: unknown;
|
|
5
|
+
sub?: unknown;
|
|
6
|
+
iat?: unknown;
|
|
7
|
+
}
|
|
8
|
+
type RevocationStatus = "live" | "revoked" | "unverified";
|
|
9
|
+
interface RevocationCheck {
|
|
10
|
+
status(claims: RevocableClaims): Promise<RevocationStatus>;
|
|
11
|
+
refuses(claims: RevocableClaims, method?: string): Promise<boolean>;
|
|
12
|
+
}
|
|
13
|
+
interface RevocationCheckOptions {
|
|
14
|
+
url?: string;
|
|
15
|
+
token?: string;
|
|
16
|
+
ttlMs?: number;
|
|
17
|
+
maxStaleMs?: number;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
retryMs?: number;
|
|
20
|
+
developing?: boolean;
|
|
21
|
+
fetch?: typeof fetch;
|
|
22
|
+
now?: () => number;
|
|
23
|
+
}
|
|
24
|
+
declare function revocationKey(kind: "jti" | "sub", value: string): Promise<string>;
|
|
25
|
+
declare function createRevocationCheck(opts?: RevocationCheckOptions): RevocationCheck;
|
|
26
|
+
declare function familyRevocationCheck(): RevocationCheck;
|
|
27
|
+
declare function _resetFamilyRevocationCheckForTests(): void;
|
|
28
|
+
|
|
3
29
|
declare const JWT_ALG: "EdDSA";
|
|
4
30
|
declare const SESSION_COOKIE: "oc_session";
|
|
5
31
|
declare const DEFAULT_ISSUER: "https://ochk.io";
|
|
@@ -102,6 +128,7 @@ declare function resolveSessionFromRequest(headers: IncomingRequestHeaders, cfg:
|
|
|
102
128
|
interface VerifyOcOptions {
|
|
103
129
|
issuer?: string;
|
|
104
130
|
jwksCacheTtlMs?: number;
|
|
131
|
+
jwksMaxStaleMs?: number;
|
|
105
132
|
audience?: AudienceRule;
|
|
106
133
|
}
|
|
107
134
|
declare function verifyOcToken(token: string, options?: VerifyOcOptions): Promise<SessionPayload | null>;
|
|
@@ -112,4 +139,4 @@ interface SessionRequestHeaders {
|
|
|
112
139
|
declare function getOcSession(headers: SessionRequestHeaders | Headers, options?: VerifyOcOptions): Promise<SessionPayload | null>;
|
|
113
140
|
declare function _resetJwksCachesForTests(): void;
|
|
114
141
|
|
|
115
|
-
export { type AudienceRule, type AuthKey, type BondClaim, type CookieOptions, DEFAULT_ISSUER, DISPLAY_IDENTITY_KINDS, type DisplayIdentity, type DisplayIdentityKind, type IncomingRequestHeaders, JWT_ALG, type ResolveSessionResult, SESSION_COOKIE, type SessionPayload, type SessionRequestHeaders, type SignConfig, TAB_SESSION_HEADER, type VerifyConfig, type VerifyOcOptions, _resetJwksCachesForTests, audienceSatisfied, bondMeets, clearSessionCookieHeader, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readBondClaim, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
|
142
|
+
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { CryptoKey, KeyObject, JWTPayload } from 'jose';
|
|
2
2
|
|
|
3
|
+
interface RevocableClaims {
|
|
4
|
+
jti?: unknown;
|
|
5
|
+
sub?: unknown;
|
|
6
|
+
iat?: unknown;
|
|
7
|
+
}
|
|
8
|
+
type RevocationStatus = "live" | "revoked" | "unverified";
|
|
9
|
+
interface RevocationCheck {
|
|
10
|
+
status(claims: RevocableClaims): Promise<RevocationStatus>;
|
|
11
|
+
refuses(claims: RevocableClaims, method?: string): Promise<boolean>;
|
|
12
|
+
}
|
|
13
|
+
interface RevocationCheckOptions {
|
|
14
|
+
url?: string;
|
|
15
|
+
token?: string;
|
|
16
|
+
ttlMs?: number;
|
|
17
|
+
maxStaleMs?: number;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
retryMs?: number;
|
|
20
|
+
developing?: boolean;
|
|
21
|
+
fetch?: typeof fetch;
|
|
22
|
+
now?: () => number;
|
|
23
|
+
}
|
|
24
|
+
declare function revocationKey(kind: "jti" | "sub", value: string): Promise<string>;
|
|
25
|
+
declare function createRevocationCheck(opts?: RevocationCheckOptions): RevocationCheck;
|
|
26
|
+
declare function familyRevocationCheck(): RevocationCheck;
|
|
27
|
+
declare function _resetFamilyRevocationCheckForTests(): void;
|
|
28
|
+
|
|
3
29
|
declare const JWT_ALG: "EdDSA";
|
|
4
30
|
declare const SESSION_COOKIE: "oc_session";
|
|
5
31
|
declare const DEFAULT_ISSUER: "https://ochk.io";
|
|
@@ -102,6 +128,7 @@ declare function resolveSessionFromRequest(headers: IncomingRequestHeaders, cfg:
|
|
|
102
128
|
interface VerifyOcOptions {
|
|
103
129
|
issuer?: string;
|
|
104
130
|
jwksCacheTtlMs?: number;
|
|
131
|
+
jwksMaxStaleMs?: number;
|
|
105
132
|
audience?: AudienceRule;
|
|
106
133
|
}
|
|
107
134
|
declare function verifyOcToken(token: string, options?: VerifyOcOptions): Promise<SessionPayload | null>;
|
|
@@ -112,4 +139,4 @@ interface SessionRequestHeaders {
|
|
|
112
139
|
declare function getOcSession(headers: SessionRequestHeaders | Headers, options?: VerifyOcOptions): Promise<SessionPayload | null>;
|
|
113
140
|
declare function _resetJwksCachesForTests(): void;
|
|
114
141
|
|
|
115
|
-
export { type AudienceRule, type AuthKey, type BondClaim, type CookieOptions, DEFAULT_ISSUER, DISPLAY_IDENTITY_KINDS, type DisplayIdentity, type DisplayIdentityKind, type IncomingRequestHeaders, JWT_ALG, type ResolveSessionResult, SESSION_COOKIE, type SessionPayload, type SessionRequestHeaders, type SignConfig, TAB_SESSION_HEADER, type VerifyConfig, type VerifyOcOptions, _resetJwksCachesForTests, audienceSatisfied, bondMeets, clearSessionCookieHeader, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readBondClaim, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
|
142
|
+
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 };
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,125 @@
|
|
|
2
2
|
|
|
3
3
|
var jose = require('jose');
|
|
4
4
|
|
|
5
|
+
// src/index.ts
|
|
6
|
+
|
|
7
|
+
// src/revocation.ts
|
|
8
|
+
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
9
|
+
var DEFAULT_TTL_MS = 6e4;
|
|
10
|
+
var DEFAULT_MAX_STALE_MS = 10 * 6e4;
|
|
11
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
12
|
+
var DEFAULT_RETRY_MS = 1e4;
|
|
13
|
+
function env(name) {
|
|
14
|
+
return typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
15
|
+
}
|
|
16
|
+
async function revocationKey(kind, value) {
|
|
17
|
+
const digest = await globalThis.crypto.subtle.digest(
|
|
18
|
+
"SHA-256",
|
|
19
|
+
new TextEncoder().encode(`ocrev1:${kind}:${value}`)
|
|
20
|
+
);
|
|
21
|
+
let bin = "";
|
|
22
|
+
for (const b of new Uint8Array(digest).subarray(0, 16)) bin += String.fromCharCode(b);
|
|
23
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
24
|
+
}
|
|
25
|
+
function parseFeed(body, fetchedAt) {
|
|
26
|
+
const b = body;
|
|
27
|
+
if (!b || b.v !== 1 || !Array.isArray(b.tokens) || typeof b.accounts !== "object" || !b.accounts) {
|
|
28
|
+
throw new Error("revocation feed: unexpected body");
|
|
29
|
+
}
|
|
30
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
31
|
+
for (const t of b.tokens) if (typeof t === "string") tokens.add(t);
|
|
32
|
+
const accounts = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const [k, v] of Object.entries(b.accounts)) {
|
|
34
|
+
if (typeof v === "number" && Number.isFinite(v)) accounts.set(k, v);
|
|
35
|
+
}
|
|
36
|
+
return { tokens, accounts, fetchedAt };
|
|
37
|
+
}
|
|
38
|
+
function createRevocationCheck(opts = {}) {
|
|
39
|
+
const issuer = (env("OC_AUTH_ISSUER") ?? "https://ochk.io").replace(/\/$/, "");
|
|
40
|
+
const url = opts.url ?? env("OC_REVOCATION_FEED_URL") ?? `${issuer}/api/auth/revocations`;
|
|
41
|
+
const token = opts.token ?? env("OC_REVOCATION_FEED_TOKEN")?.trim();
|
|
42
|
+
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
43
|
+
const maxStaleMs = opts.maxStaleMs ?? DEFAULT_MAX_STALE_MS;
|
|
44
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
45
|
+
const retryMs = opts.retryMs ?? DEFAULT_RETRY_MS;
|
|
46
|
+
const developing = opts.developing ?? env("NODE_ENV") !== "production";
|
|
47
|
+
const doFetch = opts.fetch ?? ((...a) => fetch(...a));
|
|
48
|
+
const now = opts.now ?? Date.now;
|
|
49
|
+
let snapshot = null;
|
|
50
|
+
let inflight2 = null;
|
|
51
|
+
let failedAt = -Infinity;
|
|
52
|
+
let warned = false;
|
|
53
|
+
const warn = (msg) => {
|
|
54
|
+
if (warned) return;
|
|
55
|
+
warned = true;
|
|
56
|
+
console.warn(`[@orangecheck/auth-core] ${msg}`);
|
|
57
|
+
};
|
|
58
|
+
async function load() {
|
|
59
|
+
const ctl = new AbortController();
|
|
60
|
+
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
61
|
+
try {
|
|
62
|
+
const res = await doFetch(url, {
|
|
63
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/json" },
|
|
64
|
+
signal: ctl.signal
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) throw new Error(`revocation feed answered ${res.status}`);
|
|
67
|
+
snapshot = parseFeed(await res.json(), now());
|
|
68
|
+
warned = false;
|
|
69
|
+
} finally {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function current() {
|
|
74
|
+
const t = now();
|
|
75
|
+
if (snapshot && t - snapshot.fetchedAt < ttlMs) return snapshot;
|
|
76
|
+
if (t - failedAt < retryMs) return snapshot;
|
|
77
|
+
inflight2 ?? (inflight2 = load().catch((err) => {
|
|
78
|
+
failedAt = now();
|
|
79
|
+
warn(`${err instanceof Error ? err.message : String(err)}; using the last copy`);
|
|
80
|
+
}).finally(() => {
|
|
81
|
+
inflight2 = null;
|
|
82
|
+
}));
|
|
83
|
+
await inflight2;
|
|
84
|
+
return snapshot;
|
|
85
|
+
}
|
|
86
|
+
async function status(claims) {
|
|
87
|
+
if (!token) {
|
|
88
|
+
if (developing) return "live";
|
|
89
|
+
warn("OC_REVOCATION_FEED_TOKEN is not set; sessions cannot be checked for revocation");
|
|
90
|
+
return "unverified";
|
|
91
|
+
}
|
|
92
|
+
const snap = await current();
|
|
93
|
+
if (!snap) return "unverified";
|
|
94
|
+
if (typeof claims.jti === "string" && snap.tokens.has(await revocationKey("jti", claims.jti))) {
|
|
95
|
+
return "revoked";
|
|
96
|
+
}
|
|
97
|
+
if (typeof claims.sub === "string" && snap.accounts.size > 0) {
|
|
98
|
+
const cutoff = snap.accounts.get(await revocationKey("sub", claims.sub));
|
|
99
|
+
if (cutoff !== void 0 && !(typeof claims.iat === "number" && claims.iat >= cutoff)) {
|
|
100
|
+
return "revoked";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return now() - snap.fetchedAt > maxStaleMs ? "unverified" : "live";
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
status,
|
|
107
|
+
async refuses(claims, method) {
|
|
108
|
+
const s = await status(claims);
|
|
109
|
+
if (s === "revoked") return true;
|
|
110
|
+
if (s === "live") return false;
|
|
111
|
+
return !SAFE_METHODS.has((method ?? "").toUpperCase());
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
var SHARED = /* @__PURE__ */ Symbol.for("@orangecheck/auth-core/revocation-check/v1");
|
|
116
|
+
function familyRevocationCheck() {
|
|
117
|
+
const g = globalThis;
|
|
118
|
+
return g[SHARED] ?? (g[SHARED] = createRevocationCheck());
|
|
119
|
+
}
|
|
120
|
+
function _resetFamilyRevocationCheckForTests() {
|
|
121
|
+
delete globalThis[SHARED];
|
|
122
|
+
}
|
|
123
|
+
|
|
5
124
|
// src/index.ts
|
|
6
125
|
var JWT_ALG = "EdDSA";
|
|
7
126
|
var SESSION_COOKIE = "oc_session";
|
|
@@ -185,11 +304,12 @@ async function resolveSessionFromRequest(headers, cfg) {
|
|
|
185
304
|
return { ok: false, reason: "no_session" };
|
|
186
305
|
}
|
|
187
306
|
var DEFAULT_JWKS_TTL_MS = 60 * 60 * 1e3;
|
|
307
|
+
var DEFAULT_JWKS_MAX_STALE_MS = 6 * 60 * 60 * 1e3;
|
|
188
308
|
var jwksCache = /* @__PURE__ */ new Map();
|
|
189
309
|
var inflight = /* @__PURE__ */ new Map();
|
|
190
310
|
var lastForcedRefresh = /* @__PURE__ */ new Map();
|
|
191
311
|
var FORCED_REFRESH_MIN_INTERVAL_MS = 6e4;
|
|
192
|
-
async function fetchJwks(issuer, ttlMs) {
|
|
312
|
+
async function fetchJwks(issuer, ttlMs, maxStaleMs = DEFAULT_JWKS_MAX_STALE_MS) {
|
|
193
313
|
const cached = jwksCache.get(issuer);
|
|
194
314
|
if (cached && Date.now() - cached.fetchedAt < ttlMs) return cached;
|
|
195
315
|
const existing = inflight.get(issuer);
|
|
@@ -210,19 +330,24 @@ async function fetchJwks(issuer, ttlMs) {
|
|
|
210
330
|
);
|
|
211
331
|
}
|
|
212
332
|
const keys = {};
|
|
333
|
+
const retiredAt = {};
|
|
213
334
|
for (const k of json.keys) {
|
|
214
335
|
if (typeof k.kid !== "string") continue;
|
|
215
336
|
try {
|
|
216
337
|
keys[k.kid] = await jose.importJWK(k, JWT_ALG);
|
|
217
338
|
} catch {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (k.status === "retired") {
|
|
342
|
+
retiredAt[k.kid] = typeof k.retired_at === "number" ? k.retired_at : 0;
|
|
218
343
|
}
|
|
219
344
|
}
|
|
220
|
-
const entry = { keys, fetchedAt: Date.now() };
|
|
345
|
+
const entry = { keys, retiredAt, fetchedAt: Date.now() };
|
|
221
346
|
jwksCache.set(issuer, entry);
|
|
222
347
|
return entry;
|
|
223
348
|
} catch (err) {
|
|
224
349
|
const stale = jwksCache.get(issuer);
|
|
225
|
-
if (stale) return stale;
|
|
350
|
+
if (stale && Date.now() - stale.fetchedAt < maxStaleMs) return stale;
|
|
226
351
|
throw err;
|
|
227
352
|
}
|
|
228
353
|
})().finally(() => {
|
|
@@ -250,9 +375,10 @@ function assertSafeIssuer(issuer) {
|
|
|
250
375
|
async function verifyOcToken(token, options = {}) {
|
|
251
376
|
const issuer = options.issuer ?? DEFAULT_ISSUER;
|
|
252
377
|
const ttl = options.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;
|
|
378
|
+
const maxStale = options.jwksMaxStaleMs ?? DEFAULT_JWKS_MAX_STALE_MS;
|
|
253
379
|
try {
|
|
254
380
|
assertSafeIssuer(issuer);
|
|
255
|
-
let entry = await fetchJwks(issuer, ttl);
|
|
381
|
+
let entry = await fetchJwks(issuer, ttl, maxStale);
|
|
256
382
|
const headerB64 = token.split(".")[0];
|
|
257
383
|
if (!headerB64) return null;
|
|
258
384
|
const headerJson = typeof Buffer !== "undefined" ? Buffer.from(headerB64, "base64url").toString("utf8") : new TextDecoder().decode(base64UrlDecode(headerB64));
|
|
@@ -264,8 +390,7 @@ async function verifyOcToken(token, options = {}) {
|
|
|
264
390
|
const last = lastForcedRefresh.get(issuer) ?? 0;
|
|
265
391
|
if (now - last >= FORCED_REFRESH_MIN_INTERVAL_MS) {
|
|
266
392
|
lastForcedRefresh.set(issuer, now);
|
|
267
|
-
|
|
268
|
-
entry = await fetchJwks(issuer, ttl);
|
|
393
|
+
entry = await fetchJwks(issuer, 0, maxStale);
|
|
269
394
|
key = entry.keys[header.kid];
|
|
270
395
|
}
|
|
271
396
|
}
|
|
@@ -276,6 +401,10 @@ async function verifyOcToken(token, options = {}) {
|
|
|
276
401
|
});
|
|
277
402
|
const p = res.payload;
|
|
278
403
|
if (!p.sub || !p.did_oc || !p.jti) return null;
|
|
404
|
+
const retiredAt = entry.retiredAt[header.kid];
|
|
405
|
+
if (retiredAt !== void 0 && !(typeof p.iat === "number" && p.iat < retiredAt)) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
279
408
|
if (!audienceSatisfied(p.aud, options.audience)) return null;
|
|
280
409
|
return p;
|
|
281
410
|
} catch {
|
|
@@ -307,10 +436,13 @@ exports.DISPLAY_IDENTITY_KINDS = DISPLAY_IDENTITY_KINDS;
|
|
|
307
436
|
exports.JWT_ALG = JWT_ALG;
|
|
308
437
|
exports.SESSION_COOKIE = SESSION_COOKIE;
|
|
309
438
|
exports.TAB_SESSION_HEADER = TAB_SESSION_HEADER;
|
|
439
|
+
exports._resetFamilyRevocationCheckForTests = _resetFamilyRevocationCheckForTests;
|
|
310
440
|
exports._resetJwksCachesForTests = _resetJwksCachesForTests;
|
|
311
441
|
exports.audienceSatisfied = audienceSatisfied;
|
|
312
442
|
exports.bondMeets = bondMeets;
|
|
313
443
|
exports.clearSessionCookieHeader = clearSessionCookieHeader;
|
|
444
|
+
exports.createRevocationCheck = createRevocationCheck;
|
|
445
|
+
exports.familyRevocationCheck = familyRevocationCheck;
|
|
314
446
|
exports.getOcSession = getOcSession;
|
|
315
447
|
exports.loadPrivateKey = loadPrivateKey;
|
|
316
448
|
exports.loadPublicKey = loadPublicKey;
|
|
@@ -320,6 +452,7 @@ exports.readBondClaim = readBondClaim;
|
|
|
320
452
|
exports.readSessionCookie = readSessionCookie;
|
|
321
453
|
exports.resolveDisplayIdentity = resolveDisplayIdentity;
|
|
322
454
|
exports.resolveSessionFromRequest = resolveSessionFromRequest;
|
|
455
|
+
exports.revocationKey = revocationKey;
|
|
323
456
|
exports.serializeSessionCookie = serializeSessionCookie;
|
|
324
457
|
exports.signSession = signSession;
|
|
325
458
|
exports.verifyOcToken = verifyOcToken;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["importJWK","SignJWT","jwtVerify"],"mappings":"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","file":"index.js","sourcesContent":["/**\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 /** Same for the same coin at your site, unrelated anywhere else. Ban it to ban the coin. */\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 worth at least `minSats`,\n * unspent at least `minDays` days, 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 persisting `jti` to a revocation list if they want\n * revocation semantics — auth-core is deliberately stateless.\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.\n * Revocation-aware checks live on the auth host.\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});\n * revocation-aware checks remain the auth host's job. 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"]}
|
|
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 * @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
|
@@ -1,5 +1,124 @@
|
|
|
1
1
|
import { importJWK, SignJWT, jwtVerify } from 'jose';
|
|
2
2
|
|
|
3
|
+
// src/index.ts
|
|
4
|
+
|
|
5
|
+
// src/revocation.ts
|
|
6
|
+
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
7
|
+
var DEFAULT_TTL_MS = 6e4;
|
|
8
|
+
var DEFAULT_MAX_STALE_MS = 10 * 6e4;
|
|
9
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
10
|
+
var DEFAULT_RETRY_MS = 1e4;
|
|
11
|
+
function env(name) {
|
|
12
|
+
return typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
13
|
+
}
|
|
14
|
+
async function revocationKey(kind, value) {
|
|
15
|
+
const digest = await globalThis.crypto.subtle.digest(
|
|
16
|
+
"SHA-256",
|
|
17
|
+
new TextEncoder().encode(`ocrev1:${kind}:${value}`)
|
|
18
|
+
);
|
|
19
|
+
let bin = "";
|
|
20
|
+
for (const b of new Uint8Array(digest).subarray(0, 16)) bin += String.fromCharCode(b);
|
|
21
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
22
|
+
}
|
|
23
|
+
function parseFeed(body, fetchedAt) {
|
|
24
|
+
const b = body;
|
|
25
|
+
if (!b || b.v !== 1 || !Array.isArray(b.tokens) || typeof b.accounts !== "object" || !b.accounts) {
|
|
26
|
+
throw new Error("revocation feed: unexpected body");
|
|
27
|
+
}
|
|
28
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
29
|
+
for (const t of b.tokens) if (typeof t === "string") tokens.add(t);
|
|
30
|
+
const accounts = /* @__PURE__ */ new Map();
|
|
31
|
+
for (const [k, v] of Object.entries(b.accounts)) {
|
|
32
|
+
if (typeof v === "number" && Number.isFinite(v)) accounts.set(k, v);
|
|
33
|
+
}
|
|
34
|
+
return { tokens, accounts, fetchedAt };
|
|
35
|
+
}
|
|
36
|
+
function createRevocationCheck(opts = {}) {
|
|
37
|
+
const issuer = (env("OC_AUTH_ISSUER") ?? "https://ochk.io").replace(/\/$/, "");
|
|
38
|
+
const url = opts.url ?? env("OC_REVOCATION_FEED_URL") ?? `${issuer}/api/auth/revocations`;
|
|
39
|
+
const token = opts.token ?? env("OC_REVOCATION_FEED_TOKEN")?.trim();
|
|
40
|
+
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
41
|
+
const maxStaleMs = opts.maxStaleMs ?? DEFAULT_MAX_STALE_MS;
|
|
42
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
43
|
+
const retryMs = opts.retryMs ?? DEFAULT_RETRY_MS;
|
|
44
|
+
const developing = opts.developing ?? env("NODE_ENV") !== "production";
|
|
45
|
+
const doFetch = opts.fetch ?? ((...a) => fetch(...a));
|
|
46
|
+
const now = opts.now ?? Date.now;
|
|
47
|
+
let snapshot = null;
|
|
48
|
+
let inflight2 = null;
|
|
49
|
+
let failedAt = -Infinity;
|
|
50
|
+
let warned = false;
|
|
51
|
+
const warn = (msg) => {
|
|
52
|
+
if (warned) return;
|
|
53
|
+
warned = true;
|
|
54
|
+
console.warn(`[@orangecheck/auth-core] ${msg}`);
|
|
55
|
+
};
|
|
56
|
+
async function load() {
|
|
57
|
+
const ctl = new AbortController();
|
|
58
|
+
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
59
|
+
try {
|
|
60
|
+
const res = await doFetch(url, {
|
|
61
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/json" },
|
|
62
|
+
signal: ctl.signal
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) throw new Error(`revocation feed answered ${res.status}`);
|
|
65
|
+
snapshot = parseFeed(await res.json(), now());
|
|
66
|
+
warned = false;
|
|
67
|
+
} finally {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function current() {
|
|
72
|
+
const t = now();
|
|
73
|
+
if (snapshot && t - snapshot.fetchedAt < ttlMs) return snapshot;
|
|
74
|
+
if (t - failedAt < retryMs) return snapshot;
|
|
75
|
+
inflight2 ?? (inflight2 = load().catch((err) => {
|
|
76
|
+
failedAt = now();
|
|
77
|
+
warn(`${err instanceof Error ? err.message : String(err)}; using the last copy`);
|
|
78
|
+
}).finally(() => {
|
|
79
|
+
inflight2 = null;
|
|
80
|
+
}));
|
|
81
|
+
await inflight2;
|
|
82
|
+
return snapshot;
|
|
83
|
+
}
|
|
84
|
+
async function status(claims) {
|
|
85
|
+
if (!token) {
|
|
86
|
+
if (developing) return "live";
|
|
87
|
+
warn("OC_REVOCATION_FEED_TOKEN is not set; sessions cannot be checked for revocation");
|
|
88
|
+
return "unverified";
|
|
89
|
+
}
|
|
90
|
+
const snap = await current();
|
|
91
|
+
if (!snap) return "unverified";
|
|
92
|
+
if (typeof claims.jti === "string" && snap.tokens.has(await revocationKey("jti", claims.jti))) {
|
|
93
|
+
return "revoked";
|
|
94
|
+
}
|
|
95
|
+
if (typeof claims.sub === "string" && snap.accounts.size > 0) {
|
|
96
|
+
const cutoff = snap.accounts.get(await revocationKey("sub", claims.sub));
|
|
97
|
+
if (cutoff !== void 0 && !(typeof claims.iat === "number" && claims.iat >= cutoff)) {
|
|
98
|
+
return "revoked";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return now() - snap.fetchedAt > maxStaleMs ? "unverified" : "live";
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
status,
|
|
105
|
+
async refuses(claims, method) {
|
|
106
|
+
const s = await status(claims);
|
|
107
|
+
if (s === "revoked") return true;
|
|
108
|
+
if (s === "live") return false;
|
|
109
|
+
return !SAFE_METHODS.has((method ?? "").toUpperCase());
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
var SHARED = /* @__PURE__ */ Symbol.for("@orangecheck/auth-core/revocation-check/v1");
|
|
114
|
+
function familyRevocationCheck() {
|
|
115
|
+
const g = globalThis;
|
|
116
|
+
return g[SHARED] ?? (g[SHARED] = createRevocationCheck());
|
|
117
|
+
}
|
|
118
|
+
function _resetFamilyRevocationCheckForTests() {
|
|
119
|
+
delete globalThis[SHARED];
|
|
120
|
+
}
|
|
121
|
+
|
|
3
122
|
// src/index.ts
|
|
4
123
|
var JWT_ALG = "EdDSA";
|
|
5
124
|
var SESSION_COOKIE = "oc_session";
|
|
@@ -183,11 +302,12 @@ async function resolveSessionFromRequest(headers, cfg) {
|
|
|
183
302
|
return { ok: false, reason: "no_session" };
|
|
184
303
|
}
|
|
185
304
|
var DEFAULT_JWKS_TTL_MS = 60 * 60 * 1e3;
|
|
305
|
+
var DEFAULT_JWKS_MAX_STALE_MS = 6 * 60 * 60 * 1e3;
|
|
186
306
|
var jwksCache = /* @__PURE__ */ new Map();
|
|
187
307
|
var inflight = /* @__PURE__ */ new Map();
|
|
188
308
|
var lastForcedRefresh = /* @__PURE__ */ new Map();
|
|
189
309
|
var FORCED_REFRESH_MIN_INTERVAL_MS = 6e4;
|
|
190
|
-
async function fetchJwks(issuer, ttlMs) {
|
|
310
|
+
async function fetchJwks(issuer, ttlMs, maxStaleMs = DEFAULT_JWKS_MAX_STALE_MS) {
|
|
191
311
|
const cached = jwksCache.get(issuer);
|
|
192
312
|
if (cached && Date.now() - cached.fetchedAt < ttlMs) return cached;
|
|
193
313
|
const existing = inflight.get(issuer);
|
|
@@ -208,19 +328,24 @@ async function fetchJwks(issuer, ttlMs) {
|
|
|
208
328
|
);
|
|
209
329
|
}
|
|
210
330
|
const keys = {};
|
|
331
|
+
const retiredAt = {};
|
|
211
332
|
for (const k of json.keys) {
|
|
212
333
|
if (typeof k.kid !== "string") continue;
|
|
213
334
|
try {
|
|
214
335
|
keys[k.kid] = await importJWK(k, JWT_ALG);
|
|
215
336
|
} catch {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (k.status === "retired") {
|
|
340
|
+
retiredAt[k.kid] = typeof k.retired_at === "number" ? k.retired_at : 0;
|
|
216
341
|
}
|
|
217
342
|
}
|
|
218
|
-
const entry = { keys, fetchedAt: Date.now() };
|
|
343
|
+
const entry = { keys, retiredAt, fetchedAt: Date.now() };
|
|
219
344
|
jwksCache.set(issuer, entry);
|
|
220
345
|
return entry;
|
|
221
346
|
} catch (err) {
|
|
222
347
|
const stale = jwksCache.get(issuer);
|
|
223
|
-
if (stale) return stale;
|
|
348
|
+
if (stale && Date.now() - stale.fetchedAt < maxStaleMs) return stale;
|
|
224
349
|
throw err;
|
|
225
350
|
}
|
|
226
351
|
})().finally(() => {
|
|
@@ -248,9 +373,10 @@ function assertSafeIssuer(issuer) {
|
|
|
248
373
|
async function verifyOcToken(token, options = {}) {
|
|
249
374
|
const issuer = options.issuer ?? DEFAULT_ISSUER;
|
|
250
375
|
const ttl = options.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;
|
|
376
|
+
const maxStale = options.jwksMaxStaleMs ?? DEFAULT_JWKS_MAX_STALE_MS;
|
|
251
377
|
try {
|
|
252
378
|
assertSafeIssuer(issuer);
|
|
253
|
-
let entry = await fetchJwks(issuer, ttl);
|
|
379
|
+
let entry = await fetchJwks(issuer, ttl, maxStale);
|
|
254
380
|
const headerB64 = token.split(".")[0];
|
|
255
381
|
if (!headerB64) return null;
|
|
256
382
|
const headerJson = typeof Buffer !== "undefined" ? Buffer.from(headerB64, "base64url").toString("utf8") : new TextDecoder().decode(base64UrlDecode(headerB64));
|
|
@@ -262,8 +388,7 @@ async function verifyOcToken(token, options = {}) {
|
|
|
262
388
|
const last = lastForcedRefresh.get(issuer) ?? 0;
|
|
263
389
|
if (now - last >= FORCED_REFRESH_MIN_INTERVAL_MS) {
|
|
264
390
|
lastForcedRefresh.set(issuer, now);
|
|
265
|
-
|
|
266
|
-
entry = await fetchJwks(issuer, ttl);
|
|
391
|
+
entry = await fetchJwks(issuer, 0, maxStale);
|
|
267
392
|
key = entry.keys[header.kid];
|
|
268
393
|
}
|
|
269
394
|
}
|
|
@@ -274,6 +399,10 @@ async function verifyOcToken(token, options = {}) {
|
|
|
274
399
|
});
|
|
275
400
|
const p = res.payload;
|
|
276
401
|
if (!p.sub || !p.did_oc || !p.jti) return null;
|
|
402
|
+
const retiredAt = entry.retiredAt[header.kid];
|
|
403
|
+
if (retiredAt !== void 0 && !(typeof p.iat === "number" && p.iat < retiredAt)) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
277
406
|
if (!audienceSatisfied(p.aud, options.audience)) return null;
|
|
278
407
|
return p;
|
|
279
408
|
} catch {
|
|
@@ -300,6 +429,6 @@ function _resetJwksCachesForTests() {
|
|
|
300
429
|
lastForcedRefresh.clear();
|
|
301
430
|
}
|
|
302
431
|
|
|
303
|
-
export { DEFAULT_ISSUER, DISPLAY_IDENTITY_KINDS, JWT_ALG, SESSION_COOKIE, TAB_SESSION_HEADER, _resetJwksCachesForTests, audienceSatisfied, bondMeets, clearSessionCookieHeader, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readBondClaim, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
|
432
|
+
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 };
|
|
304
433
|
//# sourceMappingURL=index.mjs.map
|
|
305
434
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\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 /** Same for the same coin at your site, unrelated anywhere else. Ban it to ban the coin. */\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 worth at least `minSats`,\n * unspent at least `minDays` days, 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 persisting `jti` to a revocation list if they want\n * revocation semantics — auth-core is deliberately stateless.\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.\n * Revocation-aware checks live on the auth host.\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});\n * revocation-aware checks remain the auth host's job. 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"]}
|
|
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 * @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.10.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",
|