@orangecheck/auth-core 2.5.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +13 -7
- package/dist/index.d.ts +13 -7
- package/dist/index.js +47 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +46 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -17,7 +17,7 @@ interface SessionPayload extends JWTPayload {
|
|
|
17
17
|
name?: string | null;
|
|
18
18
|
npub?: string | null;
|
|
19
19
|
home_federation?: string | null;
|
|
20
|
-
signing_method?:
|
|
20
|
+
signing_method?: "fedimint_threshold" | "fedimint_client" | "bip322" | null;
|
|
21
21
|
merged_from?: string[];
|
|
22
22
|
step_up_at?: number;
|
|
23
23
|
sudo_at?: number;
|
|
@@ -31,9 +31,12 @@ declare function verifySudoClaim(payload: SessionPayload, opts: {
|
|
|
31
31
|
max_age_secs: number;
|
|
32
32
|
}): boolean;
|
|
33
33
|
declare function resolveDisplayIdentity(payload: SessionPayload): DisplayIdentity;
|
|
34
|
+
type AudienceRule = string | readonly string[] | "*";
|
|
35
|
+
declare function audienceSatisfied(aud: unknown, rule: AudienceRule | undefined): boolean;
|
|
34
36
|
interface VerifyConfig {
|
|
35
37
|
publicJwk: string;
|
|
36
38
|
issuer?: string;
|
|
39
|
+
audience?: AudienceRule;
|
|
37
40
|
}
|
|
38
41
|
interface SignConfig extends VerifyConfig {
|
|
39
42
|
privateJwk: string;
|
|
@@ -49,11 +52,12 @@ declare function signSession(claims: {
|
|
|
49
52
|
name?: string | null;
|
|
50
53
|
npub?: string | null;
|
|
51
54
|
home_federation?: string | null;
|
|
52
|
-
signing_method?:
|
|
55
|
+
signing_method?: "fedimint_threshold" | "fedimint_client" | "bip322" | null;
|
|
53
56
|
merged_from?: string[];
|
|
54
57
|
step_up_at?: number;
|
|
55
58
|
sudo_at?: number;
|
|
56
59
|
display_identity?: DisplayIdentity | null;
|
|
60
|
+
aud?: string;
|
|
57
61
|
}, cfg: SignConfig, ttlSeconds: number): Promise<string>;
|
|
58
62
|
declare function verifySessionToken(token: string, cfg: VerifyConfig): Promise<SessionPayload | null>;
|
|
59
63
|
interface CookieOptions {
|
|
@@ -63,26 +67,27 @@ interface CookieOptions {
|
|
|
63
67
|
path?: string;
|
|
64
68
|
secure?: boolean;
|
|
65
69
|
httpOnly?: boolean;
|
|
66
|
-
sameSite?:
|
|
70
|
+
sameSite?: "Lax" | "Strict" | "None";
|
|
67
71
|
}
|
|
68
72
|
declare function serializeSessionCookie(token: string, opts?: CookieOptions): string;
|
|
69
|
-
declare function clearSessionCookieHeader(opts?: Pick<CookieOptions,
|
|
73
|
+
declare function clearSessionCookieHeader(opts?: Pick<CookieOptions, "domain" | "path" | "secure">): string;
|
|
70
74
|
declare function readSessionCookie(cookieHeader: string | null | undefined): string | null;
|
|
71
75
|
declare function readAllSessionCookies(cookieHeader: string | null | undefined): string[];
|
|
72
76
|
declare const TAB_SESSION_HEADER: "x-oc-tab-session";
|
|
73
77
|
type ResolveSessionResult = {
|
|
74
78
|
ok: true;
|
|
75
79
|
payload: SessionPayload;
|
|
76
|
-
via:
|
|
80
|
+
via: "tab" | "cookie";
|
|
77
81
|
} | {
|
|
78
82
|
ok: false;
|
|
79
|
-
reason:
|
|
83
|
+
reason: "tab_invalid" | "no_session";
|
|
80
84
|
};
|
|
81
85
|
type IncomingRequestHeaders = Headers | Record<string, string | string[] | undefined>;
|
|
82
86
|
declare function resolveSessionFromRequest(headers: IncomingRequestHeaders, cfg: VerifyConfig): Promise<ResolveSessionResult>;
|
|
83
87
|
interface VerifyOcOptions {
|
|
84
88
|
issuer?: string;
|
|
85
89
|
jwksCacheTtlMs?: number;
|
|
90
|
+
audience?: AudienceRule;
|
|
86
91
|
}
|
|
87
92
|
declare function verifyOcToken(token: string, options?: VerifyOcOptions): Promise<SessionPayload | null>;
|
|
88
93
|
interface SessionRequestHeaders {
|
|
@@ -90,5 +95,6 @@ interface SessionRequestHeaders {
|
|
|
90
95
|
authorization?: string | null;
|
|
91
96
|
}
|
|
92
97
|
declare function getOcSession(headers: SessionRequestHeaders | Headers, options?: VerifyOcOptions): Promise<SessionPayload | null>;
|
|
98
|
+
declare function _resetJwksCachesForTests(): void;
|
|
93
99
|
|
|
94
|
-
export { type AuthKey, 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, clearSessionCookieHeader, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
|
100
|
+
export { type AudienceRule, type AuthKey, 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, clearSessionCookieHeader, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ interface SessionPayload extends JWTPayload {
|
|
|
17
17
|
name?: string | null;
|
|
18
18
|
npub?: string | null;
|
|
19
19
|
home_federation?: string | null;
|
|
20
|
-
signing_method?:
|
|
20
|
+
signing_method?: "fedimint_threshold" | "fedimint_client" | "bip322" | null;
|
|
21
21
|
merged_from?: string[];
|
|
22
22
|
step_up_at?: number;
|
|
23
23
|
sudo_at?: number;
|
|
@@ -31,9 +31,12 @@ declare function verifySudoClaim(payload: SessionPayload, opts: {
|
|
|
31
31
|
max_age_secs: number;
|
|
32
32
|
}): boolean;
|
|
33
33
|
declare function resolveDisplayIdentity(payload: SessionPayload): DisplayIdentity;
|
|
34
|
+
type AudienceRule = string | readonly string[] | "*";
|
|
35
|
+
declare function audienceSatisfied(aud: unknown, rule: AudienceRule | undefined): boolean;
|
|
34
36
|
interface VerifyConfig {
|
|
35
37
|
publicJwk: string;
|
|
36
38
|
issuer?: string;
|
|
39
|
+
audience?: AudienceRule;
|
|
37
40
|
}
|
|
38
41
|
interface SignConfig extends VerifyConfig {
|
|
39
42
|
privateJwk: string;
|
|
@@ -49,11 +52,12 @@ declare function signSession(claims: {
|
|
|
49
52
|
name?: string | null;
|
|
50
53
|
npub?: string | null;
|
|
51
54
|
home_federation?: string | null;
|
|
52
|
-
signing_method?:
|
|
55
|
+
signing_method?: "fedimint_threshold" | "fedimint_client" | "bip322" | null;
|
|
53
56
|
merged_from?: string[];
|
|
54
57
|
step_up_at?: number;
|
|
55
58
|
sudo_at?: number;
|
|
56
59
|
display_identity?: DisplayIdentity | null;
|
|
60
|
+
aud?: string;
|
|
57
61
|
}, cfg: SignConfig, ttlSeconds: number): Promise<string>;
|
|
58
62
|
declare function verifySessionToken(token: string, cfg: VerifyConfig): Promise<SessionPayload | null>;
|
|
59
63
|
interface CookieOptions {
|
|
@@ -63,26 +67,27 @@ interface CookieOptions {
|
|
|
63
67
|
path?: string;
|
|
64
68
|
secure?: boolean;
|
|
65
69
|
httpOnly?: boolean;
|
|
66
|
-
sameSite?:
|
|
70
|
+
sameSite?: "Lax" | "Strict" | "None";
|
|
67
71
|
}
|
|
68
72
|
declare function serializeSessionCookie(token: string, opts?: CookieOptions): string;
|
|
69
|
-
declare function clearSessionCookieHeader(opts?: Pick<CookieOptions,
|
|
73
|
+
declare function clearSessionCookieHeader(opts?: Pick<CookieOptions, "domain" | "path" | "secure">): string;
|
|
70
74
|
declare function readSessionCookie(cookieHeader: string | null | undefined): string | null;
|
|
71
75
|
declare function readAllSessionCookies(cookieHeader: string | null | undefined): string[];
|
|
72
76
|
declare const TAB_SESSION_HEADER: "x-oc-tab-session";
|
|
73
77
|
type ResolveSessionResult = {
|
|
74
78
|
ok: true;
|
|
75
79
|
payload: SessionPayload;
|
|
76
|
-
via:
|
|
80
|
+
via: "tab" | "cookie";
|
|
77
81
|
} | {
|
|
78
82
|
ok: false;
|
|
79
|
-
reason:
|
|
83
|
+
reason: "tab_invalid" | "no_session";
|
|
80
84
|
};
|
|
81
85
|
type IncomingRequestHeaders = Headers | Record<string, string | string[] | undefined>;
|
|
82
86
|
declare function resolveSessionFromRequest(headers: IncomingRequestHeaders, cfg: VerifyConfig): Promise<ResolveSessionResult>;
|
|
83
87
|
interface VerifyOcOptions {
|
|
84
88
|
issuer?: string;
|
|
85
89
|
jwksCacheTtlMs?: number;
|
|
90
|
+
audience?: AudienceRule;
|
|
86
91
|
}
|
|
87
92
|
declare function verifyOcToken(token: string, options?: VerifyOcOptions): Promise<SessionPayload | null>;
|
|
88
93
|
interface SessionRequestHeaders {
|
|
@@ -90,5 +95,6 @@ interface SessionRequestHeaders {
|
|
|
90
95
|
authorization?: string | null;
|
|
91
96
|
}
|
|
92
97
|
declare function getOcSession(headers: SessionRequestHeaders | Headers, options?: VerifyOcOptions): Promise<SessionPayload | null>;
|
|
98
|
+
declare function _resetJwksCachesForTests(): void;
|
|
93
99
|
|
|
94
|
-
export { type AuthKey, 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, clearSessionCookieHeader, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
|
100
|
+
export { type AudienceRule, type AuthKey, 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, clearSessionCookieHeader, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
package/dist/index.js
CHANGED
|
@@ -28,11 +28,20 @@ function resolveDisplayIdentity(payload) {
|
|
|
28
28
|
}
|
|
29
29
|
return { kind: "did", value: payload.did_oc };
|
|
30
30
|
}
|
|
31
|
+
function audienceSatisfied(aud, rule) {
|
|
32
|
+
const have = aud === void 0 ? [] : Array.isArray(aud) ? aud : [aud];
|
|
33
|
+
if (rule === "*") return true;
|
|
34
|
+
if (rule === void 0) return have.length === 0;
|
|
35
|
+
const want = typeof rule === "string" ? [rule] : rule;
|
|
36
|
+
return have.some((a) => typeof a === "string" && want.includes(a));
|
|
37
|
+
}
|
|
31
38
|
function decodeJwk(value) {
|
|
32
39
|
const json = typeof Buffer !== "undefined" ? Buffer.from(value, "base64url").toString("utf8") : new TextDecoder().decode(base64UrlDecode(value));
|
|
33
40
|
const parsed = JSON.parse(json);
|
|
34
41
|
if (!parsed || typeof parsed !== "object") {
|
|
35
|
-
throw new Error(
|
|
42
|
+
throw new Error(
|
|
43
|
+
"[@orangecheck/auth-core] JWK env did not decode to an object"
|
|
44
|
+
);
|
|
36
45
|
}
|
|
37
46
|
return parsed;
|
|
38
47
|
}
|
|
@@ -49,7 +58,10 @@ var publicKeyCache = null;
|
|
|
49
58
|
function loadPublicKey(publicJwk) {
|
|
50
59
|
if (!publicKeyPromise || publicKeyCache !== publicJwk) {
|
|
51
60
|
publicKeyCache = publicJwk;
|
|
52
|
-
publicKeyPromise = jose.importJWK(
|
|
61
|
+
publicKeyPromise = jose.importJWK(
|
|
62
|
+
decodeJwk(publicJwk),
|
|
63
|
+
JWT_ALG
|
|
64
|
+
);
|
|
53
65
|
}
|
|
54
66
|
return publicKeyPromise;
|
|
55
67
|
}
|
|
@@ -58,7 +70,10 @@ var privateKeyCache = null;
|
|
|
58
70
|
function loadPrivateKey(privateJwk) {
|
|
59
71
|
if (!privateKeyPromise || privateKeyCache !== privateJwk) {
|
|
60
72
|
privateKeyCache = privateJwk;
|
|
61
|
-
privateKeyPromise = jose.importJWK(
|
|
73
|
+
privateKeyPromise = jose.importJWK(
|
|
74
|
+
decodeJwk(privateJwk),
|
|
75
|
+
JWT_ALG
|
|
76
|
+
);
|
|
62
77
|
}
|
|
63
78
|
return privateKeyPromise;
|
|
64
79
|
}
|
|
@@ -67,7 +82,10 @@ function parsePublicJwk(publicJwk) {
|
|
|
67
82
|
}
|
|
68
83
|
async function signSession(claims, cfg, ttlSeconds) {
|
|
69
84
|
const now = Math.floor(Date.now() / 1e3);
|
|
70
|
-
|
|
85
|
+
const { aud, ...rest } = claims;
|
|
86
|
+
const jwt = new jose.SignJWT(rest).setProtectedHeader({ alg: JWT_ALG, typ: "JWT", kid: cfg.kid }).setIssuer(cfg.issuer ?? DEFAULT_ISSUER).setIssuedAt(now).setExpirationTime(now + ttlSeconds);
|
|
87
|
+
if (aud) jwt.setAudience(aud);
|
|
88
|
+
return jwt.sign(await loadPrivateKey(cfg.privateJwk));
|
|
71
89
|
}
|
|
72
90
|
async function verifySessionToken(token, cfg) {
|
|
73
91
|
try {
|
|
@@ -77,6 +95,7 @@ async function verifySessionToken(token, cfg) {
|
|
|
77
95
|
});
|
|
78
96
|
const p = res.payload;
|
|
79
97
|
if (!p.sub || !p.did_oc || !p.jti) return null;
|
|
98
|
+
if (!audienceSatisfied(p.aud, cfg.audience)) return null;
|
|
80
99
|
return p;
|
|
81
100
|
} catch {
|
|
82
101
|
return null;
|
|
@@ -149,6 +168,8 @@ async function resolveSessionFromRequest(headers, cfg) {
|
|
|
149
168
|
var DEFAULT_JWKS_TTL_MS = 60 * 60 * 1e3;
|
|
150
169
|
var jwksCache = /* @__PURE__ */ new Map();
|
|
151
170
|
var inflight = /* @__PURE__ */ new Map();
|
|
171
|
+
var lastForcedRefresh = /* @__PURE__ */ new Map();
|
|
172
|
+
var FORCED_REFRESH_MIN_INTERVAL_MS = 6e4;
|
|
152
173
|
async function fetchJwks(issuer, ttlMs) {
|
|
153
174
|
const cached = jwksCache.get(issuer);
|
|
154
175
|
if (cached && Date.now() - cached.fetchedAt < ttlMs) return cached;
|
|
@@ -159,11 +180,15 @@ async function fetchJwks(issuer, ttlMs) {
|
|
|
159
180
|
const url = `${issuer.replace(/\/$/, "")}/.well-known/jwks.json`;
|
|
160
181
|
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
161
182
|
if (!res.ok) {
|
|
162
|
-
throw new Error(
|
|
183
|
+
throw new Error(
|
|
184
|
+
`[@orangecheck/auth-core] JWKS fetch ${url} returned ${res.status}`
|
|
185
|
+
);
|
|
163
186
|
}
|
|
164
187
|
const json = await res.json();
|
|
165
188
|
if (!json || !Array.isArray(json.keys)) {
|
|
166
|
-
throw new Error(
|
|
189
|
+
throw new Error(
|
|
190
|
+
`[@orangecheck/auth-core] JWKS at ${url} did not return {keys:[\u2026]}`
|
|
191
|
+
);
|
|
167
192
|
}
|
|
168
193
|
const keys = {};
|
|
169
194
|
for (const k of json.keys) {
|
|
@@ -216,9 +241,14 @@ async function verifyOcToken(token, options = {}) {
|
|
|
216
241
|
if (!header.kid) return null;
|
|
217
242
|
let key = entry.keys[header.kid];
|
|
218
243
|
if (!key) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
const last = lastForcedRefresh.get(issuer) ?? 0;
|
|
246
|
+
if (now - last >= FORCED_REFRESH_MIN_INTERVAL_MS) {
|
|
247
|
+
lastForcedRefresh.set(issuer, now);
|
|
248
|
+
jwksCache.delete(issuer);
|
|
249
|
+
entry = await fetchJwks(issuer, ttl);
|
|
250
|
+
key = entry.keys[header.kid];
|
|
251
|
+
}
|
|
222
252
|
}
|
|
223
253
|
if (!key) return null;
|
|
224
254
|
const res = await jose.jwtVerify(token, key, {
|
|
@@ -227,6 +257,7 @@ async function verifyOcToken(token, options = {}) {
|
|
|
227
257
|
});
|
|
228
258
|
const p = res.payload;
|
|
229
259
|
if (!p.sub || !p.did_oc || !p.jti) return null;
|
|
260
|
+
if (!audienceSatisfied(p.aud, options.audience)) return null;
|
|
230
261
|
return p;
|
|
231
262
|
} catch {
|
|
232
263
|
return null;
|
|
@@ -246,12 +277,19 @@ async function getOcSession(headers, options = {}) {
|
|
|
246
277
|
function isHeaders(value) {
|
|
247
278
|
return typeof Headers !== "undefined" && value !== null && typeof value === "object" && value instanceof Headers;
|
|
248
279
|
}
|
|
280
|
+
function _resetJwksCachesForTests() {
|
|
281
|
+
jwksCache.clear();
|
|
282
|
+
inflight.clear();
|
|
283
|
+
lastForcedRefresh.clear();
|
|
284
|
+
}
|
|
249
285
|
|
|
250
286
|
exports.DEFAULT_ISSUER = DEFAULT_ISSUER;
|
|
251
287
|
exports.DISPLAY_IDENTITY_KINDS = DISPLAY_IDENTITY_KINDS;
|
|
252
288
|
exports.JWT_ALG = JWT_ALG;
|
|
253
289
|
exports.SESSION_COOKIE = SESSION_COOKIE;
|
|
254
290
|
exports.TAB_SESSION_HEADER = TAB_SESSION_HEADER;
|
|
291
|
+
exports._resetJwksCachesForTests = _resetJwksCachesForTests;
|
|
292
|
+
exports.audienceSatisfied = audienceSatisfied;
|
|
255
293
|
exports.clearSessionCookieHeader = clearSessionCookieHeader;
|
|
256
294
|
exports.getOcSession = getOcSession;
|
|
257
295
|
exports.loadPrivateKey = loadPrivateKey;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["importJWK","SignJWT","jwtVerify"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @orangecheck/auth-core\n *\n * Cross-subdomain oc_session JWT verification + cookie helpers.\n *\n * The auth HOST (whoever owns sign-in) holds a private key and issues\n * tokens with `signSession()`. Every CONSUMER subdomain holds only the\n * public JWK and verifies tokens locally with `verifySessionToken()` —\n * no network round-trip, no shared secret.\n *\n * The ochk.io ecosystem picks Ed25519 (EdDSA) with JWKs distributed\n * via environment variable. The public key is also published at\n * `/.well-known/jwks.json` on the auth host for dynamic discovery.\n */\n\nimport {\n importJWK,\n jwtVerify,\n SignJWT,\n type CryptoKey,\n type JWTPayload,\n type KeyObject,\n} from 'jose';\n\nexport const JWT_ALG = 'EdDSA' as const;\nexport const SESSION_COOKIE = 'oc_session' as const;\nexport const DEFAULT_ISSUER = 'https://ochk.io' as const;\n\nexport type AuthKey = CryptoKey | KeyObject;\n\n/**\n * The kinds of identity a user can surface as their account-badge\n * label. `did` is the canonical `did:oc` identifier (always available\n * and the ultimate fallback); the other three are linked identities\n * the user may or may not have.\n */\nexport const DISPLAY_IDENTITY_KINDS = ['did', 'btc', 'email', 'nostr'] as const;\nexport type DisplayIdentityKind = (typeof DISPLAY_IDENTITY_KINDS)[number];\n\n/**\n * The identity a user has chosen to display in their account badge —\n * carried as a JWT claim so every `.ochk.io` subdomain (and any\n * integrator reading the session) renders the same label with no\n * network round-trip, exactly like `name` / `npub`.\n *\n * `kind` is which identity; `value` is the full, renderable value (the\n * `did:oc`, the Bitcoin address, the email, or the npub). Only the\n * *promoted* identity's value is carried — never the whole identity\n * set — so an unrelated email never enters the token.\n */\nexport interface DisplayIdentity {\n kind: DisplayIdentityKind;\n value: string;\n}\n\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\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(payload: SessionPayload): 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\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}\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('[@orangecheck/auth-core] JWK env did not decode to an object');\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(decodeJwk(publicJwk), JWT_ALG) 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(decodeJwk(privateJwk), JWT_ALG) 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 },\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 return new SignJWT(claims)\n .setProtectedHeader({ alg: JWT_ALG, typ: 'JWT', kid: cfg.kid })\n .setIssuer(cfg.issuer ?? DEFAULT_ISSUER)\n .setIssuedAt(now)\n .setExpirationTime(now + ttlSeconds)\n .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 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(opts: Pick<CookieOptions, 'domain' | 'path' | 'secure'> = {}): 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(cookieHeader: string | null | undefined): 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(cookieHeader: string | null | undefined): 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\n | Record<string, string | string[] | undefined>;\n\nfunction headerValue(headers: IncomingRequestHeaders, name: string): 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\ninterface JwksJson {\n keys: Array<Record<string, unknown> & { kid?: string }>;\n}\n\nasync function fetchJwks(issuer: string, ttlMs: number): 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(`[@orangecheck/auth-core] JWKS fetch ${url} returned ${res.status}`);\n }\n const json = (await res.json()) as JwksJson;\n if (!json || !Array.isArray(json.keys)) {\n throw new Error(`[@orangecheck/auth-core] JWKS at ${url} did not return {keys:[…]}`);\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}\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\n // older than the active key set on the auth host.\n jwksCache.delete(issuer);\n entry = await fetchJwks(issuer, ttl);\n key = entry.keys[header.kid];\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 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) ? headers.get('cookie') : (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' && value !== null && typeof value === 'object' && value instanceof Headers\n );\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["importJWK","SignJWT","jwtVerify"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @orangecheck/auth-core\n *\n * Cross-subdomain oc_session JWT verification + cookie helpers.\n *\n * The auth HOST (whoever owns sign-in) holds a private key and issues\n * tokens with `signSession()`. Every CONSUMER subdomain holds only the\n * public JWK and verifies tokens locally with `verifySessionToken()` —\n * no network round-trip, no shared secret.\n *\n * The ochk.io ecosystem picks Ed25519 (EdDSA) with JWKs distributed\n * via environment variable. The public key is also published at\n * `/.well-known/jwks.json` on the auth host for dynamic discovery.\n */\n\nimport {\n importJWK,\n jwtVerify,\n SignJWT,\n type CryptoKey,\n type JWTPayload,\n type KeyObject,\n} from \"jose\";\n\nexport const JWT_ALG = \"EdDSA\" as const;\nexport const SESSION_COOKIE = \"oc_session\" as const;\nexport const DEFAULT_ISSUER = \"https://ochk.io\" as const;\n\nexport type AuthKey = CryptoKey | KeyObject;\n\n/**\n * The kinds of identity a user can surface as their account-badge\n * label. `did` is the canonical `did:oc` identifier (always available\n * and the ultimate fallback); the other three are linked identities\n * the user may or may not have.\n */\nexport const DISPLAY_IDENTITY_KINDS = [\"did\", \"btc\", \"email\", \"nostr\"] as const;\nexport type DisplayIdentityKind = (typeof DISPLAY_IDENTITY_KINDS)[number];\n\n/**\n * The identity a user has chosen to display in their account badge —\n * carried as a JWT claim so every `.ochk.io` subdomain (and any\n * integrator reading the session) renders the same label with no\n * network round-trip, exactly like `name` / `npub`.\n *\n * `kind` is which identity; `value` is the full, renderable value (the\n * `did:oc`, the Bitcoin address, the email, or the npub). Only the\n * *promoted* identity's value is carried — never the whole identity\n * set — so an unrelated email never enters the token.\n */\nexport interface DisplayIdentity {\n kind: DisplayIdentityKind;\n value: string;\n}\n\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\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"]}
|
package/dist/index.mjs
CHANGED
|
@@ -26,11 +26,20 @@ function resolveDisplayIdentity(payload) {
|
|
|
26
26
|
}
|
|
27
27
|
return { kind: "did", value: payload.did_oc };
|
|
28
28
|
}
|
|
29
|
+
function audienceSatisfied(aud, rule) {
|
|
30
|
+
const have = aud === void 0 ? [] : Array.isArray(aud) ? aud : [aud];
|
|
31
|
+
if (rule === "*") return true;
|
|
32
|
+
if (rule === void 0) return have.length === 0;
|
|
33
|
+
const want = typeof rule === "string" ? [rule] : rule;
|
|
34
|
+
return have.some((a) => typeof a === "string" && want.includes(a));
|
|
35
|
+
}
|
|
29
36
|
function decodeJwk(value) {
|
|
30
37
|
const json = typeof Buffer !== "undefined" ? Buffer.from(value, "base64url").toString("utf8") : new TextDecoder().decode(base64UrlDecode(value));
|
|
31
38
|
const parsed = JSON.parse(json);
|
|
32
39
|
if (!parsed || typeof parsed !== "object") {
|
|
33
|
-
throw new Error(
|
|
40
|
+
throw new Error(
|
|
41
|
+
"[@orangecheck/auth-core] JWK env did not decode to an object"
|
|
42
|
+
);
|
|
34
43
|
}
|
|
35
44
|
return parsed;
|
|
36
45
|
}
|
|
@@ -47,7 +56,10 @@ var publicKeyCache = null;
|
|
|
47
56
|
function loadPublicKey(publicJwk) {
|
|
48
57
|
if (!publicKeyPromise || publicKeyCache !== publicJwk) {
|
|
49
58
|
publicKeyCache = publicJwk;
|
|
50
|
-
publicKeyPromise = importJWK(
|
|
59
|
+
publicKeyPromise = importJWK(
|
|
60
|
+
decodeJwk(publicJwk),
|
|
61
|
+
JWT_ALG
|
|
62
|
+
);
|
|
51
63
|
}
|
|
52
64
|
return publicKeyPromise;
|
|
53
65
|
}
|
|
@@ -56,7 +68,10 @@ var privateKeyCache = null;
|
|
|
56
68
|
function loadPrivateKey(privateJwk) {
|
|
57
69
|
if (!privateKeyPromise || privateKeyCache !== privateJwk) {
|
|
58
70
|
privateKeyCache = privateJwk;
|
|
59
|
-
privateKeyPromise = importJWK(
|
|
71
|
+
privateKeyPromise = importJWK(
|
|
72
|
+
decodeJwk(privateJwk),
|
|
73
|
+
JWT_ALG
|
|
74
|
+
);
|
|
60
75
|
}
|
|
61
76
|
return privateKeyPromise;
|
|
62
77
|
}
|
|
@@ -65,7 +80,10 @@ function parsePublicJwk(publicJwk) {
|
|
|
65
80
|
}
|
|
66
81
|
async function signSession(claims, cfg, ttlSeconds) {
|
|
67
82
|
const now = Math.floor(Date.now() / 1e3);
|
|
68
|
-
|
|
83
|
+
const { aud, ...rest } = claims;
|
|
84
|
+
const jwt = new SignJWT(rest).setProtectedHeader({ alg: JWT_ALG, typ: "JWT", kid: cfg.kid }).setIssuer(cfg.issuer ?? DEFAULT_ISSUER).setIssuedAt(now).setExpirationTime(now + ttlSeconds);
|
|
85
|
+
if (aud) jwt.setAudience(aud);
|
|
86
|
+
return jwt.sign(await loadPrivateKey(cfg.privateJwk));
|
|
69
87
|
}
|
|
70
88
|
async function verifySessionToken(token, cfg) {
|
|
71
89
|
try {
|
|
@@ -75,6 +93,7 @@ async function verifySessionToken(token, cfg) {
|
|
|
75
93
|
});
|
|
76
94
|
const p = res.payload;
|
|
77
95
|
if (!p.sub || !p.did_oc || !p.jti) return null;
|
|
96
|
+
if (!audienceSatisfied(p.aud, cfg.audience)) return null;
|
|
78
97
|
return p;
|
|
79
98
|
} catch {
|
|
80
99
|
return null;
|
|
@@ -147,6 +166,8 @@ async function resolveSessionFromRequest(headers, cfg) {
|
|
|
147
166
|
var DEFAULT_JWKS_TTL_MS = 60 * 60 * 1e3;
|
|
148
167
|
var jwksCache = /* @__PURE__ */ new Map();
|
|
149
168
|
var inflight = /* @__PURE__ */ new Map();
|
|
169
|
+
var lastForcedRefresh = /* @__PURE__ */ new Map();
|
|
170
|
+
var FORCED_REFRESH_MIN_INTERVAL_MS = 6e4;
|
|
150
171
|
async function fetchJwks(issuer, ttlMs) {
|
|
151
172
|
const cached = jwksCache.get(issuer);
|
|
152
173
|
if (cached && Date.now() - cached.fetchedAt < ttlMs) return cached;
|
|
@@ -157,11 +178,15 @@ async function fetchJwks(issuer, ttlMs) {
|
|
|
157
178
|
const url = `${issuer.replace(/\/$/, "")}/.well-known/jwks.json`;
|
|
158
179
|
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
159
180
|
if (!res.ok) {
|
|
160
|
-
throw new Error(
|
|
181
|
+
throw new Error(
|
|
182
|
+
`[@orangecheck/auth-core] JWKS fetch ${url} returned ${res.status}`
|
|
183
|
+
);
|
|
161
184
|
}
|
|
162
185
|
const json = await res.json();
|
|
163
186
|
if (!json || !Array.isArray(json.keys)) {
|
|
164
|
-
throw new Error(
|
|
187
|
+
throw new Error(
|
|
188
|
+
`[@orangecheck/auth-core] JWKS at ${url} did not return {keys:[\u2026]}`
|
|
189
|
+
);
|
|
165
190
|
}
|
|
166
191
|
const keys = {};
|
|
167
192
|
for (const k of json.keys) {
|
|
@@ -214,9 +239,14 @@ async function verifyOcToken(token, options = {}) {
|
|
|
214
239
|
if (!header.kid) return null;
|
|
215
240
|
let key = entry.keys[header.kid];
|
|
216
241
|
if (!key) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
242
|
+
const now = Date.now();
|
|
243
|
+
const last = lastForcedRefresh.get(issuer) ?? 0;
|
|
244
|
+
if (now - last >= FORCED_REFRESH_MIN_INTERVAL_MS) {
|
|
245
|
+
lastForcedRefresh.set(issuer, now);
|
|
246
|
+
jwksCache.delete(issuer);
|
|
247
|
+
entry = await fetchJwks(issuer, ttl);
|
|
248
|
+
key = entry.keys[header.kid];
|
|
249
|
+
}
|
|
220
250
|
}
|
|
221
251
|
if (!key) return null;
|
|
222
252
|
const res = await jwtVerify(token, key, {
|
|
@@ -225,6 +255,7 @@ async function verifyOcToken(token, options = {}) {
|
|
|
225
255
|
});
|
|
226
256
|
const p = res.payload;
|
|
227
257
|
if (!p.sub || !p.did_oc || !p.jti) return null;
|
|
258
|
+
if (!audienceSatisfied(p.aud, options.audience)) return null;
|
|
228
259
|
return p;
|
|
229
260
|
} catch {
|
|
230
261
|
return null;
|
|
@@ -244,7 +275,12 @@ async function getOcSession(headers, options = {}) {
|
|
|
244
275
|
function isHeaders(value) {
|
|
245
276
|
return typeof Headers !== "undefined" && value !== null && typeof value === "object" && value instanceof Headers;
|
|
246
277
|
}
|
|
278
|
+
function _resetJwksCachesForTests() {
|
|
279
|
+
jwksCache.clear();
|
|
280
|
+
inflight.clear();
|
|
281
|
+
lastForcedRefresh.clear();
|
|
282
|
+
}
|
|
247
283
|
|
|
248
|
-
export { DEFAULT_ISSUER, DISPLAY_IDENTITY_KINDS, JWT_ALG, SESSION_COOKIE, TAB_SESSION_HEADER, clearSessionCookieHeader, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
|
284
|
+
export { DEFAULT_ISSUER, DISPLAY_IDENTITY_KINDS, JWT_ALG, SESSION_COOKIE, TAB_SESSION_HEADER, _resetJwksCachesForTests, audienceSatisfied, clearSessionCookieHeader, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readAllSessionCookies, readSessionCookie, resolveDisplayIdentity, resolveSessionFromRequest, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
|
|
249
285
|
//# sourceMappingURL=index.mjs.map
|
|
250
286
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @orangecheck/auth-core\n *\n * Cross-subdomain oc_session JWT verification + cookie helpers.\n *\n * The auth HOST (whoever owns sign-in) holds a private key and issues\n * tokens with `signSession()`. Every CONSUMER subdomain holds only the\n * public JWK and verifies tokens locally with `verifySessionToken()` —\n * no network round-trip, no shared secret.\n *\n * The ochk.io ecosystem picks Ed25519 (EdDSA) with JWKs distributed\n * via environment variable. The public key is also published at\n * `/.well-known/jwks.json` on the auth host for dynamic discovery.\n */\n\nimport {\n importJWK,\n jwtVerify,\n SignJWT,\n type CryptoKey,\n type JWTPayload,\n type KeyObject,\n} from 'jose';\n\nexport const JWT_ALG = 'EdDSA' as const;\nexport const SESSION_COOKIE = 'oc_session' as const;\nexport const DEFAULT_ISSUER = 'https://ochk.io' as const;\n\nexport type AuthKey = CryptoKey | KeyObject;\n\n/**\n * The kinds of identity a user can surface as their account-badge\n * label. `did` is the canonical `did:oc` identifier (always available\n * and the ultimate fallback); the other three are linked identities\n * the user may or may not have.\n */\nexport const DISPLAY_IDENTITY_KINDS = ['did', 'btc', 'email', 'nostr'] as const;\nexport type DisplayIdentityKind = (typeof DISPLAY_IDENTITY_KINDS)[number];\n\n/**\n * The identity a user has chosen to display in their account badge —\n * carried as a JWT claim so every `.ochk.io` subdomain (and any\n * integrator reading the session) renders the same label with no\n * network round-trip, exactly like `name` / `npub`.\n *\n * `kind` is which identity; `value` is the full, renderable value (the\n * `did:oc`, the Bitcoin address, the email, or the npub). Only the\n * *promoted* identity's value is carried — never the whole identity\n * set — so an unrelated email never enters the token.\n */\nexport interface DisplayIdentity {\n kind: DisplayIdentityKind;\n value: string;\n}\n\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\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(payload: SessionPayload): 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\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}\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('[@orangecheck/auth-core] JWK env did not decode to an object');\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(decodeJwk(publicJwk), JWT_ALG) 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(decodeJwk(privateJwk), JWT_ALG) 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 },\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 return new SignJWT(claims)\n .setProtectedHeader({ alg: JWT_ALG, typ: 'JWT', kid: cfg.kid })\n .setIssuer(cfg.issuer ?? DEFAULT_ISSUER)\n .setIssuedAt(now)\n .setExpirationTime(now + ttlSeconds)\n .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 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(opts: Pick<CookieOptions, 'domain' | 'path' | 'secure'> = {}): 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(cookieHeader: string | null | undefined): 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(cookieHeader: string | null | undefined): 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\n | Record<string, string | string[] | undefined>;\n\nfunction headerValue(headers: IncomingRequestHeaders, name: string): 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\ninterface JwksJson {\n keys: Array<Record<string, unknown> & { kid?: string }>;\n}\n\nasync function fetchJwks(issuer: string, ttlMs: number): 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(`[@orangecheck/auth-core] JWKS fetch ${url} returned ${res.status}`);\n }\n const json = (await res.json()) as JwksJson;\n if (!json || !Array.isArray(json.keys)) {\n throw new Error(`[@orangecheck/auth-core] JWKS at ${url} did not return {keys:[…]}`);\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}\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\n // older than the active key set on the auth host.\n jwksCache.delete(issuer);\n entry = await fetchJwks(issuer, ttl);\n key = entry.keys[header.kid];\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 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) ? headers.get('cookie') : (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' && value !== null && typeof value === 'object' && value instanceof Headers\n );\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @orangecheck/auth-core\n *\n * Cross-subdomain oc_session JWT verification + cookie helpers.\n *\n * The auth HOST (whoever owns sign-in) holds a private key and issues\n * tokens with `signSession()`. Every CONSUMER subdomain holds only the\n * public JWK and verifies tokens locally with `verifySessionToken()` —\n * no network round-trip, no shared secret.\n *\n * The ochk.io ecosystem picks Ed25519 (EdDSA) with JWKs distributed\n * via environment variable. The public key is also published at\n * `/.well-known/jwks.json` on the auth host for dynamic discovery.\n */\n\nimport {\n importJWK,\n jwtVerify,\n SignJWT,\n type CryptoKey,\n type JWTPayload,\n type KeyObject,\n} from \"jose\";\n\nexport const JWT_ALG = \"EdDSA\" as const;\nexport const SESSION_COOKIE = \"oc_session\" as const;\nexport const DEFAULT_ISSUER = \"https://ochk.io\" as const;\n\nexport type AuthKey = CryptoKey | KeyObject;\n\n/**\n * The kinds of identity a user can surface as their account-badge\n * label. `did` is the canonical `did:oc` identifier (always available\n * and the ultimate fallback); the other three are linked identities\n * the user may or may not have.\n */\nexport const DISPLAY_IDENTITY_KINDS = [\"did\", \"btc\", \"email\", \"nostr\"] as const;\nexport type DisplayIdentityKind = (typeof DISPLAY_IDENTITY_KINDS)[number];\n\n/**\n * The identity a user has chosen to display in their account badge —\n * carried as a JWT claim so every `.ochk.io` subdomain (and any\n * integrator reading the session) renders the same label with no\n * network round-trip, exactly like `name` / `npub`.\n *\n * `kind` is which identity; `value` is the full, renderable value (the\n * `did:oc`, the Bitcoin address, the email, or the npub). Only the\n * *promoted* identity's value is carried — never the whole identity\n * set — so an unrelated email never enters the token.\n */\nexport interface DisplayIdentity {\n kind: DisplayIdentityKind;\n value: string;\n}\n\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\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"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangecheck/auth-core",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "OrangeCheck session JWT primitives. Verifies oc_session tokens against the auth host's published JWKS
|
|
3
|
+
"version": "2.7.0",
|
|
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",
|
|
7
7
|
"bitcoin",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@types/node": "^25.6.0",
|
|
54
54
|
"tsup": "^8.5.1",
|
|
55
55
|
"typescript": "^6.0.3",
|
|
56
|
-
"vitest": "^4.1.
|
|
56
|
+
"vitest": "^4.1.11"
|
|
57
57
|
},
|
|
58
58
|
"publishConfig": {
|
|
59
59
|
"access": "public"
|