@orangecheck/auth-core 2.8.0 → 2.9.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 +27 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +120 -1
- 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";
|
|
@@ -112,4 +138,4 @@ interface SessionRequestHeaders {
|
|
|
112
138
|
declare function getOcSession(headers: SessionRequestHeaders | Headers, options?: VerifyOcOptions): Promise<SessionPayload | null>;
|
|
113
139
|
declare function _resetJwksCachesForTests(): void;
|
|
114
140
|
|
|
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 };
|
|
141
|
+
export { type AudienceRule, type AuthKey, type BondClaim, type CookieOptions, DEFAULT_ISSUER, DISPLAY_IDENTITY_KINDS, type DisplayIdentity, type DisplayIdentityKind, type IncomingRequestHeaders, JWT_ALG, type ResolveSessionResult, type RevocableClaims, type RevocationCheck, type RevocationCheckOptions, type RevocationStatus, SESSION_COOKIE, type SessionPayload, type SessionRequestHeaders, type SignConfig, TAB_SESSION_HEADER, type VerifyConfig, type VerifyOcOptions, _resetFamilyRevocationCheckForTests, _resetJwksCachesForTests, audienceSatisfied, bondMeets, clearSessionCookieHeader, createRevocationCheck, familyRevocationCheck, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readBondClaim, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, revocationKey, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
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";
|
|
@@ -112,4 +138,4 @@ interface SessionRequestHeaders {
|
|
|
112
138
|
declare function getOcSession(headers: SessionRequestHeaders | Headers, options?: VerifyOcOptions): Promise<SessionPayload | null>;
|
|
113
139
|
declare function _resetJwksCachesForTests(): void;
|
|
114
140
|
|
|
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 };
|
|
141
|
+
export { type AudienceRule, type AuthKey, type BondClaim, type CookieOptions, DEFAULT_ISSUER, DISPLAY_IDENTITY_KINDS, type DisplayIdentity, type DisplayIdentityKind, type IncomingRequestHeaders, JWT_ALG, type ResolveSessionResult, type RevocableClaims, type RevocationCheck, type RevocationCheckOptions, type RevocationStatus, SESSION_COOKIE, type SessionPayload, type SessionRequestHeaders, type SignConfig, TAB_SESSION_HEADER, type VerifyConfig, type VerifyOcOptions, _resetFamilyRevocationCheckForTests, _resetJwksCachesForTests, audienceSatisfied, bondMeets, clearSessionCookieHeader, createRevocationCheck, familyRevocationCheck, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readBondClaim, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, revocationKey, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
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";
|
|
@@ -307,10 +426,13 @@ exports.DISPLAY_IDENTITY_KINDS = DISPLAY_IDENTITY_KINDS;
|
|
|
307
426
|
exports.JWT_ALG = JWT_ALG;
|
|
308
427
|
exports.SESSION_COOKIE = SESSION_COOKIE;
|
|
309
428
|
exports.TAB_SESSION_HEADER = TAB_SESSION_HEADER;
|
|
429
|
+
exports._resetFamilyRevocationCheckForTests = _resetFamilyRevocationCheckForTests;
|
|
310
430
|
exports._resetJwksCachesForTests = _resetJwksCachesForTests;
|
|
311
431
|
exports.audienceSatisfied = audienceSatisfied;
|
|
312
432
|
exports.bondMeets = bondMeets;
|
|
313
433
|
exports.clearSessionCookieHeader = clearSessionCookieHeader;
|
|
434
|
+
exports.createRevocationCheck = createRevocationCheck;
|
|
435
|
+
exports.familyRevocationCheck = familyRevocationCheck;
|
|
314
436
|
exports.getOcSession = getOcSession;
|
|
315
437
|
exports.loadPrivateKey = loadPrivateKey;
|
|
316
438
|
exports.loadPublicKey = loadPublicKey;
|
|
@@ -320,6 +442,7 @@ exports.readBondClaim = readBondClaim;
|
|
|
320
442
|
exports.readSessionCookie = readSessionCookie;
|
|
321
443
|
exports.resolveDisplayIdentity = resolveDisplayIdentity;
|
|
322
444
|
exports.resolveSessionFromRequest = resolveSessionFromRequest;
|
|
445
|
+
exports.revocationKey = revocationKey;
|
|
323
446
|
exports.serializeSessionCookie = serializeSessionCookie;
|
|
324
447
|
exports.signSession = signSession;
|
|
325
448
|
exports.verifyOcToken = verifyOcToken;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["importJWK","SignJWT","jwtVerify"],"mappings":";;;;;AAwBO,IAAM,OAAA,GAAU;AAChB,IAAM,cAAA,GAAiB;AACvB,IAAM,cAAA,GAAiB;AAUvB,IAAM,sBAAA,GAAyB,CAAC,KAAA,EAAO,KAAA,EAAO,SAAS,OAAO;AAgK9D,SAAS,cAAc,OAAA,EAA2C;AACvE,EAAA,MAAM,IAAI,OAAA,CAAQ,OAAA;AAClB,EAAA,IAAI,CAAC,KAAK,OAAO,CAAA,KAAM,YAAY,CAAA,CAAE,CAAA,KAAM,GAAG,OAAO,IAAA;AACrD,EAAA,MAAM,IAAA,GAAO,CAAC,CAAA,CAAE,UAAA,EAAY,EAAE,UAAA,EAAY,CAAA,CAAE,MAAA,EAAQ,CAAA,CAAE,UAAU,CAAA;AAChE,EAAA,IAAI,CAAC,IAAA,CAAK,KAAA,CAAM,CAAC,MAAM,OAAO,CAAA,KAAM,QAAA,IAAY,MAAA,CAAO,SAAS,CAAC,CAAA,IAAK,CAAA,IAAK,CAAC,GAAG,OAAO,IAAA;AACtF,EAAA,IAAI,OAAO,CAAA,CAAE,SAAA,KAAc,QAAA,IAAY,CAAC,iBAAiB,IAAA,CAAK,CAAA,CAAE,SAAS,CAAA,EAAG,OAAO,IAAA;AACnF,EAAA,OAAO,CAAA;AACT;AAQO,SAAS,SAAA,CACd,SACA,IAAA,EACS;AACT,EAAA,MAAM,CAAA,GAAI,cAAc,OAAO,CAAA;AAC/B,EAAA,IAAI,CAAC,GAAG,OAAO,KAAA;AACf,EAAA,IAAI,CAAA,CAAE,UAAA,IAAc,IAAA,CAAK,OAAA,IAAW,IAAI,OAAO,KAAA;AAC/C,EAAA,IAAI,CAAA,CAAE,UAAA,IAAc,IAAA,CAAK,OAAA,IAAW,IAAI,OAAO,KAAA;AAC/C,EAAA,IAAI,IAAA,CAAK,eAAe,MAAA,EAAW;AACjC,IAAA,MAAM,GAAA,GAAM,KAAK,KAAA,CAAM,IAAA,CAAK,KAAI,GAAI,GAAI,IAAI,CAAA,CAAE,UAAA;AAC9C,IAAA,IAAI,GAAA,GAAM,CAAA,IAAK,GAAA,GAAM,IAAA,CAAK,YAAY,OAAO,KAAA;AAAA,EAC/C;AACA,EAAA,OAAO,IAAA;AACT;AAoBO,SAAS,iBAAA,CACd,SACA,IAAA,EACS;AACT,EAAA,IAAI,OAAO,OAAA,CAAQ,UAAA,KAAe,QAAA,EAAU,OAAO,KAAA;AACnD,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,OAAA,CAAQ,UAAU,GAAG,OAAO,KAAA;AACjD,EAAA,IAAI,IAAA,CAAK,YAAA,IAAgB,CAAA,EAAG,OAAO,KAAA;AACnC,EAAA,MAAM,MAAA,GAAS,KAAK,KAAA,CAAM,IAAA,CAAK,KAAI,GAAI,GAAI,IAAI,OAAA,CAAQ,UAAA;AACvD,EAAA,OAAO,MAAA,IAAU,CAAA,IAAK,MAAA,GAAS,IAAA,CAAK,YAAA;AACtC;AAkBO,SAAS,eAAA,CACd,SACA,IAAA,EACS;AACT,EAAA,IAAI,OAAO,OAAA,CAAQ,OAAA,KAAY,QAAA,EAAU,OAAO,KAAA;AAChD,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,OAAA,CAAQ,OAAO,GAAG,OAAO,KAAA;AAC9C,EAAA,IAAI,IAAA,CAAK,YAAA,IAAgB,CAAA,EAAG,OAAO,KAAA;AACnC,EAAA,MAAM,MAAA,GAAS,KAAK,KAAA,CAAM,IAAA,CAAK,KAAI,GAAI,GAAI,IAAI,OAAA,CAAQ,OAAA;AACvD,EAAA,OAAO,MAAA,IAAU,CAAA,IAAK,MAAA,GAAS,IAAA,CAAK,YAAA;AACtC;AAeO,SAAS,uBACd,OAAA,EACiB;AACjB,EAAA,MAAM,QAAQ,OAAA,CAAQ,gBAAA;AACtB,EAAA,IACE,SACA,OAAO,KAAA,KAAU,QAAA,IACjB,OAAO,MAAM,KAAA,KAAU,QAAA,IACvB,KAAA,CAAM,KAAA,CAAM,SAAS,CAAA,IACpB,sBAAA,CAA6C,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA,EACjE;AACA,IAAA,OAAO,EAAE,IAAA,EAAM,KAAA,CAAM,IAAA,EAAM,KAAA,EAAO,MAAM,KAAA,EAAM;AAAA,EAChD;AACA,EAAA,OAAO,EAAE,IAAA,EAAM,KAAA,EAAO,KAAA,EAAO,QAAQ,MAAA,EAAO;AAC9C;AAkBO,SAAS,iBAAA,CAAkB,KAAc,IAAA,EAAyC;AACvF,EAAA,MAAM,IAAA,GAAO,GAAA,KAAQ,MAAA,GAAY,EAAC,GAAI,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA,GAAI,GAAA,GAAM,CAAC,GAAG,CAAA;AACrE,EAAA,IAAI,IAAA,KAAS,KAAK,OAAO,IAAA;AACzB,EAAA,IAAI,IAAA,KAAS,MAAA,EAAW,OAAO,IAAA,CAAK,MAAA,KAAW,CAAA;AAC/C,EAAA,MAAM,OAAO,OAAO,IAAA,KAAS,QAAA,GAAW,CAAC,IAAI,CAAA,GAAI,IAAA;AACjD,EAAA,OAAO,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA,KAAM,OAAO,MAAM,QAAA,IAAY,IAAA,CAAK,QAAA,CAAS,CAAC,CAAC,CAAA;AACnE;AAoBA,SAAS,UAAU,KAAA,EAAwC;AACzD,EAAA,MAAM,OACJ,OAAO,MAAA,KAAW,cACd,MAAA,CAAO,IAAA,CAAK,OAAO,WAAW,CAAA,CAAE,QAAA,CAAS,MAAM,IAC/C,IAAI,WAAA,GAAc,MAAA,CAAO,eAAA,CAAgB,KAAK,CAAC,CAAA;AACrD,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC9B,EAAA,IAAI,CAAC,MAAA,IAAU,OAAO,MAAA,KAAW,QAAA,EAAU;AACzC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,gBAAgB,KAAA,EAA2B;AAClD,EAAA,MAAM,GAAA,GAAM,KAAA,CAAM,MAAA,GAAS,CAAA,KAAM,CAAA,GAAI,EAAA,GAAK,GAAA,CAAI,MAAA,CAAO,CAAA,GAAK,KAAA,CAAM,MAAA,GAAS,CAAE,CAAA;AAC3E,EAAA,MAAM,GAAA,GAAA,CAAO,QAAQ,GAAA,EAAK,OAAA,CAAQ,MAAM,GAAG,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAA;AAC9D,EAAA,MAAM,MAAM,OAAO,IAAA,KAAS,WAAA,GAAc,IAAA,CAAK,GAAG,CAAA,GAAI,EAAA;AACtD,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,GAAA,CAAI,MAAM,CAAA;AACvC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,GAAA,CAAI,MAAA,EAAQ,CAAA,EAAA,EAAK,KAAA,CAAM,CAAC,CAAA,GAAI,GAAA,CAAI,UAAA,CAAW,CAAC,CAAA;AAChE,EAAA,OAAO,KAAA;AACT;AAEA,IAAI,gBAAA,GAA4C,IAAA;AAChD,IAAI,cAAA,GAAgC,IAAA;AAC7B,SAAS,cAAc,SAAA,EAAqC;AACjE,EAAA,IAAI,CAAC,gBAAA,IAAoB,cAAA,KAAmB,SAAA,EAAW;AACrD,IAAA,cAAA,GAAiB,SAAA;AACjB,IAAA,gBAAA,GAAmBA,cAAA;AAAA,MACjB,UAAU,SAAS,CAAA;AAAA,MACnB;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,gBAAA;AACT;AAEA,IAAI,iBAAA,GAA6C,IAAA;AACjD,IAAI,eAAA,GAAiC,IAAA;AAC9B,SAAS,eAAe,UAAA,EAAsC;AACnE,EAAA,IAAI,CAAC,iBAAA,IAAqB,eAAA,KAAoB,UAAA,EAAY;AACxD,IAAA,eAAA,GAAkB,UAAA;AAClB,IAAA,iBAAA,GAAoBA,cAAA;AAAA,MAClB,UAAU,UAAU,CAAA;AAAA,MACpB;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,iBAAA;AACT;AAGO,SAAS,eAAe,SAAA,EAA4C;AACzE,EAAA,OAAO,UAAU,SAAS,CAAA;AAC5B;AAWA,eAAsB,WAAA,CACpB,MAAA,EAiBA,GAAA,EACA,UAAA,EACiB;AACjB,EAAA,MAAM,MAAM,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,KAAQ,GAAI,CAAA;AAGxC,EAAA,MAAM,EAAE,GAAA,EAAK,GAAG,IAAA,EAAK,GAAI,MAAA;AACzB,EAAA,MAAM,GAAA,GAAM,IAAIC,YAAA,CAAQ,IAAI,CAAA,CACzB,kBAAA,CAAmB,EAAE,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,KAAA,EAAO,GAAA,EAAK,GAAA,CAAI,KAAK,CAAA,CAC7D,SAAA,CAAU,GAAA,CAAI,MAAA,IAAU,cAAc,CAAA,CACtC,WAAA,CAAY,GAAG,CAAA,CACf,iBAAA,CAAkB,GAAA,GAAM,UAAU,CAAA;AACrC,EAAA,IAAI,GAAA,EAAK,GAAA,CAAI,WAAA,CAAY,GAAG,CAAA;AAC5B,EAAA,OAAO,IAAI,IAAA,CAAK,MAAM,cAAA,CAAe,GAAA,CAAI,UAAU,CAAC,CAAA;AACtD;AASA,eAAsB,kBAAA,CACpB,OACA,GAAA,EACgC;AAChC,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,MAAMC,cAAA,CAAU,KAAA,EAAO,MAAM,aAAA,CAAc,GAAA,CAAI,SAAS,CAAA,EAAG;AAAA,MACrE,UAAA,EAAY,CAAC,OAAO,CAAA;AAAA,MACpB,MAAA,EAAQ,IAAI,MAAA,IAAU;AAAA,KACvB,CAAA;AACD,IAAA,MAAM,IAAI,GAAA,CAAI,OAAA;AACd,IAAA,IAAI,CAAC,EAAE,GAAA,IAAO,CAAC,EAAE,MAAA,IAAU,CAAC,CAAA,CAAE,GAAA,EAAK,OAAO,IAAA;AAC1C,IAAA,IAAI,CAAC,iBAAA,CAAkB,CAAA,CAAE,KAAK,GAAA,CAAI,QAAQ,GAAG,OAAO,IAAA;AACpD,IAAA,OAAO,CAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAkBO,SAAS,sBAAA,CACd,KAAA,EACA,IAAA,GAAsB,EAAC,EACf;AACR,EAAA,MAAM,QAAkB,CAAC,CAAA,EAAG,cAAc,CAAA,CAAA,EAAI,KAAK,CAAA,CAAE,CAAA;AACrD,EAAA,KAAA,CAAM,IAAA,CAAK,CAAA,KAAA,EAAQ,IAAA,CAAK,IAAA,IAAQ,GAAG,CAAA,CAAE,CAAA;AACrC,EAAA,IAAI,IAAA,CAAK,QAAA,IAAY,IAAA,EAAM,KAAA,CAAM,KAAK,UAAU,CAAA;AAChD,EAAA,KAAA,CAAM,IAAA,CAAK,CAAA,SAAA,EAAY,IAAA,CAAK,QAAA,IAAY,KAAK,CAAA,CAAE,CAAA;AAC/C,EAAA,IAAI,IAAA,CAAK,MAAA,IAAU,IAAA,EAAM,KAAA,CAAM,KAAK,QAAQ,CAAA;AAC5C,EAAA,IAAI,KAAK,MAAA,EAAQ,KAAA,CAAM,KAAK,CAAA,OAAA,EAAU,IAAA,CAAK,MAAM,CAAA,CAAE,CAAA;AACnD,EAAA,IAAI,IAAA,CAAK,UAAU,IAAA,EAAM,KAAA,CAAM,KAAK,CAAA,QAAA,EAAW,IAAA,CAAK,MAAM,CAAA,CAAE,CAAA;AAC5D,EAAA,IAAI,IAAA,CAAK,SAAS,KAAA,CAAM,IAAA,CAAK,WAAW,IAAA,CAAK,OAAA,CAAQ,WAAA,EAAa,CAAA,CAAE,CAAA;AACpE,EAAA,OAAO,KAAA,CAAM,KAAK,IAAI,CAAA;AACxB;AAGO,SAAS,wBAAA,CACd,IAAA,GAA0D,EAAC,EACnD;AACR,EAAA,OAAO,uBAAuB,EAAA,EAAI;AAAA,IAChC,GAAG,IAAA;AAAA,IACH,MAAA,EAAQ,CAAA;AAAA,IACR,OAAA,kBAAS,IAAI,IAAA,CAAK,CAAC;AAAA,GACpB,CAAA;AACH;AAMO,SAAS,kBACd,YAAA,EACe;AACf,EAAA,IAAI,CAAC,cAAc,OAAO,IAAA;AAC1B,EAAA,MAAM,MAAA,GAAS,GAAG,cAAc,CAAA,CAAA,CAAA;AAChC,EAAA,KAAA,MAAW,IAAA,IAAQ,YAAA,CAAa,KAAA,CAAM,GAAG,CAAA,EAAG;AAC1C,IAAA,MAAM,OAAA,GAAU,KAAK,IAAA,EAAK;AAC1B,IAAA,IAAI,OAAA,CAAQ,UAAA,CAAW,MAAM,CAAA,EAAG;AAC9B,MAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,KAAA,CAAM,MAAA,CAAO,MAAM,CAAA;AACvC,MAAA,OAAO,GAAA,CAAI,MAAA,GAAS,CAAA,GAAI,GAAA,GAAM,IAAA;AAAA,IAChC;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT;AAQO,SAAS,sBACd,YAAA,EACU;AACV,EAAA,IAAI,CAAC,YAAA,EAAc,OAAO,EAAC;AAC3B,EAAA,MAAM,MAAA,GAAS,GAAG,cAAc,CAAA,CAAA,CAAA;AAChC,EAAA,MAAM,MAAgB,EAAC;AACvB,EAAA,KAAA,MAAW,IAAA,IAAQ,YAAA,CAAa,KAAA,CAAM,GAAG,CAAA,EAAG;AAC1C,IAAA,MAAM,OAAA,GAAU,KAAK,IAAA,EAAK;AAC1B,IAAA,IAAI,OAAA,CAAQ,UAAA,CAAW,MAAM,CAAA,EAAG;AAC9B,MAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,KAAA,CAAM,MAAA,CAAO,MAAM,CAAA;AACvC,MAAA,IAAI,GAAA,CAAI,MAAA,GAAS,CAAA,EAAG,GAAA,CAAI,KAAK,GAAG,CAAA;AAAA,IAClC;AAAA,EACF;AACA,EAAA,OAAO,GAAA;AACT;AAcO,IAAM,kBAAA,GAAqB;AAUlC,SAAS,WAAA,CACP,SACA,IAAA,EACe;AACf,EAAA,IAAI,UAAU,OAAO,CAAA,EAAG,OAAO,OAAA,CAAQ,IAAI,IAAI,CAAA;AAC/C,EAAA,MAAM,MAAM,OAAA,CAAQ,IAAI,KAAK,OAAA,CAAQ,IAAA,CAAK,aAAa,CAAA;AACvD,EAAA,IAAI,MAAM,OAAA,CAAQ,GAAG,GAAG,OAAO,GAAA,CAAI,CAAC,CAAA,IAAK,IAAA;AACzC,EAAA,OAAO,OAAO,GAAA,KAAQ,QAAA,GAAW,GAAA,GAAM,IAAA;AACzC;AAmBA,eAAsB,yBAAA,CACpB,SACA,GAAA,EAC+B;AAC/B,EAAA,MAAM,QAAA,GAAW,WAAA,CAAY,OAAA,EAAS,kBAAkB,CAAA;AACxD,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,MAAM,OAAA,GAAU,MAAM,kBAAA,CAAmB,QAAA,EAAU,GAAG,CAAA;AACtD,IAAA,IAAI,SAAS,OAAO,EAAE,IAAI,IAAA,EAAM,OAAA,EAAS,KAAK,KAAA,EAAM;AACpD,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,aAAA,EAAc;AAAA,EAC5C;AACA,EAAA,MAAM,YAAA,GAAe,WAAA,CAAY,OAAA,EAAS,QAAQ,CAAA;AAClD,EAAA,KAAA,MAAW,KAAA,IAAS,qBAAA,CAAsB,YAAY,CAAA,EAAG;AACvD,IAAA,MAAM,OAAA,GAAU,MAAM,kBAAA,CAAmB,KAAA,EAAO,GAAG,CAAA;AACnD,IAAA,IAAI,SAAS,OAAO,EAAE,IAAI,IAAA,EAAM,OAAA,EAAS,KAAK,QAAA,EAAS;AAAA,EACzD;AACA,EAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,YAAA,EAAa;AAC3C;AAWA,IAAM,mBAAA,GAAsB,KAAK,EAAA,GAAK,GAAA;AAOtC,IAAM,SAAA,uBAAgB,GAAA,EAA4B;AAClD,IAAM,QAAA,uBAAe,GAAA,EAAqC;AAgB1D,IAAM,iBAAA,uBAAwB,GAAA,EAAoB;AAClD,IAAM,8BAAA,GAAiC,GAAA;AAMvC,eAAe,SAAA,CACb,QACA,KAAA,EACyB;AACzB,EAAA,MAAM,MAAA,GAAS,SAAA,CAAU,GAAA,CAAI,MAAM,CAAA;AACnC,EAAA,IAAI,UAAU,IAAA,CAAK,GAAA,KAAQ,MAAA,CAAO,SAAA,GAAY,OAAO,OAAO,MAAA;AAC5D,EAAA,MAAM,QAAA,GAAW,QAAA,CAAS,GAAA,CAAI,MAAM,CAAA;AACpC,EAAA,IAAI,UAAU,OAAO,QAAA;AACrB,EAAA,MAAM,KAAK,YAAqC;AAC9C,IAAA,IAAI;AACF,MAAA,MAAM,MAAM,CAAA,EAAG,MAAA,CAAO,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAC,CAAA,sBAAA,CAAA;AACxC,MAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,GAAA,EAAK,EAAE,SAAS,EAAE,MAAA,EAAQ,kBAAA,EAAmB,EAAG,CAAA;AACxE,MAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAA,oCAAA,EAAuC,GAAG,CAAA,UAAA,EAAa,GAAA,CAAI,MAAM,CAAA;AAAA,SACnE;AAAA,MACF;AACA,MAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAC7B,MAAA,IAAI,CAAC,IAAA,IAAQ,CAAC,MAAM,OAAA,CAAQ,IAAA,CAAK,IAAI,CAAA,EAAG;AACtC,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,oCAAoC,GAAG,CAAA,+BAAA;AAAA,SACzC;AAAA,MACF;AACA,MAAA,MAAM,OAAgC,EAAC;AACvC,MAAA,KAAA,MAAW,CAAA,IAAK,KAAK,IAAA,EAAM;AACzB,QAAA,IAAI,OAAO,CAAA,CAAE,GAAA,KAAQ,QAAA,EAAU;AAC/B,QAAA,IAAI;AACF,UAAA,IAAA,CAAK,EAAE,GAAG,CAAA,GAAK,MAAMF,cAAA,CAAU,GAAG,OAAO,CAAA;AAAA,QAC3C,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF;AACA,MAAA,MAAM,QAAwB,EAAE,IAAA,EAAM,SAAA,EAAW,IAAA,CAAK,KAAI,EAAE;AAC5D,MAAA,SAAA,CAAU,GAAA,CAAI,QAAQ,KAAK,CAAA;AAC3B,MAAA,OAAO,KAAA;AAAA,IACT,SAAS,GAAA,EAAK;AAKZ,MAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,GAAA,CAAI,MAAM,CAAA;AAClC,MAAA,IAAI,OAAO,OAAO,KAAA;AAClB,MAAA,MAAM,GAAA;AAAA,IACR;AAAA,EACF,CAAA,GAAG,CAAE,OAAA,CAAQ,MAAM;AACjB,IAAA,QAAA,CAAS,OAAO,MAAM,CAAA;AAAA,EACxB,CAAC,CAAA;AACD,EAAA,QAAA,CAAS,GAAA,CAAI,QAAQ,CAAC,CAAA;AACtB,EAAA,OAAO,CAAA;AACT;AAEA,SAAS,iBAAiB,MAAA,EAAsB;AAC9C,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI;AACF,IAAA,GAAA,GAAM,IAAI,IAAI,MAAM,CAAA;AAAA,EACtB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,gEAAgE,MAAM,CAAA;AAAA,KACxE;AAAA,EACF;AAKA,EAAA,MAAM,UAAA,GACJ,IAAI,QAAA,KAAa,WAAA,IACjB,IAAI,QAAA,KAAa,WAAA,IACjB,IAAI,QAAA,KAAa,KAAA;AACnB,EAAA,IAAI,GAAA,CAAI,QAAA,KAAa,QAAA,IAAY,CAAC,UAAA,EAAY;AAC5C,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,mDAAA,EAAsD,GAAA,CAAI,QAAQ,CAAA,EAAA,EAAK,IAAI,QAAQ,CAAA,6BAAA;AAAA,KACrF;AAAA,EACF;AACF;AA4BA,eAAsB,aAAA,CACpB,KAAA,EACA,OAAA,GAA2B,EAAC,EACI;AAChC,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,cAAA;AACjC,EAAA,MAAM,GAAA,GAAM,QAAQ,cAAA,IAAkB,mBAAA;AACtC,EAAA,IAAI;AACF,IAAA,gBAAA,CAAiB,MAAM,CAAA;AAGvB,IAAA,IAAI,KAAA,GAAQ,MAAM,SAAA,CAAU,MAAA,EAAQ,GAAG,CAAA;AACvC,IAAA,MAAM,SAAA,GAAY,KAAA,CAAM,KAAA,CAAM,GAAG,EAAE,CAAC,CAAA;AACpC,IAAA,IAAI,CAAC,WAAW,OAAO,IAAA;AACvB,IAAA,MAAM,aACJ,OAAO,MAAA,KAAW,cACd,MAAA,CAAO,IAAA,CAAK,WAAW,WAAW,CAAA,CAAE,QAAA,CAAS,MAAM,IACnD,IAAI,WAAA,GAAc,MAAA,CAAO,eAAA,CAAgB,SAAS,CAAC,CAAA;AACzD,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,UAAU,CAAA;AACpC,IAAA,IAAI,CAAC,MAAA,CAAO,GAAA,EAAK,OAAO,IAAA;AACxB,IAAA,IAAI,GAAA,GAAM,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,GAAG,CAAA;AAC/B,IAAA,IAAI,CAAC,GAAA,EAAK;AAKR,MAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,MAAA,MAAM,IAAA,GAAO,iBAAA,CAAkB,GAAA,CAAI,MAAM,CAAA,IAAK,CAAA;AAC9C,MAAA,IAAI,GAAA,GAAM,QAAQ,8BAAA,EAAgC;AAChD,QAAA,iBAAA,CAAkB,GAAA,CAAI,QAAQ,GAAG,CAAA;AACjC,QAAA,SAAA,CAAU,OAAO,MAAM,CAAA;AACvB,QAAA,KAAA,GAAQ,MAAM,SAAA,CAAU,MAAA,EAAQ,GAAG,CAAA;AACnC,QAAA,GAAA,GAAM,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,GAAG,CAAA;AAAA,MAC7B;AAAA,IACF;AACA,IAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AACjB,IAAA,MAAM,GAAA,GAAM,MAAME,cAAA,CAAU,KAAA,EAAO,GAAA,EAAK;AAAA,MACtC,UAAA,EAAY,CAAC,OAAO,CAAA;AAAA,MACpB;AAAA,KACD,CAAA;AACD,IAAA,MAAM,IAAI,GAAA,CAAI,OAAA;AACd,IAAA,IAAI,CAAC,EAAE,GAAA,IAAO,CAAC,EAAE,MAAA,IAAU,CAAC,CAAA,CAAE,GAAA,EAAK,OAAO,IAAA;AAC1C,IAAA,IAAI,CAAC,iBAAA,CAAkB,CAAA,CAAE,KAAK,OAAA,CAAQ,QAAQ,GAAG,OAAO,IAAA;AACxD,IAAA,OAAO,CAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAkBA,eAAsB,YAAA,CACpB,OAAA,EACA,OAAA,GAA2B,EAAC,EACI;AAChC,EAAA,MAAM,MAAA,GAAS,UAAU,OAAO,CAAA,GAC5B,QAAQ,GAAA,CAAI,QAAQ,CAAA,GACnB,OAAA,CAAQ,MAAA,IAAU,IAAA;AACvB,EAAA,MAAM,aAAA,GAAgB,UAAU,OAAO,CAAA,GACnC,QAAQ,GAAA,CAAI,eAAe,CAAA,GAC1B,OAAA,CAAQ,aAAA,IAAiB,IAAA;AAC9B,EAAA,IAAI,KAAA,GAAQ,kBAAkB,MAAM,CAAA;AACpC,EAAA,IAAI,CAAC,KAAA,IAAS,OAAO,aAAA,KAAkB,QAAA,EAAU;AAC/C,IAAA,MAAM,CAAA,GAAI,kBAAA,CAAmB,IAAA,CAAK,aAAa,CAAA;AAC/C,IAAA,IAAI,CAAA,EAAG,KAAA,GAAQ,CAAA,CAAE,CAAC,CAAA;AAAA,EACpB;AACA,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,EAAA,OAAO,aAAA,CAAc,OAAO,OAAO,CAAA;AACrC;AAEA,SAAS,UAAU,KAAA,EAAkC;AACnD,EAAA,OACE,OAAO,YAAY,WAAA,IACnB,KAAA,KAAU,QACV,OAAO,KAAA,KAAU,YACjB,KAAA,YAAiB,OAAA;AAErB;AAOO,SAAS,wBAAA,GAAiC;AAC/C,EAAA,SAAA,CAAU,KAAA,EAAM;AAChB,EAAA,QAAA,CAAS,KAAA,EAAM;AACf,EAAA,iBAAA,CAAkB,KAAA,EAAM;AAC1B","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 * Slug of the federation this user is bound to (their \"home\"\n * federation). Multi-federation routing reads this; v1 has one live\n * federation so it's set on first signin and rarely changes. Absent\n * on tokens minted before this field shipped, and on accounts that\n * have never bound to a federation. Consumers fall back to the\n * directory default at /api/federations when absent.\n */\n home_federation?: string | null;\n /**\n * Discriminator recording where this user is on the custody-state\n * graph. Three legitimate states:\n *\n * - 'fedimint_threshold' — federation custody via the OC-introduced\n * federation (default for email-OTP signups; OC's threshold\n * guardians hold key shares).\n * - 'fedimint_client' — user moved their balance to a Fedimint\n * federation they picked themselves (still federation custody,\n * but on a federation OC didn't introduce).\n * - 'bip322' — full self-custody. The user signs with\n * their own Bitcoin key. OC has zero authority over their funds.\n *\n * Graduation is the product thesis. This field is the structural\n * primitive that records progress along the custody-state graph;\n * transitions are recorded as anchored \"rebind\" envelopes. Absent\n * on tokens minted before the field shipped — consumers should\n * treat undefined as 'fedimint_threshold' for email-OTP identities\n * and 'bip322' for BIP-322 identities (the default-by-construction\n * mapping).\n */\n signing_method?: \"fedimint_threshold\" | \"fedimint_client\" | \"bip322\" | null;\n /**\n * Historical did_ocs consolidated INTO this account via the\n * /api/auth/link/btc transfer-under-dual-proof flow. Consumers that\n * aggregate per-user data should union queries across\n * `[did_oc, ...merged_from]`. Absent / empty means \"no absorbed\n * accounts.\" Order is not significant.\n */\n merged_from?: string[];\n /**\n * Unix seconds when the user last successfully completed a WebAuthn\n * step-up assertion on the auth host. Consumer subdomains gate\n * sensitive actions on `(now - step_up_at) < their_window` —\n * typically 5 minutes. Absent on JWTs minted before WebAuthn\n * shipped, or on tokens minted by ordinary signin (only set by\n * /api/auth/webauthn/assertion/verify on success). Use\n * `verifyStepUpClaim()` to read it safely.\n */\n step_up_at?: number;\n /**\n * Unix seconds when the user last successfully completed an inline\n * sudo-mode re-authentication on the auth host (email-OTP for\n * email-primary identities, BIP-322 challenge for btc-primary\n * identities). Gates auth-graph-mutating operations — register an\n * additional WebAuthn credential, revoke one, link a new identity,\n * change recovery method — without forcing a full sign-out/sign-in.\n * Same shape and freshness window posture as `step_up_at`; the two\n * are independent claims (sudo proves \"you typed your password\n * again,\" step-up proves \"you tapped your hardware key\"). Read it\n * with `verifySudoClaim()`.\n */\n sudo_at?: number;\n /**\n * Optional best-effort owner flag · set by the auth host at signin\n * time when the new did_oc matches OWNER_OC_ADDRESSES. Surfaced\n * through to clients via `useOcSession().account.isOwner` so the\n * family-switcher and other low-stakes UX can render owner-only\n * affordances (e.g. an analytics.ochk.io entry).\n *\n * **NOT A SECURITY BOUNDARY.** Sensitive surfaces still re-check\n * the live `OWNER_OC_ADDRESSES` env against `session.did_oc` on\n * every request (analytics.ochk.io's requireOwner does this).\n * If an owner is removed from the env, their JWT may still carry\n * `is_owner: true` for up to 30 days — the UX hint goes stale,\n * but every gated action they attempt re-fails server-side.\n *\n * Treat absence as `false`. Set only on first signin or re-issue;\n * never mutated mid-session.\n */\n is_owner?: boolean;\n /**\n * The identity the user has chosen to show in their account badge\n * — the collapsed label every family site (and any integrator)\n * renders instead of the raw `did:oc`. Baked into the JWT so the\n * choice is consistent across every `.ochk.io` subdomain with no\n * round-trip, the same posture as `name` / `npub`.\n *\n * Re-resolved by the auth host on every mint: an explicit user\n * promotion wins; absent that, it defaults to the account's\n * sign-in identity (BIP-322 accounts → their Bitcoin address,\n * federation accounts → their email). Absent / null on tokens\n * minted before this field shipped — consumers fall back to the\n * `did_oc`. Read it safely with `resolveDisplayIdentity()`.\n */\n display_identity?: DisplayIdentity | null;\n /**\n * Present when the account is backed by a bond. Absent means no bond, not\n * a failed check. Read it with `readBondClaim()` / `bondMeets()`.\n */\n oc_bond?: BondClaim;\n}\n\n/** The token's bond claim when it is well-formed, else null. */\nexport function readBondClaim(payload: SessionPayload): BondClaim | null {\n const b = payload.oc_bond as Partial<BondClaim> | undefined;\n if (!b || typeof b !== \"object\" || b.v !== 1) return null;\n const nums = [b.sats_floor, b.days_floor, b.height, b.checked_at];\n if (!nums.every((n) => typeof n === \"number\" && Number.isFinite(n) && n >= 0)) return null;\n if (typeof b.nullifier !== \"string\" || !/^[0-9a-f]{64}$/.test(b.nullifier)) return null;\n return b as BondClaim;\n}\n\n/**\n * True when the account is backed by a coin whose floors reach `minSats`\n * and `minDays`, and the issuer checked it within\n * `maxAgeSecs` (default: no limit). Compare against the floor ladder: a\n * `minSats` of 50000 is met by a `sats_floor` of 100000, not 10000.\n */\nexport function bondMeets(\n payload: SessionPayload,\n need: { minSats?: number; minDays?: number; maxAgeSecs?: number },\n): boolean {\n const b = readBondClaim(payload);\n if (!b) return false;\n if (b.sats_floor < (need.minSats ?? 0)) return false;\n if (b.days_floor < (need.minDays ?? 0)) return false;\n if (need.maxAgeSecs !== undefined) {\n const age = Math.floor(Date.now() / 1000) - b.checked_at;\n if (age < 0 || age > need.maxAgeSecs) return false;\n }\n return true;\n}\n\n/**\n * Returns true when the JWT carries a fresh `step_up_at` claim —\n * i.e. the user successfully completed a WebAuthn assertion within\n * the supplied freshness window (in seconds). Returns false for\n * tokens that lack the claim entirely, that carry a stale value,\n * that carry a value somehow in the future, or where the freshness\n * window is non-positive.\n *\n * Consumers gating sensitive actions read this in two places:\n *\n * - Client side · before calling the action, to decide whether to\n * trigger `useStepUpAuth()`. Skip the prompt if already fresh.\n * - Server side · in the route handler, AFTER verifying the JWT,\n * to enforce the policy regardless of what the client did.\n *\n * The freshness window is consumer-chosen — typical default is 300s\n * (5 min); higher-value spends might use 60s.\n */\nexport function verifyStepUpClaim(\n payload: SessionPayload,\n opts: { max_age_secs: number },\n): boolean {\n if (typeof payload.step_up_at !== \"number\") return false;\n if (!Number.isFinite(payload.step_up_at)) return false;\n if (opts.max_age_secs <= 0) return false;\n const ageSec = Math.floor(Date.now() / 1000) - payload.step_up_at;\n return ageSec >= 0 && ageSec < opts.max_age_secs;\n}\n\n/**\n * Returns true when the JWT carries a fresh `sudo_at` claim — i.e. the\n * user successfully re-authenticated inline (email-OTP or BIP-322\n * challenge) within the supplied freshness window. Independent of\n * `step_up_at` · sudo is \"you proved your primary identity again,\"\n * step-up is \"you tapped your hardware key.\"\n *\n * Returns false for tokens that lack the claim, that carry a stale\n * value, that carry a future-dated value (clock skew or malicious\n * mint), or where the freshness window is non-positive.\n *\n * Use this to gate auth-graph-mutating operations on the auth host —\n * adding a second hardware key, linking a new identity, generating\n * recovery codes, changing recovery method. Default window is 5\n * minutes; tighten per-operation as needed.\n */\nexport function verifySudoClaim(\n payload: SessionPayload,\n opts: { max_age_secs: number },\n): boolean {\n if (typeof payload.sudo_at !== \"number\") return false;\n if (!Number.isFinite(payload.sudo_at)) return false;\n if (opts.max_age_secs <= 0) return false;\n const ageSec = Math.floor(Date.now() / 1000) - payload.sudo_at;\n return ageSec >= 0 && ageSec < opts.max_age_secs;\n}\n\n/**\n * Resolve the account-badge identity for a session — total, never\n * throws, always returns a renderable `{ kind, value }`.\n *\n * Returns the JWT's `display_identity` claim when it is present and\n * well-formed; otherwise falls back to the canonical `did:oc`. Use\n * this everywhere you render the badge label rather than reading the\n * raw claim, so tokens minted before the field shipped (and tokens\n * carrying a malformed claim) degrade gracefully to the did.\n *\n * This is the single resolver both `<OcAccountMenu>` and integrators\n * building their own chip should call.\n */\nexport function resolveDisplayIdentity(\n payload: SessionPayload,\n): DisplayIdentity {\n const claim = payload.display_identity;\n if (\n claim &&\n typeof claim === \"object\" &&\n typeof claim.value === \"string\" &&\n claim.value.length > 0 &&\n (DISPLAY_IDENTITY_KINDS as readonly string[]).includes(claim.kind)\n ) {\n return { kind: claim.kind, value: claim.value };\n }\n return { kind: \"did\", value: payload.did_oc };\n}\n\n/**\n * Which audience a verifier accepts.\n *\n * omitted → a FAMILY session only: the token must carry no `aud`.\n * a string → a token bound to that audience (or one of several).\n * \"*\" → either. For the routes built to receive tokens that\n * integrators forward.\n *\n * Omitted is the strict default: a token bound to a relying party is that\n * party's credential and never a family sign-in. Tokens minted before\n * audiences existed carry no `aud`, so every existing session keeps\n * verifying exactly as before.\n */\nexport type AudienceRule = string | readonly string[] | \"*\";\n\n/** Pure · does the payload's `aud` satisfy the rule? */\nexport function audienceSatisfied(aud: unknown, rule: AudienceRule | undefined): boolean {\n const have = aud === undefined ? [] : Array.isArray(aud) ? aud : [aud];\n if (rule === \"*\") return true;\n if (rule === undefined) return have.length === 0;\n const want = typeof rule === \"string\" ? [rule] : rule;\n return have.some((a) => typeof a === \"string\" && want.includes(a));\n}\n\nexport interface VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 public key. */\n publicJwk: string;\n /** Expected `iss` claim. Tokens with a different issuer are rejected. */\n issuer?: string;\n /** Accepted `aud` · see AudienceRule. Omit for family sessions. */\n audience?: AudienceRule;\n}\n\nexport interface SignConfig extends VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 private key. */\n privateJwk: string;\n /** Short key id placed in the JWT header (`kid`). Must match the JWK. */\n kid: string;\n}\n\n// ─── JWK helpers ────────────────────────────────────────────────────────\n\nfunction decodeJwk(value: string): Record<string, unknown> {\n const json =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(value, \"base64url\").toString(\"utf8\")\n : new TextDecoder().decode(base64UrlDecode(value));\n const parsed = JSON.parse(json) as Record<string, unknown>;\n if (!parsed || typeof parsed !== \"object\") {\n throw new Error(\n \"[@orangecheck/auth-core] JWK env did not decode to an object\",\n );\n }\n return parsed;\n}\n\nfunction base64UrlDecode(input: string): Uint8Array {\n const pad = input.length % 4 === 0 ? \"\" : \"=\".repeat(4 - (input.length % 4));\n const b64 = (input + pad).replace(/-/g, \"+\").replace(/_/g, \"/\");\n const bin = typeof atob !== \"undefined\" ? atob(b64) : \"\";\n const bytes = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);\n return bytes;\n}\n\nlet publicKeyPromise: Promise<AuthKey> | null = null;\nlet publicKeyCache: string | null = null;\nexport function loadPublicKey(publicJwk: string): Promise<AuthKey> {\n if (!publicKeyPromise || publicKeyCache !== publicJwk) {\n publicKeyCache = publicJwk;\n publicKeyPromise = importJWK(\n decodeJwk(publicJwk),\n JWT_ALG,\n ) as Promise<AuthKey>;\n }\n return publicKeyPromise;\n}\n\nlet privateKeyPromise: Promise<AuthKey> | null = null;\nlet privateKeyCache: string | null = null;\nexport function loadPrivateKey(privateJwk: string): Promise<AuthKey> {\n if (!privateKeyPromise || privateKeyCache !== privateJwk) {\n privateKeyCache = privateJwk;\n privateKeyPromise = importJWK(\n decodeJwk(privateJwk),\n JWT_ALG,\n ) as Promise<AuthKey>;\n }\n return privateKeyPromise;\n}\n\n/** Parse the `OC_AUTH_PUBLIC_JWK` env var back into a JWK object. */\nexport function parsePublicJwk(publicJwk: string): Record<string, unknown> {\n return decodeJwk(publicJwk);\n}\n\n// ─── Sign / verify ──────────────────────────────────────────────────────\n\n/**\n * Mint a fresh oc_session JWT. Only called on the auth host (the only\n * place that holds the private key).\n *\n * The caller owns recording an ended token's `jti` for the revocation\n * feed that {@link createRevocationCheck} reads.\n */\nexport async function signSession(\n claims: {\n sub: string;\n did_oc: string;\n jti: string;\n name?: string | null;\n npub?: string | null;\n home_federation?: string | null;\n signing_method?: \"fedimint_threshold\" | \"fedimint_client\" | \"bip322\" | null;\n merged_from?: string[];\n step_up_at?: number;\n sudo_at?: number;\n display_identity?: DisplayIdentity | null;\n /** Bind the token to one relying party (e.g. `https://example.com`).\n * Omit for a family session. A bound token verifies only where that\n * audience is expected — never as a family sign-in. */\n aud?: string;\n },\n cfg: SignConfig,\n ttlSeconds: number,\n): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n // did_oc is the sole user identifier. The legacy `addr` shim that\n // mirrored did_oc was dropped in v2.0.0 along with the type field.\n const { aud, ...rest } = claims;\n const jwt = new SignJWT(rest)\n .setProtectedHeader({ alg: JWT_ALG, typ: \"JWT\", kid: cfg.kid })\n .setIssuer(cfg.issuer ?? DEFAULT_ISSUER)\n .setIssuedAt(now)\n .setExpirationTime(now + ttlSeconds);\n if (aud) jwt.setAudience(aud);\n return jwt.sign(await loadPrivateKey(cfg.privateJwk));\n}\n\n/**\n * Crypto-only JWT verify. Returns the payload on success, `null` on\n * any failure (bad signature, expired, wrong issuer, malformed).\n *\n * Safe for consumer subdomains — they only need the public JWK. A signed-out\n * token still verifies here; pass the payload to\n * {@link familyRevocationCheck} to refuse it.\n */\nexport async function verifySessionToken(\n token: string,\n cfg: VerifyConfig,\n): Promise<SessionPayload | null> {\n try {\n const res = await jwtVerify(token, await loadPublicKey(cfg.publicJwk), {\n algorithms: [JWT_ALG],\n issuer: cfg.issuer ?? DEFAULT_ISSUER,\n });\n const p = res.payload as SessionPayload;\n if (!p.sub || !p.did_oc || !p.jti) return null;\n if (!audienceSatisfied(p.aud, cfg.audience)) return null;\n return p;\n } catch {\n return null;\n }\n}\n\n// ─── Cookie helpers ─────────────────────────────────────────────────────\n\nexport interface CookieOptions {\n domain?: string;\n maxAge?: number;\n expires?: Date;\n path?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: \"Lax\" | \"Strict\" | \"None\";\n}\n\n/**\n * Serialize a cookie header value for `Set-Cookie`. Defaults match the\n * ochk.io session shape: HttpOnly, SameSite=Lax, Path=/, Secure in prod.\n */\nexport function serializeSessionCookie(\n token: string,\n opts: CookieOptions = {},\n): string {\n const parts: string[] = [`${SESSION_COOKIE}=${token}`];\n parts.push(`Path=${opts.path ?? \"/\"}`);\n if (opts.httpOnly ?? true) parts.push(\"HttpOnly\");\n parts.push(`SameSite=${opts.sameSite ?? \"Lax\"}`);\n if (opts.secure ?? true) parts.push(\"Secure\");\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n if (opts.maxAge != null) parts.push(`Max-Age=${opts.maxAge}`);\n if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);\n return parts.join(\"; \");\n}\n\n/** Clear the session cookie. Must match the Domain used when it was set. */\nexport function clearSessionCookieHeader(\n opts: Pick<CookieOptions, \"domain\" | \"path\" | \"secure\"> = {},\n): string {\n return serializeSessionCookie(\"\", {\n ...opts,\n maxAge: 0,\n expires: new Date(0),\n });\n}\n\n/**\n * Read the oc_session token out of a raw `Cookie:` header string.\n * Returns `null` if the cookie is missing or empty.\n */\nexport function readSessionCookie(\n cookieHeader: string | null | undefined,\n): string | null {\n if (!cookieHeader) return null;\n const prefix = `${SESSION_COOKIE}=`;\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim();\n if (trimmed.startsWith(prefix)) {\n const val = trimmed.slice(prefix.length);\n return val.length > 0 ? val : null;\n }\n }\n return null;\n}\n\n/**\n * Read EVERY oc_session value out of a raw `Cookie:` header string.\n * Multiple same-name cookies are legitimate (e.g. a stale host-scoped\n * cookie shadowing the `Domain=.ochk.io` one) — verification should\n * try each rather than trust ordering.\n */\nexport function readAllSessionCookies(\n cookieHeader: string | null | undefined,\n): string[] {\n if (!cookieHeader) return [];\n const prefix = `${SESSION_COOKIE}=`;\n const out: string[] = [];\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim();\n if (trimmed.startsWith(prefix)) {\n const val = trimmed.slice(prefix.length);\n if (val.length > 0) out.push(val);\n }\n }\n return out;\n}\n\n// ─── Per-tab session resolution (multi-account · tab pinning) ───────────\n//\n// One browser holds ONE shared `oc_session` cookie (the browser-wide\n// default account) but may be signed into N roster accounts, each with\n// its own concurrently-valid session JWT. A tab \"pins\" itself to one\n// account by holding that account's JWT in sessionStorage and sending\n// it on every API call as the `x-oc-tab-session` header. The header\n// value is a full session JWT — a *credential*, never a bare selector —\n// so servers verify it exactly like the cookie token and stay\n// stateless.\n\n/** Header carrying a tab-pinned session JWT. Lowercase (Node folds headers). */\nexport const TAB_SESSION_HEADER = \"x-oc-tab-session\" as const;\n\nexport type ResolveSessionResult =\n | { ok: true; payload: SessionPayload; via: \"tab\" | \"cookie\" }\n | { ok: false; reason: \"tab_invalid\" | \"no_session\" };\n\n/** Header bag shapes accepted by {@link resolveSessionFromRequest}. */\nexport type IncomingRequestHeaders =\n Headers | Record<string, string | string[] | undefined>;\n\nfunction headerValue(\n headers: IncomingRequestHeaders,\n name: string,\n): string | null {\n if (isHeaders(headers)) return headers.get(name);\n const raw = headers[name] ?? headers[name.toLowerCase()];\n if (Array.isArray(raw)) return raw[0] ?? null;\n return typeof raw === \"string\" ? raw : null;\n}\n\n/**\n * Resolve the EFFECTIVE session for a request — the per-tab choke\n * point every consumer's `readJwtSession` should delegate to.\n *\n * Precedence:\n * 1. `x-oc-tab-session` header, when present. **Fail-closed**: a\n * present-but-invalid tab token resolves to\n * `{ ok: false, reason: 'tab_invalid' }` rather than silently\n * falling back to the cookie — falling back would execute the\n * request as a DIFFERENT account than the tab is displaying,\n * which is precisely the bug per-tab pinning exists to prevent.\n * Clients clear their pin on 401 and re-resolve.\n * 2. Every `oc_session` cookie in the jar, first one that verifies.\n *\n * Crypto-only (signature + exp + iss via {@link verifySessionToken}); a\n * signed-out token still resolves, so pass the payload to\n * {@link familyRevocationCheck}. Never throws.\n */\nexport async function resolveSessionFromRequest(\n headers: IncomingRequestHeaders,\n cfg: VerifyConfig,\n): Promise<ResolveSessionResult> {\n const tabToken = headerValue(headers, TAB_SESSION_HEADER);\n if (tabToken) {\n const payload = await verifySessionToken(tabToken, cfg);\n if (payload) return { ok: true, payload, via: \"tab\" };\n return { ok: false, reason: \"tab_invalid\" };\n }\n const cookieHeader = headerValue(headers, \"cookie\");\n for (const token of readAllSessionCookies(cookieHeader)) {\n const payload = await verifySessionToken(token, cfg);\n if (payload) return { ok: true, payload, via: \"cookie\" };\n }\n return { ok: false, reason: \"no_session\" };\n}\n\n// ─── JWKS-aware verification (zero env vars · zero JWK handling) ─────────\n//\n// verifyOcToken / getOcSession are the integrator-facing verification\n// primitives. They lazy-fetch the auth host's published JWKS at\n// `<issuer>/.well-known/jwks.json` and cache it in-process. Integrators\n// install @orangecheck/me-client (which re-exports these), call\n// `verifyOcToken(token)` or `getOcSession(headers)`, and never see a\n// JWK string, an env var, or a base64 dance.\n\nconst DEFAULT_JWKS_TTL_MS = 60 * 60 * 1000; // 1 hour\n\ninterface JwksCacheEntry {\n keys: Record<string, AuthKey>;\n fetchedAt: number;\n}\n\nconst jwksCache = new Map<string, JwksCacheEntry>();\nconst inflight = new Map<string, Promise<JwksCacheEntry>>();\n\n/**\n * When each issuer last had its cache force-refreshed by an unrecognised\n * `kid`, and how long that path must wait before doing it again.\n *\n * Without this, a token carrying any unknown kid makes the verifier drop its\n * cache and fetch the JWKS again — so a stream of tokens with random kids turns\n * every integrator's server into a refetch amplifier pointed at the auth host,\n * and puts a network round-trip in front of each rejection. `inflight` only\n * collapses CONCURRENT fetches; sequential requests each got their own.\n *\n * A minute is far below any real rotation window, so a genuinely new key is\n * still picked up promptly — the bust exists for rotation, not for every\n * malformed token.\n */\nconst lastForcedRefresh = new Map<string, number>();\nconst FORCED_REFRESH_MIN_INTERVAL_MS = 60_000;\n\ninterface JwksJson {\n keys: Array<Record<string, unknown> & { kid?: string }>;\n}\n\nasync function fetchJwks(\n issuer: string,\n ttlMs: number,\n): Promise<JwksCacheEntry> {\n const cached = jwksCache.get(issuer);\n if (cached && Date.now() - cached.fetchedAt < ttlMs) return cached;\n const existing = inflight.get(issuer);\n if (existing) return existing;\n const p = (async (): Promise<JwksCacheEntry> => {\n try {\n const url = `${issuer.replace(/\\/$/, \"\")}/.well-known/jwks.json`;\n const res = await fetch(url, { headers: { Accept: \"application/json\" } });\n if (!res.ok) {\n throw new Error(\n `[@orangecheck/auth-core] JWKS fetch ${url} returned ${res.status}`,\n );\n }\n const json = (await res.json()) as JwksJson;\n if (!json || !Array.isArray(json.keys)) {\n throw new Error(\n `[@orangecheck/auth-core] JWKS at ${url} did not return {keys:[…]}`,\n );\n }\n const keys: Record<string, AuthKey> = {};\n for (const k of json.keys) {\n if (typeof k.kid !== \"string\") continue;\n try {\n keys[k.kid] = (await importJWK(k, JWT_ALG)) as AuthKey;\n } catch {\n /* skip keys that won't import for our alg */\n }\n }\n const entry: JwksCacheEntry = { keys, fetchedAt: Date.now() };\n jwksCache.set(issuer, entry);\n return entry;\n } catch (err) {\n // Stale-on-error: a transient JWKS outage shouldn't lock out\n // every consumer. If we have a cache entry — even an expired\n // one — keep using it until the next successful fetch. Throw\n // only when we've never seen a usable JWKS.\n const stale = jwksCache.get(issuer);\n if (stale) return stale;\n throw err;\n }\n })().finally(() => {\n inflight.delete(issuer);\n });\n inflight.set(issuer, p);\n return p;\n}\n\nfunction assertSafeIssuer(issuer: string): void {\n let url: URL;\n try {\n url = new URL(issuer);\n } catch {\n throw new Error(\n `[@orangecheck/auth-core] issuer must be an absolute URL, got ${issuer}`,\n );\n }\n // Allow http only for loopback / local dev. Any production-shaped\n // hostname (real DNS) is required to be https — JWKS fetched over\n // plaintext HTTP is trivially MITM-able and would defeat the whole\n // verification stack.\n const isLoopback =\n url.hostname === \"localhost\" ||\n url.hostname === \"127.0.0.1\" ||\n url.hostname === \"::1\";\n if (url.protocol !== \"https:\" && !isLoopback) {\n throw new Error(\n `[@orangecheck/auth-core] issuer must be https (got ${url.protocol}//${url.hostname}); plaintext JWKS is not safe`,\n );\n }\n}\n\nexport interface VerifyOcOptions {\n /** Auth host issuer. Defaults to https://ochk.io. */\n issuer?: string;\n /** JWKS cache TTL in ms. Defaults to 1 hour. Stale-on-error: if the\n * cache exists, verification still works during a transient outage. */\n jwksCacheTtlMs?: number;\n /** Accepted `aud` · see AudienceRule. An integrator verifying tokens from\n * the sign-in popup sets this to its own origin, e.g.\n * `\"https://example.com\"`, so a token issued to another site is refused. */\n audience?: AudienceRule;\n}\n\n/**\n * Verify a JWT issued by an OC auth host. Lazy-fetches the JWKS from\n * `<issuer>/.well-known/jwks.json`, picks the key whose `kid` matches\n * the token's protected header, and verifies the signature.\n *\n * Returns the payload on success, `null` on any failure (bad signature,\n * expired, wrong issuer, kid not in JWKS, malformed). Never throws.\n *\n * Integrators don't need to handle JWKs, env vars, or rotation — the\n * cache picks up new keys automatically when they appear in the JWKS\n * response. Stale tokens signed under a retired key continue to verify\n * as long as the retired key is still published in the JWKS (standard\n * key-rotation overlap window).\n */\nexport async function verifyOcToken(\n token: string,\n options: VerifyOcOptions = {},\n): Promise<SessionPayload | null> {\n const issuer = options.issuer ?? DEFAULT_ISSUER;\n const ttl = options.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;\n try {\n assertSafeIssuer(issuer);\n // Fast path: try the cache. If the kid isn't in the cache (e.g.\n // brand-new key just rotated in), fall through to a fresh fetch.\n let entry = await fetchJwks(issuer, ttl);\n const headerB64 = token.split(\".\")[0];\n if (!headerB64) return null;\n const headerJson =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(headerB64, \"base64url\").toString(\"utf8\")\n : new TextDecoder().decode(base64UrlDecode(headerB64));\n const header = JSON.parse(headerJson) as { kid?: string; alg?: string };\n if (!header.kid) return null;\n let key = entry.keys[header.kid];\n if (!key) {\n // Force a fresh JWKS fetch in case the integrator's cache is older\n // than the active key set on the auth host — but at most once a\n // minute per issuer, so an unknown kid cannot be used to drive\n // unbounded refetches (see lastForcedRefresh).\n const now = Date.now();\n const last = lastForcedRefresh.get(issuer) ?? 0;\n if (now - last >= FORCED_REFRESH_MIN_INTERVAL_MS) {\n lastForcedRefresh.set(issuer, now);\n jwksCache.delete(issuer);\n entry = await fetchJwks(issuer, ttl);\n key = entry.keys[header.kid];\n }\n }\n if (!key) return null;\n const res = await jwtVerify(token, key, {\n algorithms: [JWT_ALG],\n issuer,\n });\n const p = res.payload as SessionPayload;\n if (!p.sub || !p.did_oc || !p.jti) return null;\n if (!audienceSatisfied(p.aud, options.audience)) return null;\n return p;\n } catch {\n return null;\n }\n}\n\nexport interface SessionRequestHeaders {\n cookie?: string | null;\n authorization?: string | null;\n}\n\n/**\n * Verify the OC session for a request. Accepts either a plain object\n * with `cookie` / `authorization` properties (Express / Next.js / etc.)\n * or a Web Headers object (Hono / edge / Fetch API). Reads the session\n * from the Cookie header first; falls back to a `Authorization: Bearer\n * <token>` header so cross-domain integrators (different eTLD+1, no\n * .ochk.io cookie) can verify the same way.\n *\n * Returns `null` for unauthenticated, missing, or invalid requests.\n * Never throws.\n */\nexport async function getOcSession(\n headers: SessionRequestHeaders | Headers,\n options: VerifyOcOptions = {},\n): Promise<SessionPayload | null> {\n const cookie = isHeaders(headers)\n ? headers.get(\"cookie\")\n : (headers.cookie ?? null);\n const authorization = isHeaders(headers)\n ? headers.get(\"authorization\")\n : (headers.authorization ?? null);\n let token = readSessionCookie(cookie);\n if (!token && typeof authorization === \"string\") {\n const m = /^Bearer\\s+(.+)$/i.exec(authorization);\n if (m) token = m[1]!;\n }\n if (!token) return null;\n return verifyOcToken(token, options);\n}\n\nfunction isHeaders(value: unknown): value is Headers {\n return (\n typeof Headers !== \"undefined\" &&\n value !== null &&\n typeof value === \"object\" &&\n value instanceof Headers\n );\n}\n\n/**\n * Clear the module-level JWKS caches. Test-only — the caches are process-wide\n * by design (that is the point of them), which makes any test that touches\n * verification order-dependent without a reset.\n */\nexport function _resetJwksCachesForTests(): void {\n jwksCache.clear();\n inflight.clear();\n lastForcedRefresh.clear();\n}\n\nexport * from \"./revocation\";\n"]}
|
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";
|
|
@@ -300,6 +419,6 @@ function _resetJwksCachesForTests() {
|
|
|
300
419
|
lastForcedRefresh.clear();
|
|
301
420
|
}
|
|
302
421
|
|
|
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 };
|
|
422
|
+
export { DEFAULT_ISSUER, DISPLAY_IDENTITY_KINDS, JWT_ALG, SESSION_COOKIE, TAB_SESSION_HEADER, _resetFamilyRevocationCheckForTests, _resetJwksCachesForTests, audienceSatisfied, bondMeets, clearSessionCookieHeader, createRevocationCheck, familyRevocationCheck, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readBondClaim, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, revocationKey, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
|
304
423
|
//# sourceMappingURL=index.mjs.map
|
|
305
424
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAwBO,IAAM,OAAA,GAAU;AAChB,IAAM,cAAA,GAAiB;AACvB,IAAM,cAAA,GAAiB;AAUvB,IAAM,sBAAA,GAAyB,CAAC,KAAA,EAAO,KAAA,EAAO,SAAS,OAAO;AAgK9D,SAAS,cAAc,OAAA,EAA2C;AACvE,EAAA,MAAM,IAAI,OAAA,CAAQ,OAAA;AAClB,EAAA,IAAI,CAAC,KAAK,OAAO,CAAA,KAAM,YAAY,CAAA,CAAE,CAAA,KAAM,GAAG,OAAO,IAAA;AACrD,EAAA,MAAM,IAAA,GAAO,CAAC,CAAA,CAAE,UAAA,EAAY,EAAE,UAAA,EAAY,CAAA,CAAE,MAAA,EAAQ,CAAA,CAAE,UAAU,CAAA;AAChE,EAAA,IAAI,CAAC,IAAA,CAAK,KAAA,CAAM,CAAC,MAAM,OAAO,CAAA,KAAM,QAAA,IAAY,MAAA,CAAO,SAAS,CAAC,CAAA,IAAK,CAAA,IAAK,CAAC,GAAG,OAAO,IAAA;AACtF,EAAA,IAAI,OAAO,CAAA,CAAE,SAAA,KAAc,QAAA,IAAY,CAAC,iBAAiB,IAAA,CAAK,CAAA,CAAE,SAAS,CAAA,EAAG,OAAO,IAAA;AACnF,EAAA,OAAO,CAAA;AACT;AAQO,SAAS,SAAA,CACd,SACA,IAAA,EACS;AACT,EAAA,MAAM,CAAA,GAAI,cAAc,OAAO,CAAA;AAC/B,EAAA,IAAI,CAAC,GAAG,OAAO,KAAA;AACf,EAAA,IAAI,CAAA,CAAE,UAAA,IAAc,IAAA,CAAK,OAAA,IAAW,IAAI,OAAO,KAAA;AAC/C,EAAA,IAAI,CAAA,CAAE,UAAA,IAAc,IAAA,CAAK,OAAA,IAAW,IAAI,OAAO,KAAA;AAC/C,EAAA,IAAI,IAAA,CAAK,eAAe,MAAA,EAAW;AACjC,IAAA,MAAM,GAAA,GAAM,KAAK,KAAA,CAAM,IAAA,CAAK,KAAI,GAAI,GAAI,IAAI,CAAA,CAAE,UAAA;AAC9C,IAAA,IAAI,GAAA,GAAM,CAAA,IAAK,GAAA,GAAM,IAAA,CAAK,YAAY,OAAO,KAAA;AAAA,EAC/C;AACA,EAAA,OAAO,IAAA;AACT;AAoBO,SAAS,iBAAA,CACd,SACA,IAAA,EACS;AACT,EAAA,IAAI,OAAO,OAAA,CAAQ,UAAA,KAAe,QAAA,EAAU,OAAO,KAAA;AACnD,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,OAAA,CAAQ,UAAU,GAAG,OAAO,KAAA;AACjD,EAAA,IAAI,IAAA,CAAK,YAAA,IAAgB,CAAA,EAAG,OAAO,KAAA;AACnC,EAAA,MAAM,MAAA,GAAS,KAAK,KAAA,CAAM,IAAA,CAAK,KAAI,GAAI,GAAI,IAAI,OAAA,CAAQ,UAAA;AACvD,EAAA,OAAO,MAAA,IAAU,CAAA,IAAK,MAAA,GAAS,IAAA,CAAK,YAAA;AACtC;AAkBO,SAAS,eAAA,CACd,SACA,IAAA,EACS;AACT,EAAA,IAAI,OAAO,OAAA,CAAQ,OAAA,KAAY,QAAA,EAAU,OAAO,KAAA;AAChD,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,OAAA,CAAQ,OAAO,GAAG,OAAO,KAAA;AAC9C,EAAA,IAAI,IAAA,CAAK,YAAA,IAAgB,CAAA,EAAG,OAAO,KAAA;AACnC,EAAA,MAAM,MAAA,GAAS,KAAK,KAAA,CAAM,IAAA,CAAK,KAAI,GAAI,GAAI,IAAI,OAAA,CAAQ,OAAA;AACvD,EAAA,OAAO,MAAA,IAAU,CAAA,IAAK,MAAA,GAAS,IAAA,CAAK,YAAA;AACtC;AAeO,SAAS,uBACd,OAAA,EACiB;AACjB,EAAA,MAAM,QAAQ,OAAA,CAAQ,gBAAA;AACtB,EAAA,IACE,SACA,OAAO,KAAA,KAAU,QAAA,IACjB,OAAO,MAAM,KAAA,KAAU,QAAA,IACvB,KAAA,CAAM,KAAA,CAAM,SAAS,CAAA,IACpB,sBAAA,CAA6C,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA,EACjE;AACA,IAAA,OAAO,EAAE,IAAA,EAAM,KAAA,CAAM,IAAA,EAAM,KAAA,EAAO,MAAM,KAAA,EAAM;AAAA,EAChD;AACA,EAAA,OAAO,EAAE,IAAA,EAAM,KAAA,EAAO,KAAA,EAAO,QAAQ,MAAA,EAAO;AAC9C;AAkBO,SAAS,iBAAA,CAAkB,KAAc,IAAA,EAAyC;AACvF,EAAA,MAAM,IAAA,GAAO,GAAA,KAAQ,MAAA,GAAY,EAAC,GAAI,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA,GAAI,GAAA,GAAM,CAAC,GAAG,CAAA;AACrE,EAAA,IAAI,IAAA,KAAS,KAAK,OAAO,IAAA;AACzB,EAAA,IAAI,IAAA,KAAS,MAAA,EAAW,OAAO,IAAA,CAAK,MAAA,KAAW,CAAA;AAC/C,EAAA,MAAM,OAAO,OAAO,IAAA,KAAS,QAAA,GAAW,CAAC,IAAI,CAAA,GAAI,IAAA;AACjD,EAAA,OAAO,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA,KAAM,OAAO,MAAM,QAAA,IAAY,IAAA,CAAK,QAAA,CAAS,CAAC,CAAC,CAAA;AACnE;AAoBA,SAAS,UAAU,KAAA,EAAwC;AACzD,EAAA,MAAM,OACJ,OAAO,MAAA,KAAW,cACd,MAAA,CAAO,IAAA,CAAK,OAAO,WAAW,CAAA,CAAE,QAAA,CAAS,MAAM,IAC/C,IAAI,WAAA,GAAc,MAAA,CAAO,eAAA,CAAgB,KAAK,CAAC,CAAA;AACrD,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC9B,EAAA,IAAI,CAAC,MAAA,IAAU,OAAO,MAAA,KAAW,QAAA,EAAU;AACzC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,gBAAgB,KAAA,EAA2B;AAClD,EAAA,MAAM,GAAA,GAAM,KAAA,CAAM,MAAA,GAAS,CAAA,KAAM,CAAA,GAAI,EAAA,GAAK,GAAA,CAAI,MAAA,CAAO,CAAA,GAAK,KAAA,CAAM,MAAA,GAAS,CAAE,CAAA;AAC3E,EAAA,MAAM,GAAA,GAAA,CAAO,QAAQ,GAAA,EAAK,OAAA,CAAQ,MAAM,GAAG,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAA;AAC9D,EAAA,MAAM,MAAM,OAAO,IAAA,KAAS,WAAA,GAAc,IAAA,CAAK,GAAG,CAAA,GAAI,EAAA;AACtD,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,GAAA,CAAI,MAAM,CAAA;AACvC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,GAAA,CAAI,MAAA,EAAQ,CAAA,EAAA,EAAK,KAAA,CAAM,CAAC,CAAA,GAAI,GAAA,CAAI,UAAA,CAAW,CAAC,CAAA;AAChE,EAAA,OAAO,KAAA;AACT;AAEA,IAAI,gBAAA,GAA4C,IAAA;AAChD,IAAI,cAAA,GAAgC,IAAA;AAC7B,SAAS,cAAc,SAAA,EAAqC;AACjE,EAAA,IAAI,CAAC,gBAAA,IAAoB,cAAA,KAAmB,SAAA,EAAW;AACrD,IAAA,cAAA,GAAiB,SAAA;AACjB,IAAA,gBAAA,GAAmB,SAAA;AAAA,MACjB,UAAU,SAAS,CAAA;AAAA,MACnB;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,gBAAA;AACT;AAEA,IAAI,iBAAA,GAA6C,IAAA;AACjD,IAAI,eAAA,GAAiC,IAAA;AAC9B,SAAS,eAAe,UAAA,EAAsC;AACnE,EAAA,IAAI,CAAC,iBAAA,IAAqB,eAAA,KAAoB,UAAA,EAAY;AACxD,IAAA,eAAA,GAAkB,UAAA;AAClB,IAAA,iBAAA,GAAoB,SAAA;AAAA,MAClB,UAAU,UAAU,CAAA;AAAA,MACpB;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,iBAAA;AACT;AAGO,SAAS,eAAe,SAAA,EAA4C;AACzE,EAAA,OAAO,UAAU,SAAS,CAAA;AAC5B;AAWA,eAAsB,WAAA,CACpB,MAAA,EAiBA,GAAA,EACA,UAAA,EACiB;AACjB,EAAA,MAAM,MAAM,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,KAAQ,GAAI,CAAA;AAGxC,EAAA,MAAM,EAAE,GAAA,EAAK,GAAG,IAAA,EAAK,GAAI,MAAA;AACzB,EAAA,MAAM,GAAA,GAAM,IAAI,OAAA,CAAQ,IAAI,CAAA,CACzB,kBAAA,CAAmB,EAAE,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,KAAA,EAAO,GAAA,EAAK,GAAA,CAAI,KAAK,CAAA,CAC7D,SAAA,CAAU,GAAA,CAAI,MAAA,IAAU,cAAc,CAAA,CACtC,WAAA,CAAY,GAAG,CAAA,CACf,iBAAA,CAAkB,GAAA,GAAM,UAAU,CAAA;AACrC,EAAA,IAAI,GAAA,EAAK,GAAA,CAAI,WAAA,CAAY,GAAG,CAAA;AAC5B,EAAA,OAAO,IAAI,IAAA,CAAK,MAAM,cAAA,CAAe,GAAA,CAAI,UAAU,CAAC,CAAA;AACtD;AASA,eAAsB,kBAAA,CACpB,OACA,GAAA,EACgC;AAChC,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,KAAA,EAAO,MAAM,aAAA,CAAc,GAAA,CAAI,SAAS,CAAA,EAAG;AAAA,MACrE,UAAA,EAAY,CAAC,OAAO,CAAA;AAAA,MACpB,MAAA,EAAQ,IAAI,MAAA,IAAU;AAAA,KACvB,CAAA;AACD,IAAA,MAAM,IAAI,GAAA,CAAI,OAAA;AACd,IAAA,IAAI,CAAC,EAAE,GAAA,IAAO,CAAC,EAAE,MAAA,IAAU,CAAC,CAAA,CAAE,GAAA,EAAK,OAAO,IAAA;AAC1C,IAAA,IAAI,CAAC,iBAAA,CAAkB,CAAA,CAAE,KAAK,GAAA,CAAI,QAAQ,GAAG,OAAO,IAAA;AACpD,IAAA,OAAO,CAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAkBO,SAAS,sBAAA,CACd,KAAA,EACA,IAAA,GAAsB,EAAC,EACf;AACR,EAAA,MAAM,QAAkB,CAAC,CAAA,EAAG,cAAc,CAAA,CAAA,EAAI,KAAK,CAAA,CAAE,CAAA;AACrD,EAAA,KAAA,CAAM,IAAA,CAAK,CAAA,KAAA,EAAQ,IAAA,CAAK,IAAA,IAAQ,GAAG,CAAA,CAAE,CAAA;AACrC,EAAA,IAAI,IAAA,CAAK,QAAA,IAAY,IAAA,EAAM,KAAA,CAAM,KAAK,UAAU,CAAA;AAChD,EAAA,KAAA,CAAM,IAAA,CAAK,CAAA,SAAA,EAAY,IAAA,CAAK,QAAA,IAAY,KAAK,CAAA,CAAE,CAAA;AAC/C,EAAA,IAAI,IAAA,CAAK,MAAA,IAAU,IAAA,EAAM,KAAA,CAAM,KAAK,QAAQ,CAAA;AAC5C,EAAA,IAAI,KAAK,MAAA,EAAQ,KAAA,CAAM,KAAK,CAAA,OAAA,EAAU,IAAA,CAAK,MAAM,CAAA,CAAE,CAAA;AACnD,EAAA,IAAI,IAAA,CAAK,UAAU,IAAA,EAAM,KAAA,CAAM,KAAK,CAAA,QAAA,EAAW,IAAA,CAAK,MAAM,CAAA,CAAE,CAAA;AAC5D,EAAA,IAAI,IAAA,CAAK,SAAS,KAAA,CAAM,IAAA,CAAK,WAAW,IAAA,CAAK,OAAA,CAAQ,WAAA,EAAa,CAAA,CAAE,CAAA;AACpE,EAAA,OAAO,KAAA,CAAM,KAAK,IAAI,CAAA;AACxB;AAGO,SAAS,wBAAA,CACd,IAAA,GAA0D,EAAC,EACnD;AACR,EAAA,OAAO,uBAAuB,EAAA,EAAI;AAAA,IAChC,GAAG,IAAA;AAAA,IACH,MAAA,EAAQ,CAAA;AAAA,IACR,OAAA,kBAAS,IAAI,IAAA,CAAK,CAAC;AAAA,GACpB,CAAA;AACH;AAMO,SAAS,kBACd,YAAA,EACe;AACf,EAAA,IAAI,CAAC,cAAc,OAAO,IAAA;AAC1B,EAAA,MAAM,MAAA,GAAS,GAAG,cAAc,CAAA,CAAA,CAAA;AAChC,EAAA,KAAA,MAAW,IAAA,IAAQ,YAAA,CAAa,KAAA,CAAM,GAAG,CAAA,EAAG;AAC1C,IAAA,MAAM,OAAA,GAAU,KAAK,IAAA,EAAK;AAC1B,IAAA,IAAI,OAAA,CAAQ,UAAA,CAAW,MAAM,CAAA,EAAG;AAC9B,MAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,KAAA,CAAM,MAAA,CAAO,MAAM,CAAA;AACvC,MAAA,OAAO,GAAA,CAAI,MAAA,GAAS,CAAA,GAAI,GAAA,GAAM,IAAA;AAAA,IAChC;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT;AAQO,SAAS,sBACd,YAAA,EACU;AACV,EAAA,IAAI,CAAC,YAAA,EAAc,OAAO,EAAC;AAC3B,EAAA,MAAM,MAAA,GAAS,GAAG,cAAc,CAAA,CAAA,CAAA;AAChC,EAAA,MAAM,MAAgB,EAAC;AACvB,EAAA,KAAA,MAAW,IAAA,IAAQ,YAAA,CAAa,KAAA,CAAM,GAAG,CAAA,EAAG;AAC1C,IAAA,MAAM,OAAA,GAAU,KAAK,IAAA,EAAK;AAC1B,IAAA,IAAI,OAAA,CAAQ,UAAA,CAAW,MAAM,CAAA,EAAG;AAC9B,MAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,KAAA,CAAM,MAAA,CAAO,MAAM,CAAA;AACvC,MAAA,IAAI,GAAA,CAAI,MAAA,GAAS,CAAA,EAAG,GAAA,CAAI,KAAK,GAAG,CAAA;AAAA,IAClC;AAAA,EACF;AACA,EAAA,OAAO,GAAA;AACT;AAcO,IAAM,kBAAA,GAAqB;AAUlC,SAAS,WAAA,CACP,SACA,IAAA,EACe;AACf,EAAA,IAAI,UAAU,OAAO,CAAA,EAAG,OAAO,OAAA,CAAQ,IAAI,IAAI,CAAA;AAC/C,EAAA,MAAM,MAAM,OAAA,CAAQ,IAAI,KAAK,OAAA,CAAQ,IAAA,CAAK,aAAa,CAAA;AACvD,EAAA,IAAI,MAAM,OAAA,CAAQ,GAAG,GAAG,OAAO,GAAA,CAAI,CAAC,CAAA,IAAK,IAAA;AACzC,EAAA,OAAO,OAAO,GAAA,KAAQ,QAAA,GAAW,GAAA,GAAM,IAAA;AACzC;AAmBA,eAAsB,yBAAA,CACpB,SACA,GAAA,EAC+B;AAC/B,EAAA,MAAM,QAAA,GAAW,WAAA,CAAY,OAAA,EAAS,kBAAkB,CAAA;AACxD,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,MAAM,OAAA,GAAU,MAAM,kBAAA,CAAmB,QAAA,EAAU,GAAG,CAAA;AACtD,IAAA,IAAI,SAAS,OAAO,EAAE,IAAI,IAAA,EAAM,OAAA,EAAS,KAAK,KAAA,EAAM;AACpD,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,aAAA,EAAc;AAAA,EAC5C;AACA,EAAA,MAAM,YAAA,GAAe,WAAA,CAAY,OAAA,EAAS,QAAQ,CAAA;AAClD,EAAA,KAAA,MAAW,KAAA,IAAS,qBAAA,CAAsB,YAAY,CAAA,EAAG;AACvD,IAAA,MAAM,OAAA,GAAU,MAAM,kBAAA,CAAmB,KAAA,EAAO,GAAG,CAAA;AACnD,IAAA,IAAI,SAAS,OAAO,EAAE,IAAI,IAAA,EAAM,OAAA,EAAS,KAAK,QAAA,EAAS;AAAA,EACzD;AACA,EAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,YAAA,EAAa;AAC3C;AAWA,IAAM,mBAAA,GAAsB,KAAK,EAAA,GAAK,GAAA;AAOtC,IAAM,SAAA,uBAAgB,GAAA,EAA4B;AAClD,IAAM,QAAA,uBAAe,GAAA,EAAqC;AAgB1D,IAAM,iBAAA,uBAAwB,GAAA,EAAoB;AAClD,IAAM,8BAAA,GAAiC,GAAA;AAMvC,eAAe,SAAA,CACb,QACA,KAAA,EACyB;AACzB,EAAA,MAAM,MAAA,GAAS,SAAA,CAAU,GAAA,CAAI,MAAM,CAAA;AACnC,EAAA,IAAI,UAAU,IAAA,CAAK,GAAA,KAAQ,MAAA,CAAO,SAAA,GAAY,OAAO,OAAO,MAAA;AAC5D,EAAA,MAAM,QAAA,GAAW,QAAA,CAAS,GAAA,CAAI,MAAM,CAAA;AACpC,EAAA,IAAI,UAAU,OAAO,QAAA;AACrB,EAAA,MAAM,KAAK,YAAqC;AAC9C,IAAA,IAAI;AACF,MAAA,MAAM,MAAM,CAAA,EAAG,MAAA,CAAO,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAC,CAAA,sBAAA,CAAA;AACxC,MAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,GAAA,EAAK,EAAE,SAAS,EAAE,MAAA,EAAQ,kBAAA,EAAmB,EAAG,CAAA;AACxE,MAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAA,oCAAA,EAAuC,GAAG,CAAA,UAAA,EAAa,GAAA,CAAI,MAAM,CAAA;AAAA,SACnE;AAAA,MACF;AACA,MAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAC7B,MAAA,IAAI,CAAC,IAAA,IAAQ,CAAC,MAAM,OAAA,CAAQ,IAAA,CAAK,IAAI,CAAA,EAAG;AACtC,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,oCAAoC,GAAG,CAAA,+BAAA;AAAA,SACzC;AAAA,MACF;AACA,MAAA,MAAM,OAAgC,EAAC;AACvC,MAAA,KAAA,MAAW,CAAA,IAAK,KAAK,IAAA,EAAM;AACzB,QAAA,IAAI,OAAO,CAAA,CAAE,GAAA,KAAQ,QAAA,EAAU;AAC/B,QAAA,IAAI;AACF,UAAA,IAAA,CAAK,EAAE,GAAG,CAAA,GAAK,MAAM,SAAA,CAAU,GAAG,OAAO,CAAA;AAAA,QAC3C,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF;AACA,MAAA,MAAM,QAAwB,EAAE,IAAA,EAAM,SAAA,EAAW,IAAA,CAAK,KAAI,EAAE;AAC5D,MAAA,SAAA,CAAU,GAAA,CAAI,QAAQ,KAAK,CAAA;AAC3B,MAAA,OAAO,KAAA;AAAA,IACT,SAAS,GAAA,EAAK;AAKZ,MAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,GAAA,CAAI,MAAM,CAAA;AAClC,MAAA,IAAI,OAAO,OAAO,KAAA;AAClB,MAAA,MAAM,GAAA;AAAA,IACR;AAAA,EACF,CAAA,GAAG,CAAE,OAAA,CAAQ,MAAM;AACjB,IAAA,QAAA,CAAS,OAAO,MAAM,CAAA;AAAA,EACxB,CAAC,CAAA;AACD,EAAA,QAAA,CAAS,GAAA,CAAI,QAAQ,CAAC,CAAA;AACtB,EAAA,OAAO,CAAA;AACT;AAEA,SAAS,iBAAiB,MAAA,EAAsB;AAC9C,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI;AACF,IAAA,GAAA,GAAM,IAAI,IAAI,MAAM,CAAA;AAAA,EACtB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,gEAAgE,MAAM,CAAA;AAAA,KACxE;AAAA,EACF;AAKA,EAAA,MAAM,UAAA,GACJ,IAAI,QAAA,KAAa,WAAA,IACjB,IAAI,QAAA,KAAa,WAAA,IACjB,IAAI,QAAA,KAAa,KAAA;AACnB,EAAA,IAAI,GAAA,CAAI,QAAA,KAAa,QAAA,IAAY,CAAC,UAAA,EAAY;AAC5C,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,mDAAA,EAAsD,GAAA,CAAI,QAAQ,CAAA,EAAA,EAAK,IAAI,QAAQ,CAAA,6BAAA;AAAA,KACrF;AAAA,EACF;AACF;AA4BA,eAAsB,aAAA,CACpB,KAAA,EACA,OAAA,GAA2B,EAAC,EACI;AAChC,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,cAAA;AACjC,EAAA,MAAM,GAAA,GAAM,QAAQ,cAAA,IAAkB,mBAAA;AACtC,EAAA,IAAI;AACF,IAAA,gBAAA,CAAiB,MAAM,CAAA;AAGvB,IAAA,IAAI,KAAA,GAAQ,MAAM,SAAA,CAAU,MAAA,EAAQ,GAAG,CAAA;AACvC,IAAA,MAAM,SAAA,GAAY,KAAA,CAAM,KAAA,CAAM,GAAG,EAAE,CAAC,CAAA;AACpC,IAAA,IAAI,CAAC,WAAW,OAAO,IAAA;AACvB,IAAA,MAAM,aACJ,OAAO,MAAA,KAAW,cACd,MAAA,CAAO,IAAA,CAAK,WAAW,WAAW,CAAA,CAAE,QAAA,CAAS,MAAM,IACnD,IAAI,WAAA,GAAc,MAAA,CAAO,eAAA,CAAgB,SAAS,CAAC,CAAA;AACzD,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,UAAU,CAAA;AACpC,IAAA,IAAI,CAAC,MAAA,CAAO,GAAA,EAAK,OAAO,IAAA;AACxB,IAAA,IAAI,GAAA,GAAM,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,GAAG,CAAA;AAC/B,IAAA,IAAI,CAAC,GAAA,EAAK;AAKR,MAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,MAAA,MAAM,IAAA,GAAO,iBAAA,CAAkB,GAAA,CAAI,MAAM,CAAA,IAAK,CAAA;AAC9C,MAAA,IAAI,GAAA,GAAM,QAAQ,8BAAA,EAAgC;AAChD,QAAA,iBAAA,CAAkB,GAAA,CAAI,QAAQ,GAAG,CAAA;AACjC,QAAA,SAAA,CAAU,OAAO,MAAM,CAAA;AACvB,QAAA,KAAA,GAAQ,MAAM,SAAA,CAAU,MAAA,EAAQ,GAAG,CAAA;AACnC,QAAA,GAAA,GAAM,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,GAAG,CAAA;AAAA,MAC7B;AAAA,IACF;AACA,IAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AACjB,IAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,KAAA,EAAO,GAAA,EAAK;AAAA,MACtC,UAAA,EAAY,CAAC,OAAO,CAAA;AAAA,MACpB;AAAA,KACD,CAAA;AACD,IAAA,MAAM,IAAI,GAAA,CAAI,OAAA;AACd,IAAA,IAAI,CAAC,EAAE,GAAA,IAAO,CAAC,EAAE,MAAA,IAAU,CAAC,CAAA,CAAE,GAAA,EAAK,OAAO,IAAA;AAC1C,IAAA,IAAI,CAAC,iBAAA,CAAkB,CAAA,CAAE,KAAK,OAAA,CAAQ,QAAQ,GAAG,OAAO,IAAA;AACxD,IAAA,OAAO,CAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAkBA,eAAsB,YAAA,CACpB,OAAA,EACA,OAAA,GAA2B,EAAC,EACI;AAChC,EAAA,MAAM,MAAA,GAAS,UAAU,OAAO,CAAA,GAC5B,QAAQ,GAAA,CAAI,QAAQ,CAAA,GACnB,OAAA,CAAQ,MAAA,IAAU,IAAA;AACvB,EAAA,MAAM,aAAA,GAAgB,UAAU,OAAO,CAAA,GACnC,QAAQ,GAAA,CAAI,eAAe,CAAA,GAC1B,OAAA,CAAQ,aAAA,IAAiB,IAAA;AAC9B,EAAA,IAAI,KAAA,GAAQ,kBAAkB,MAAM,CAAA;AACpC,EAAA,IAAI,CAAC,KAAA,IAAS,OAAO,aAAA,KAAkB,QAAA,EAAU;AAC/C,IAAA,MAAM,CAAA,GAAI,kBAAA,CAAmB,IAAA,CAAK,aAAa,CAAA;AAC/C,IAAA,IAAI,CAAA,EAAG,KAAA,GAAQ,CAAA,CAAE,CAAC,CAAA;AAAA,EACpB;AACA,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,EAAA,OAAO,aAAA,CAAc,OAAO,OAAO,CAAA;AACrC;AAEA,SAAS,UAAU,KAAA,EAAkC;AACnD,EAAA,OACE,OAAO,YAAY,WAAA,IACnB,KAAA,KAAU,QACV,OAAO,KAAA,KAAU,YACjB,KAAA,YAAiB,OAAA;AAErB;AAOO,SAAS,wBAAA,GAAiC;AAC/C,EAAA,SAAA,CAAU,KAAA,EAAM;AAChB,EAAA,QAAA,CAAS,KAAA,EAAM;AACf,EAAA,iBAAA,CAAkB,KAAA,EAAM;AAC1B","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 * Slug of the federation this user is bound to (their \"home\"\n * federation). Multi-federation routing reads this; v1 has one live\n * federation so it's set on first signin and rarely changes. Absent\n * on tokens minted before this field shipped, and on accounts that\n * have never bound to a federation. Consumers fall back to the\n * directory default at /api/federations when absent.\n */\n home_federation?: string | null;\n /**\n * Discriminator recording where this user is on the custody-state\n * graph. Three legitimate states:\n *\n * - 'fedimint_threshold' — federation custody via the OC-introduced\n * federation (default for email-OTP signups; OC's threshold\n * guardians hold key shares).\n * - 'fedimint_client' — user moved their balance to a Fedimint\n * federation they picked themselves (still federation custody,\n * but on a federation OC didn't introduce).\n * - 'bip322' — full self-custody. The user signs with\n * their own Bitcoin key. OC has zero authority over their funds.\n *\n * Graduation is the product thesis. This field is the structural\n * primitive that records progress along the custody-state graph;\n * transitions are recorded as anchored \"rebind\" envelopes. Absent\n * on tokens minted before the field shipped — consumers should\n * treat undefined as 'fedimint_threshold' for email-OTP identities\n * and 'bip322' for BIP-322 identities (the default-by-construction\n * mapping).\n */\n signing_method?: \"fedimint_threshold\" | \"fedimint_client\" | \"bip322\" | null;\n /**\n * Historical did_ocs consolidated INTO this account via the\n * /api/auth/link/btc transfer-under-dual-proof flow. Consumers that\n * aggregate per-user data should union queries across\n * `[did_oc, ...merged_from]`. Absent / empty means \"no absorbed\n * accounts.\" Order is not significant.\n */\n merged_from?: string[];\n /**\n * Unix seconds when the user last successfully completed a WebAuthn\n * step-up assertion on the auth host. Consumer subdomains gate\n * sensitive actions on `(now - step_up_at) < their_window` —\n * typically 5 minutes. Absent on JWTs minted before WebAuthn\n * shipped, or on tokens minted by ordinary signin (only set by\n * /api/auth/webauthn/assertion/verify on success). Use\n * `verifyStepUpClaim()` to read it safely.\n */\n step_up_at?: number;\n /**\n * Unix seconds when the user last successfully completed an inline\n * sudo-mode re-authentication on the auth host (email-OTP for\n * email-primary identities, BIP-322 challenge for btc-primary\n * identities). Gates auth-graph-mutating operations — register an\n * additional WebAuthn credential, revoke one, link a new identity,\n * change recovery method — without forcing a full sign-out/sign-in.\n * Same shape and freshness window posture as `step_up_at`; the two\n * are independent claims (sudo proves \"you typed your password\n * again,\" step-up proves \"you tapped your hardware key\"). Read it\n * with `verifySudoClaim()`.\n */\n sudo_at?: number;\n /**\n * Optional best-effort owner flag · set by the auth host at signin\n * time when the new did_oc matches OWNER_OC_ADDRESSES. Surfaced\n * through to clients via `useOcSession().account.isOwner` so the\n * family-switcher and other low-stakes UX can render owner-only\n * affordances (e.g. an analytics.ochk.io entry).\n *\n * **NOT A SECURITY BOUNDARY.** Sensitive surfaces still re-check\n * the live `OWNER_OC_ADDRESSES` env against `session.did_oc` on\n * every request (analytics.ochk.io's requireOwner does this).\n * If an owner is removed from the env, their JWT may still carry\n * `is_owner: true` for up to 30 days — the UX hint goes stale,\n * but every gated action they attempt re-fails server-side.\n *\n * Treat absence as `false`. Set only on first signin or re-issue;\n * never mutated mid-session.\n */\n is_owner?: boolean;\n /**\n * The identity the user has chosen to show in their account badge\n * — the collapsed label every family site (and any integrator)\n * renders instead of the raw `did:oc`. Baked into the JWT so the\n * choice is consistent across every `.ochk.io` subdomain with no\n * round-trip, the same posture as `name` / `npub`.\n *\n * Re-resolved by the auth host on every mint: an explicit user\n * promotion wins; absent that, it defaults to the account's\n * sign-in identity (BIP-322 accounts → their Bitcoin address,\n * federation accounts → their email). Absent / null on tokens\n * minted before this field shipped — consumers fall back to the\n * `did_oc`. Read it safely with `resolveDisplayIdentity()`.\n */\n display_identity?: DisplayIdentity | null;\n /**\n * Present when the account is backed by a bond. Absent means no bond, not\n * a failed check. Read it with `readBondClaim()` / `bondMeets()`.\n */\n oc_bond?: BondClaim;\n}\n\n/** The token's bond claim when it is well-formed, else null. */\nexport function readBondClaim(payload: SessionPayload): BondClaim | null {\n const b = payload.oc_bond as Partial<BondClaim> | undefined;\n if (!b || typeof b !== \"object\" || b.v !== 1) return null;\n const nums = [b.sats_floor, b.days_floor, b.height, b.checked_at];\n if (!nums.every((n) => typeof n === \"number\" && Number.isFinite(n) && n >= 0)) return null;\n if (typeof b.nullifier !== \"string\" || !/^[0-9a-f]{64}$/.test(b.nullifier)) return null;\n return b as BondClaim;\n}\n\n/**\n * True when the account is backed by a coin whose floors reach `minSats`\n * and `minDays`, and the issuer checked it within\n * `maxAgeSecs` (default: no limit). Compare against the floor ladder: a\n * `minSats` of 50000 is met by a `sats_floor` of 100000, not 10000.\n */\nexport function bondMeets(\n payload: SessionPayload,\n need: { minSats?: number; minDays?: number; maxAgeSecs?: number },\n): boolean {\n const b = readBondClaim(payload);\n if (!b) return false;\n if (b.sats_floor < (need.minSats ?? 0)) return false;\n if (b.days_floor < (need.minDays ?? 0)) return false;\n if (need.maxAgeSecs !== undefined) {\n const age = Math.floor(Date.now() / 1000) - b.checked_at;\n if (age < 0 || age > need.maxAgeSecs) return false;\n }\n return true;\n}\n\n/**\n * Returns true when the JWT carries a fresh `step_up_at` claim —\n * i.e. the user successfully completed a WebAuthn assertion within\n * the supplied freshness window (in seconds). Returns false for\n * tokens that lack the claim entirely, that carry a stale value,\n * that carry a value somehow in the future, or where the freshness\n * window is non-positive.\n *\n * Consumers gating sensitive actions read this in two places:\n *\n * - Client side · before calling the action, to decide whether to\n * trigger `useStepUpAuth()`. Skip the prompt if already fresh.\n * - Server side · in the route handler, AFTER verifying the JWT,\n * to enforce the policy regardless of what the client did.\n *\n * The freshness window is consumer-chosen — typical default is 300s\n * (5 min); higher-value spends might use 60s.\n */\nexport function verifyStepUpClaim(\n payload: SessionPayload,\n opts: { max_age_secs: number },\n): boolean {\n if (typeof payload.step_up_at !== \"number\") return false;\n if (!Number.isFinite(payload.step_up_at)) return false;\n if (opts.max_age_secs <= 0) return false;\n const ageSec = Math.floor(Date.now() / 1000) - payload.step_up_at;\n return ageSec >= 0 && ageSec < opts.max_age_secs;\n}\n\n/**\n * Returns true when the JWT carries a fresh `sudo_at` claim — i.e. the\n * user successfully re-authenticated inline (email-OTP or BIP-322\n * challenge) within the supplied freshness window. Independent of\n * `step_up_at` · sudo is \"you proved your primary identity again,\"\n * step-up is \"you tapped your hardware key.\"\n *\n * Returns false for tokens that lack the claim, that carry a stale\n * value, that carry a future-dated value (clock skew or malicious\n * mint), or where the freshness window is non-positive.\n *\n * Use this to gate auth-graph-mutating operations on the auth host —\n * adding a second hardware key, linking a new identity, generating\n * recovery codes, changing recovery method. Default window is 5\n * minutes; tighten per-operation as needed.\n */\nexport function verifySudoClaim(\n payload: SessionPayload,\n opts: { max_age_secs: number },\n): boolean {\n if (typeof payload.sudo_at !== \"number\") return false;\n if (!Number.isFinite(payload.sudo_at)) return false;\n if (opts.max_age_secs <= 0) return false;\n const ageSec = Math.floor(Date.now() / 1000) - payload.sudo_at;\n return ageSec >= 0 && ageSec < opts.max_age_secs;\n}\n\n/**\n * Resolve the account-badge identity for a session — total, never\n * throws, always returns a renderable `{ kind, value }`.\n *\n * Returns the JWT's `display_identity` claim when it is present and\n * well-formed; otherwise falls back to the canonical `did:oc`. Use\n * this everywhere you render the badge label rather than reading the\n * raw claim, so tokens minted before the field shipped (and tokens\n * carrying a malformed claim) degrade gracefully to the did.\n *\n * This is the single resolver both `<OcAccountMenu>` and integrators\n * building their own chip should call.\n */\nexport function resolveDisplayIdentity(\n payload: SessionPayload,\n): DisplayIdentity {\n const claim = payload.display_identity;\n if (\n claim &&\n typeof claim === \"object\" &&\n typeof claim.value === \"string\" &&\n claim.value.length > 0 &&\n (DISPLAY_IDENTITY_KINDS as readonly string[]).includes(claim.kind)\n ) {\n return { kind: claim.kind, value: claim.value };\n }\n return { kind: \"did\", value: payload.did_oc };\n}\n\n/**\n * Which audience a verifier accepts.\n *\n * omitted → a FAMILY session only: the token must carry no `aud`.\n * a string → a token bound to that audience (or one of several).\n * \"*\" → either. For the routes built to receive tokens that\n * integrators forward.\n *\n * Omitted is the strict default: a token bound to a relying party is that\n * party's credential and never a family sign-in. Tokens minted before\n * audiences existed carry no `aud`, so every existing session keeps\n * verifying exactly as before.\n */\nexport type AudienceRule = string | readonly string[] | \"*\";\n\n/** Pure · does the payload's `aud` satisfy the rule? */\nexport function audienceSatisfied(aud: unknown, rule: AudienceRule | undefined): boolean {\n const have = aud === undefined ? [] : Array.isArray(aud) ? aud : [aud];\n if (rule === \"*\") return true;\n if (rule === undefined) return have.length === 0;\n const want = typeof rule === \"string\" ? [rule] : rule;\n return have.some((a) => typeof a === \"string\" && want.includes(a));\n}\n\nexport interface VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 public key. */\n publicJwk: string;\n /** Expected `iss` claim. Tokens with a different issuer are rejected. */\n issuer?: string;\n /** Accepted `aud` · see AudienceRule. Omit for family sessions. */\n audience?: AudienceRule;\n}\n\nexport interface SignConfig extends VerifyConfig {\n /** Base64url-encoded JWK containing the Ed25519 private key. */\n privateJwk: string;\n /** Short key id placed in the JWT header (`kid`). Must match the JWK. */\n kid: string;\n}\n\n// ─── JWK helpers ────────────────────────────────────────────────────────\n\nfunction decodeJwk(value: string): Record<string, unknown> {\n const json =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(value, \"base64url\").toString(\"utf8\")\n : new TextDecoder().decode(base64UrlDecode(value));\n const parsed = JSON.parse(json) as Record<string, unknown>;\n if (!parsed || typeof parsed !== \"object\") {\n throw new Error(\n \"[@orangecheck/auth-core] JWK env did not decode to an object\",\n );\n }\n return parsed;\n}\n\nfunction base64UrlDecode(input: string): Uint8Array {\n const pad = input.length % 4 === 0 ? \"\" : \"=\".repeat(4 - (input.length % 4));\n const b64 = (input + pad).replace(/-/g, \"+\").replace(/_/g, \"/\");\n const bin = typeof atob !== \"undefined\" ? atob(b64) : \"\";\n const bytes = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);\n return bytes;\n}\n\nlet publicKeyPromise: Promise<AuthKey> | null = null;\nlet publicKeyCache: string | null = null;\nexport function loadPublicKey(publicJwk: string): Promise<AuthKey> {\n if (!publicKeyPromise || publicKeyCache !== publicJwk) {\n publicKeyCache = publicJwk;\n publicKeyPromise = importJWK(\n decodeJwk(publicJwk),\n JWT_ALG,\n ) as Promise<AuthKey>;\n }\n return publicKeyPromise;\n}\n\nlet privateKeyPromise: Promise<AuthKey> | null = null;\nlet privateKeyCache: string | null = null;\nexport function loadPrivateKey(privateJwk: string): Promise<AuthKey> {\n if (!privateKeyPromise || privateKeyCache !== privateJwk) {\n privateKeyCache = privateJwk;\n privateKeyPromise = importJWK(\n decodeJwk(privateJwk),\n JWT_ALG,\n ) as Promise<AuthKey>;\n }\n return privateKeyPromise;\n}\n\n/** Parse the `OC_AUTH_PUBLIC_JWK` env var back into a JWK object. */\nexport function parsePublicJwk(publicJwk: string): Record<string, unknown> {\n return decodeJwk(publicJwk);\n}\n\n// ─── Sign / verify ──────────────────────────────────────────────────────\n\n/**\n * Mint a fresh oc_session JWT. Only called on the auth host (the only\n * place that holds the private key).\n *\n * The caller owns recording an ended token's `jti` for the revocation\n * feed that {@link createRevocationCheck} reads.\n */\nexport async function signSession(\n claims: {\n sub: string;\n did_oc: string;\n jti: string;\n name?: string | null;\n npub?: string | null;\n home_federation?: string | null;\n signing_method?: \"fedimint_threshold\" | \"fedimint_client\" | \"bip322\" | null;\n merged_from?: string[];\n step_up_at?: number;\n sudo_at?: number;\n display_identity?: DisplayIdentity | null;\n /** Bind the token to one relying party (e.g. `https://example.com`).\n * Omit for a family session. A bound token verifies only where that\n * audience is expected — never as a family sign-in. */\n aud?: string;\n },\n cfg: SignConfig,\n ttlSeconds: number,\n): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n // did_oc is the sole user identifier. The legacy `addr` shim that\n // mirrored did_oc was dropped in v2.0.0 along with the type field.\n const { aud, ...rest } = claims;\n const jwt = new SignJWT(rest)\n .setProtectedHeader({ alg: JWT_ALG, typ: \"JWT\", kid: cfg.kid })\n .setIssuer(cfg.issuer ?? DEFAULT_ISSUER)\n .setIssuedAt(now)\n .setExpirationTime(now + ttlSeconds);\n if (aud) jwt.setAudience(aud);\n return jwt.sign(await loadPrivateKey(cfg.privateJwk));\n}\n\n/**\n * Crypto-only JWT verify. Returns the payload on success, `null` on\n * any failure (bad signature, expired, wrong issuer, malformed).\n *\n * Safe for consumer subdomains — they only need the public JWK. A signed-out\n * token still verifies here; pass the payload to\n * {@link familyRevocationCheck} to refuse it.\n */\nexport async function verifySessionToken(\n token: string,\n cfg: VerifyConfig,\n): Promise<SessionPayload | null> {\n try {\n const res = await jwtVerify(token, await loadPublicKey(cfg.publicJwk), {\n algorithms: [JWT_ALG],\n issuer: cfg.issuer ?? DEFAULT_ISSUER,\n });\n const p = res.payload as SessionPayload;\n if (!p.sub || !p.did_oc || !p.jti) return null;\n if (!audienceSatisfied(p.aud, cfg.audience)) return null;\n return p;\n } catch {\n return null;\n }\n}\n\n// ─── Cookie helpers ─────────────────────────────────────────────────────\n\nexport interface CookieOptions {\n domain?: string;\n maxAge?: number;\n expires?: Date;\n path?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: \"Lax\" | \"Strict\" | \"None\";\n}\n\n/**\n * Serialize a cookie header value for `Set-Cookie`. Defaults match the\n * ochk.io session shape: HttpOnly, SameSite=Lax, Path=/, Secure in prod.\n */\nexport function serializeSessionCookie(\n token: string,\n opts: CookieOptions = {},\n): string {\n const parts: string[] = [`${SESSION_COOKIE}=${token}`];\n parts.push(`Path=${opts.path ?? \"/\"}`);\n if (opts.httpOnly ?? true) parts.push(\"HttpOnly\");\n parts.push(`SameSite=${opts.sameSite ?? \"Lax\"}`);\n if (opts.secure ?? true) parts.push(\"Secure\");\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n if (opts.maxAge != null) parts.push(`Max-Age=${opts.maxAge}`);\n if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);\n return parts.join(\"; \");\n}\n\n/** Clear the session cookie. Must match the Domain used when it was set. */\nexport function clearSessionCookieHeader(\n opts: Pick<CookieOptions, \"domain\" | \"path\" | \"secure\"> = {},\n): string {\n return serializeSessionCookie(\"\", {\n ...opts,\n maxAge: 0,\n expires: new Date(0),\n });\n}\n\n/**\n * Read the oc_session token out of a raw `Cookie:` header string.\n * Returns `null` if the cookie is missing or empty.\n */\nexport function readSessionCookie(\n cookieHeader: string | null | undefined,\n): string | null {\n if (!cookieHeader) return null;\n const prefix = `${SESSION_COOKIE}=`;\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim();\n if (trimmed.startsWith(prefix)) {\n const val = trimmed.slice(prefix.length);\n return val.length > 0 ? val : null;\n }\n }\n return null;\n}\n\n/**\n * Read EVERY oc_session value out of a raw `Cookie:` header string.\n * Multiple same-name cookies are legitimate (e.g. a stale host-scoped\n * cookie shadowing the `Domain=.ochk.io` one) — verification should\n * try each rather than trust ordering.\n */\nexport function readAllSessionCookies(\n cookieHeader: string | null | undefined,\n): string[] {\n if (!cookieHeader) return [];\n const prefix = `${SESSION_COOKIE}=`;\n const out: string[] = [];\n for (const part of cookieHeader.split(\";\")) {\n const trimmed = part.trim();\n if (trimmed.startsWith(prefix)) {\n const val = trimmed.slice(prefix.length);\n if (val.length > 0) out.push(val);\n }\n }\n return out;\n}\n\n// ─── Per-tab session resolution (multi-account · tab pinning) ───────────\n//\n// One browser holds ONE shared `oc_session` cookie (the browser-wide\n// default account) but may be signed into N roster accounts, each with\n// its own concurrently-valid session JWT. A tab \"pins\" itself to one\n// account by holding that account's JWT in sessionStorage and sending\n// it on every API call as the `x-oc-tab-session` header. The header\n// value is a full session JWT — a *credential*, never a bare selector —\n// so servers verify it exactly like the cookie token and stay\n// stateless.\n\n/** Header carrying a tab-pinned session JWT. Lowercase (Node folds headers). */\nexport const TAB_SESSION_HEADER = \"x-oc-tab-session\" as const;\n\nexport type ResolveSessionResult =\n | { ok: true; payload: SessionPayload; via: \"tab\" | \"cookie\" }\n | { ok: false; reason: \"tab_invalid\" | \"no_session\" };\n\n/** Header bag shapes accepted by {@link resolveSessionFromRequest}. */\nexport type IncomingRequestHeaders =\n Headers | Record<string, string | string[] | undefined>;\n\nfunction headerValue(\n headers: IncomingRequestHeaders,\n name: string,\n): string | null {\n if (isHeaders(headers)) return headers.get(name);\n const raw = headers[name] ?? headers[name.toLowerCase()];\n if (Array.isArray(raw)) return raw[0] ?? null;\n return typeof raw === \"string\" ? raw : null;\n}\n\n/**\n * Resolve the EFFECTIVE session for a request — the per-tab choke\n * point every consumer's `readJwtSession` should delegate to.\n *\n * Precedence:\n * 1. `x-oc-tab-session` header, when present. **Fail-closed**: a\n * present-but-invalid tab token resolves to\n * `{ ok: false, reason: 'tab_invalid' }` rather than silently\n * falling back to the cookie — falling back would execute the\n * request as a DIFFERENT account than the tab is displaying,\n * which is precisely the bug per-tab pinning exists to prevent.\n * Clients clear their pin on 401 and re-resolve.\n * 2. Every `oc_session` cookie in the jar, first one that verifies.\n *\n * Crypto-only (signature + exp + iss via {@link verifySessionToken}); a\n * signed-out token still resolves, so pass the payload to\n * {@link familyRevocationCheck}. Never throws.\n */\nexport async function resolveSessionFromRequest(\n headers: IncomingRequestHeaders,\n cfg: VerifyConfig,\n): Promise<ResolveSessionResult> {\n const tabToken = headerValue(headers, TAB_SESSION_HEADER);\n if (tabToken) {\n const payload = await verifySessionToken(tabToken, cfg);\n if (payload) return { ok: true, payload, via: \"tab\" };\n return { ok: false, reason: \"tab_invalid\" };\n }\n const cookieHeader = headerValue(headers, \"cookie\");\n for (const token of readAllSessionCookies(cookieHeader)) {\n const payload = await verifySessionToken(token, cfg);\n if (payload) return { ok: true, payload, via: \"cookie\" };\n }\n return { ok: false, reason: \"no_session\" };\n}\n\n// ─── JWKS-aware verification (zero env vars · zero JWK handling) ─────────\n//\n// verifyOcToken / getOcSession are the integrator-facing verification\n// primitives. They lazy-fetch the auth host's published JWKS at\n// `<issuer>/.well-known/jwks.json` and cache it in-process. Integrators\n// install @orangecheck/me-client (which re-exports these), call\n// `verifyOcToken(token)` or `getOcSession(headers)`, and never see a\n// JWK string, an env var, or a base64 dance.\n\nconst DEFAULT_JWKS_TTL_MS = 60 * 60 * 1000; // 1 hour\n\ninterface JwksCacheEntry {\n keys: Record<string, AuthKey>;\n fetchedAt: number;\n}\n\nconst jwksCache = new Map<string, JwksCacheEntry>();\nconst inflight = new Map<string, Promise<JwksCacheEntry>>();\n\n/**\n * When each issuer last had its cache force-refreshed by an unrecognised\n * `kid`, and how long that path must wait before doing it again.\n *\n * Without this, a token carrying any unknown kid makes the verifier drop its\n * cache and fetch the JWKS again — so a stream of tokens with random kids turns\n * every integrator's server into a refetch amplifier pointed at the auth host,\n * and puts a network round-trip in front of each rejection. `inflight` only\n * collapses CONCURRENT fetches; sequential requests each got their own.\n *\n * A minute is far below any real rotation window, so a genuinely new key is\n * still picked up promptly — the bust exists for rotation, not for every\n * malformed token.\n */\nconst lastForcedRefresh = new Map<string, number>();\nconst FORCED_REFRESH_MIN_INTERVAL_MS = 60_000;\n\ninterface JwksJson {\n keys: Array<Record<string, unknown> & { kid?: string }>;\n}\n\nasync function fetchJwks(\n issuer: string,\n ttlMs: number,\n): Promise<JwksCacheEntry> {\n const cached = jwksCache.get(issuer);\n if (cached && Date.now() - cached.fetchedAt < ttlMs) return cached;\n const existing = inflight.get(issuer);\n if (existing) return existing;\n const p = (async (): Promise<JwksCacheEntry> => {\n try {\n const url = `${issuer.replace(/\\/$/, \"\")}/.well-known/jwks.json`;\n const res = await fetch(url, { headers: { Accept: \"application/json\" } });\n if (!res.ok) {\n throw new Error(\n `[@orangecheck/auth-core] JWKS fetch ${url} returned ${res.status}`,\n );\n }\n const json = (await res.json()) as JwksJson;\n if (!json || !Array.isArray(json.keys)) {\n throw new Error(\n `[@orangecheck/auth-core] JWKS at ${url} did not return {keys:[…]}`,\n );\n }\n const keys: Record<string, AuthKey> = {};\n for (const k of json.keys) {\n if (typeof k.kid !== \"string\") continue;\n try {\n keys[k.kid] = (await importJWK(k, JWT_ALG)) as AuthKey;\n } catch {\n /* skip keys that won't import for our alg */\n }\n }\n const entry: JwksCacheEntry = { keys, fetchedAt: Date.now() };\n jwksCache.set(issuer, entry);\n return entry;\n } catch (err) {\n // Stale-on-error: a transient JWKS outage shouldn't lock out\n // every consumer. If we have a cache entry — even an expired\n // one — keep using it until the next successful fetch. Throw\n // only when we've never seen a usable JWKS.\n const stale = jwksCache.get(issuer);\n if (stale) return stale;\n throw err;\n }\n })().finally(() => {\n inflight.delete(issuer);\n });\n inflight.set(issuer, p);\n return p;\n}\n\nfunction assertSafeIssuer(issuer: string): void {\n let url: URL;\n try {\n url = new URL(issuer);\n } catch {\n throw new Error(\n `[@orangecheck/auth-core] issuer must be an absolute URL, got ${issuer}`,\n );\n }\n // Allow http only for loopback / local dev. Any production-shaped\n // hostname (real DNS) is required to be https — JWKS fetched over\n // plaintext HTTP is trivially MITM-able and would defeat the whole\n // verification stack.\n const isLoopback =\n url.hostname === \"localhost\" ||\n url.hostname === \"127.0.0.1\" ||\n url.hostname === \"::1\";\n if (url.protocol !== \"https:\" && !isLoopback) {\n throw new Error(\n `[@orangecheck/auth-core] issuer must be https (got ${url.protocol}//${url.hostname}); plaintext JWKS is not safe`,\n );\n }\n}\n\nexport interface VerifyOcOptions {\n /** Auth host issuer. Defaults to https://ochk.io. */\n issuer?: string;\n /** JWKS cache TTL in ms. Defaults to 1 hour. Stale-on-error: if the\n * cache exists, verification still works during a transient outage. */\n jwksCacheTtlMs?: number;\n /** Accepted `aud` · see AudienceRule. An integrator verifying tokens from\n * the sign-in popup sets this to its own origin, e.g.\n * `\"https://example.com\"`, so a token issued to another site is refused. */\n audience?: AudienceRule;\n}\n\n/**\n * Verify a JWT issued by an OC auth host. Lazy-fetches the JWKS from\n * `<issuer>/.well-known/jwks.json`, picks the key whose `kid` matches\n * the token's protected header, and verifies the signature.\n *\n * Returns the payload on success, `null` on any failure (bad signature,\n * expired, wrong issuer, kid not in JWKS, malformed). Never throws.\n *\n * Integrators don't need to handle JWKs, env vars, or rotation — the\n * cache picks up new keys automatically when they appear in the JWKS\n * response. Stale tokens signed under a retired key continue to verify\n * as long as the retired key is still published in the JWKS (standard\n * key-rotation overlap window).\n */\nexport async function verifyOcToken(\n token: string,\n options: VerifyOcOptions = {},\n): Promise<SessionPayload | null> {\n const issuer = options.issuer ?? DEFAULT_ISSUER;\n const ttl = options.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;\n try {\n assertSafeIssuer(issuer);\n // Fast path: try the cache. If the kid isn't in the cache (e.g.\n // brand-new key just rotated in), fall through to a fresh fetch.\n let entry = await fetchJwks(issuer, ttl);\n const headerB64 = token.split(\".\")[0];\n if (!headerB64) return null;\n const headerJson =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(headerB64, \"base64url\").toString(\"utf8\")\n : new TextDecoder().decode(base64UrlDecode(headerB64));\n const header = JSON.parse(headerJson) as { kid?: string; alg?: string };\n if (!header.kid) return null;\n let key = entry.keys[header.kid];\n if (!key) {\n // Force a fresh JWKS fetch in case the integrator's cache is older\n // than the active key set on the auth host — but at most once a\n // minute per issuer, so an unknown kid cannot be used to drive\n // unbounded refetches (see lastForcedRefresh).\n const now = Date.now();\n const last = lastForcedRefresh.get(issuer) ?? 0;\n if (now - last >= FORCED_REFRESH_MIN_INTERVAL_MS) {\n lastForcedRefresh.set(issuer, now);\n jwksCache.delete(issuer);\n entry = await fetchJwks(issuer, ttl);\n key = entry.keys[header.kid];\n }\n }\n if (!key) return null;\n const res = await jwtVerify(token, key, {\n algorithms: [JWT_ALG],\n issuer,\n });\n const p = res.payload as SessionPayload;\n if (!p.sub || !p.did_oc || !p.jti) return null;\n if (!audienceSatisfied(p.aud, options.audience)) return null;\n return p;\n } catch {\n return null;\n }\n}\n\nexport interface SessionRequestHeaders {\n cookie?: string | null;\n authorization?: string | null;\n}\n\n/**\n * Verify the OC session for a request. Accepts either a plain object\n * with `cookie` / `authorization` properties (Express / Next.js / etc.)\n * or a Web Headers object (Hono / edge / Fetch API). Reads the session\n * from the Cookie header first; falls back to a `Authorization: Bearer\n * <token>` header so cross-domain integrators (different eTLD+1, no\n * .ochk.io cookie) can verify the same way.\n *\n * Returns `null` for unauthenticated, missing, or invalid requests.\n * Never throws.\n */\nexport async function getOcSession(\n headers: SessionRequestHeaders | Headers,\n options: VerifyOcOptions = {},\n): Promise<SessionPayload | null> {\n const cookie = isHeaders(headers)\n ? headers.get(\"cookie\")\n : (headers.cookie ?? null);\n const authorization = isHeaders(headers)\n ? headers.get(\"authorization\")\n : (headers.authorization ?? null);\n let token = readSessionCookie(cookie);\n if (!token && typeof authorization === \"string\") {\n const m = /^Bearer\\s+(.+)$/i.exec(authorization);\n if (m) token = m[1]!;\n }\n if (!token) return null;\n return verifyOcToken(token, options);\n}\n\nfunction isHeaders(value: unknown): value is Headers {\n return (\n typeof Headers !== \"undefined\" &&\n value !== null &&\n typeof value === \"object\" &&\n value instanceof Headers\n );\n}\n\n/**\n * Clear the module-level JWKS caches. Test-only — the caches are process-wide\n * by design (that is the point of them), which makes any test that touches\n * verification order-dependent without a reset.\n */\nexport function _resetJwksCachesForTests(): void {\n jwksCache.clear();\n inflight.clear();\n lastForcedRefresh.clear();\n}\n\nexport * from \"./revocation\";\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangecheck/auth-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.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",
|