@orangecheck/auth-core 2.3.0 → 2.5.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 CHANGED
@@ -4,6 +4,12 @@ declare const JWT_ALG: "EdDSA";
4
4
  declare const SESSION_COOKIE: "oc_session";
5
5
  declare const DEFAULT_ISSUER: "https://ochk.io";
6
6
  type AuthKey = CryptoKey | KeyObject;
7
+ declare const DISPLAY_IDENTITY_KINDS: readonly ["did", "btc", "email", "nostr"];
8
+ type DisplayIdentityKind = (typeof DISPLAY_IDENTITY_KINDS)[number];
9
+ interface DisplayIdentity {
10
+ kind: DisplayIdentityKind;
11
+ value: string;
12
+ }
7
13
  interface SessionPayload extends JWTPayload {
8
14
  sub: string;
9
15
  did_oc: string;
@@ -16,6 +22,7 @@ interface SessionPayload extends JWTPayload {
16
22
  step_up_at?: number;
17
23
  sudo_at?: number;
18
24
  is_owner?: boolean;
25
+ display_identity?: DisplayIdentity | null;
19
26
  }
20
27
  declare function verifyStepUpClaim(payload: SessionPayload, opts: {
21
28
  max_age_secs: number;
@@ -23,6 +30,7 @@ declare function verifyStepUpClaim(payload: SessionPayload, opts: {
23
30
  declare function verifySudoClaim(payload: SessionPayload, opts: {
24
31
  max_age_secs: number;
25
32
  }): boolean;
33
+ declare function resolveDisplayIdentity(payload: SessionPayload): DisplayIdentity;
26
34
  interface VerifyConfig {
27
35
  publicJwk: string;
28
36
  issuer?: string;
@@ -45,6 +53,7 @@ declare function signSession(claims: {
45
53
  merged_from?: string[];
46
54
  step_up_at?: number;
47
55
  sudo_at?: number;
56
+ display_identity?: DisplayIdentity | null;
48
57
  }, cfg: SignConfig, ttlSeconds: number): Promise<string>;
49
58
  declare function verifySessionToken(token: string, cfg: VerifyConfig): Promise<SessionPayload | null>;
50
59
  interface CookieOptions {
@@ -59,6 +68,18 @@ interface CookieOptions {
59
68
  declare function serializeSessionCookie(token: string, opts?: CookieOptions): string;
60
69
  declare function clearSessionCookieHeader(opts?: Pick<CookieOptions, 'domain' | 'path' | 'secure'>): string;
61
70
  declare function readSessionCookie(cookieHeader: string | null | undefined): string | null;
71
+ declare function readAllSessionCookies(cookieHeader: string | null | undefined): string[];
72
+ declare const TAB_SESSION_HEADER: "x-oc-tab-session";
73
+ type ResolveSessionResult = {
74
+ ok: true;
75
+ payload: SessionPayload;
76
+ via: 'tab' | 'cookie';
77
+ } | {
78
+ ok: false;
79
+ reason: 'tab_invalid' | 'no_session';
80
+ };
81
+ type IncomingRequestHeaders = Headers | Record<string, string | string[] | undefined>;
82
+ declare function resolveSessionFromRequest(headers: IncomingRequestHeaders, cfg: VerifyConfig): Promise<ResolveSessionResult>;
62
83
  interface VerifyOcOptions {
63
84
  issuer?: string;
64
85
  jwksCacheTtlMs?: number;
@@ -70,4 +91,4 @@ interface SessionRequestHeaders {
70
91
  }
71
92
  declare function getOcSession(headers: SessionRequestHeaders | Headers, options?: VerifyOcOptions): Promise<SessionPayload | null>;
72
93
 
73
- export { type AuthKey, type CookieOptions, DEFAULT_ISSUER, JWT_ALG, SESSION_COOKIE, type SessionPayload, type SessionRequestHeaders, type SignConfig, type VerifyConfig, type VerifyOcOptions, clearSessionCookieHeader, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readSessionCookie, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
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 };
package/dist/index.d.ts CHANGED
@@ -4,6 +4,12 @@ declare const JWT_ALG: "EdDSA";
4
4
  declare const SESSION_COOKIE: "oc_session";
5
5
  declare const DEFAULT_ISSUER: "https://ochk.io";
6
6
  type AuthKey = CryptoKey | KeyObject;
7
+ declare const DISPLAY_IDENTITY_KINDS: readonly ["did", "btc", "email", "nostr"];
8
+ type DisplayIdentityKind = (typeof DISPLAY_IDENTITY_KINDS)[number];
9
+ interface DisplayIdentity {
10
+ kind: DisplayIdentityKind;
11
+ value: string;
12
+ }
7
13
  interface SessionPayload extends JWTPayload {
8
14
  sub: string;
9
15
  did_oc: string;
@@ -16,6 +22,7 @@ interface SessionPayload extends JWTPayload {
16
22
  step_up_at?: number;
17
23
  sudo_at?: number;
18
24
  is_owner?: boolean;
25
+ display_identity?: DisplayIdentity | null;
19
26
  }
20
27
  declare function verifyStepUpClaim(payload: SessionPayload, opts: {
21
28
  max_age_secs: number;
@@ -23,6 +30,7 @@ declare function verifyStepUpClaim(payload: SessionPayload, opts: {
23
30
  declare function verifySudoClaim(payload: SessionPayload, opts: {
24
31
  max_age_secs: number;
25
32
  }): boolean;
33
+ declare function resolveDisplayIdentity(payload: SessionPayload): DisplayIdentity;
26
34
  interface VerifyConfig {
27
35
  publicJwk: string;
28
36
  issuer?: string;
@@ -45,6 +53,7 @@ declare function signSession(claims: {
45
53
  merged_from?: string[];
46
54
  step_up_at?: number;
47
55
  sudo_at?: number;
56
+ display_identity?: DisplayIdentity | null;
48
57
  }, cfg: SignConfig, ttlSeconds: number): Promise<string>;
49
58
  declare function verifySessionToken(token: string, cfg: VerifyConfig): Promise<SessionPayload | null>;
50
59
  interface CookieOptions {
@@ -59,6 +68,18 @@ interface CookieOptions {
59
68
  declare function serializeSessionCookie(token: string, opts?: CookieOptions): string;
60
69
  declare function clearSessionCookieHeader(opts?: Pick<CookieOptions, 'domain' | 'path' | 'secure'>): string;
61
70
  declare function readSessionCookie(cookieHeader: string | null | undefined): string | null;
71
+ declare function readAllSessionCookies(cookieHeader: string | null | undefined): string[];
72
+ declare const TAB_SESSION_HEADER: "x-oc-tab-session";
73
+ type ResolveSessionResult = {
74
+ ok: true;
75
+ payload: SessionPayload;
76
+ via: 'tab' | 'cookie';
77
+ } | {
78
+ ok: false;
79
+ reason: 'tab_invalid' | 'no_session';
80
+ };
81
+ type IncomingRequestHeaders = Headers | Record<string, string | string[] | undefined>;
82
+ declare function resolveSessionFromRequest(headers: IncomingRequestHeaders, cfg: VerifyConfig): Promise<ResolveSessionResult>;
62
83
  interface VerifyOcOptions {
63
84
  issuer?: string;
64
85
  jwksCacheTtlMs?: number;
@@ -70,4 +91,4 @@ interface SessionRequestHeaders {
70
91
  }
71
92
  declare function getOcSession(headers: SessionRequestHeaders | Headers, options?: VerifyOcOptions): Promise<SessionPayload | null>;
72
93
 
73
- export { type AuthKey, type CookieOptions, DEFAULT_ISSUER, JWT_ALG, SESSION_COOKIE, type SessionPayload, type SessionRequestHeaders, type SignConfig, type VerifyConfig, type VerifyOcOptions, clearSessionCookieHeader, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readSessionCookie, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
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 };
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ var jose = require('jose');
6
6
  var JWT_ALG = "EdDSA";
7
7
  var SESSION_COOKIE = "oc_session";
8
8
  var DEFAULT_ISSUER = "https://ochk.io";
9
+ var DISPLAY_IDENTITY_KINDS = ["did", "btc", "email", "nostr"];
9
10
  function verifyStepUpClaim(payload, opts) {
10
11
  if (typeof payload.step_up_at !== "number") return false;
11
12
  if (!Number.isFinite(payload.step_up_at)) return false;
@@ -20,6 +21,13 @@ function verifySudoClaim(payload, opts) {
20
21
  const ageSec = Math.floor(Date.now() / 1e3) - payload.sudo_at;
21
22
  return ageSec >= 0 && ageSec < opts.max_age_secs;
22
23
  }
24
+ function resolveDisplayIdentity(payload) {
25
+ const claim = payload.display_identity;
26
+ if (claim && typeof claim === "object" && typeof claim.value === "string" && claim.value.length > 0 && DISPLAY_IDENTITY_KINDS.includes(claim.kind)) {
27
+ return { kind: claim.kind, value: claim.value };
28
+ }
29
+ return { kind: "did", value: payload.did_oc };
30
+ }
23
31
  function decodeJwk(value) {
24
32
  const json = typeof Buffer !== "undefined" ? Buffer.from(value, "base64url").toString("utf8") : new TextDecoder().decode(base64UrlDecode(value));
25
33
  const parsed = JSON.parse(json);
@@ -104,6 +112,40 @@ function readSessionCookie(cookieHeader) {
104
112
  }
105
113
  return null;
106
114
  }
115
+ function readAllSessionCookies(cookieHeader) {
116
+ if (!cookieHeader) return [];
117
+ const prefix = `${SESSION_COOKIE}=`;
118
+ const out = [];
119
+ for (const part of cookieHeader.split(";")) {
120
+ const trimmed = part.trim();
121
+ if (trimmed.startsWith(prefix)) {
122
+ const val = trimmed.slice(prefix.length);
123
+ if (val.length > 0) out.push(val);
124
+ }
125
+ }
126
+ return out;
127
+ }
128
+ var TAB_SESSION_HEADER = "x-oc-tab-session";
129
+ function headerValue(headers, name) {
130
+ if (isHeaders(headers)) return headers.get(name);
131
+ const raw = headers[name] ?? headers[name.toLowerCase()];
132
+ if (Array.isArray(raw)) return raw[0] ?? null;
133
+ return typeof raw === "string" ? raw : null;
134
+ }
135
+ async function resolveSessionFromRequest(headers, cfg) {
136
+ const tabToken = headerValue(headers, TAB_SESSION_HEADER);
137
+ if (tabToken) {
138
+ const payload = await verifySessionToken(tabToken, cfg);
139
+ if (payload) return { ok: true, payload, via: "tab" };
140
+ return { ok: false, reason: "tab_invalid" };
141
+ }
142
+ const cookieHeader = headerValue(headers, "cookie");
143
+ for (const token of readAllSessionCookies(cookieHeader)) {
144
+ const payload = await verifySessionToken(token, cfg);
145
+ if (payload) return { ok: true, payload, via: "cookie" };
146
+ }
147
+ return { ok: false, reason: "no_session" };
148
+ }
107
149
  var DEFAULT_JWKS_TTL_MS = 60 * 60 * 1e3;
108
150
  var jwksCache = /* @__PURE__ */ new Map();
109
151
  var inflight = /* @__PURE__ */ new Map();
@@ -206,14 +248,19 @@ function isHeaders(value) {
206
248
  }
207
249
 
208
250
  exports.DEFAULT_ISSUER = DEFAULT_ISSUER;
251
+ exports.DISPLAY_IDENTITY_KINDS = DISPLAY_IDENTITY_KINDS;
209
252
  exports.JWT_ALG = JWT_ALG;
210
253
  exports.SESSION_COOKIE = SESSION_COOKIE;
254
+ exports.TAB_SESSION_HEADER = TAB_SESSION_HEADER;
211
255
  exports.clearSessionCookieHeader = clearSessionCookieHeader;
212
256
  exports.getOcSession = getOcSession;
213
257
  exports.loadPrivateKey = loadPrivateKey;
214
258
  exports.loadPublicKey = loadPublicKey;
215
259
  exports.parsePublicJwk = parsePublicJwk;
260
+ exports.readAllSessionCookies = readAllSessionCookies;
216
261
  exports.readSessionCookie = readSessionCookie;
262
+ exports.resolveDisplayIdentity = resolveDisplayIdentity;
263
+ exports.resolveSessionFromRequest = resolveSessionFromRequest;
217
264
  exports.serializeSessionCookie = serializeSessionCookie;
218
265
  exports.signSession = signSession;
219
266
  exports.verifyOcToken = verifyOcToken;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["importJWK","SignJWT","jwtVerify"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @orangecheck/auth-core\n *\n * Cross-subdomain oc_session JWT verification + cookie helpers.\n *\n * The auth HOST (whoever owns sign-in) holds a private key and issues\n * tokens with `signSession()`. Every CONSUMER subdomain holds only the\n * public JWK and verifies tokens locally with `verifySessionToken()` —\n * no network round-trip, no shared secret.\n *\n * The ochk.io ecosystem picks Ed25519 (EdDSA) with JWKs distributed\n * via environment variable. The public key is also published at\n * `/.well-known/jwks.json` on the auth host for dynamic discovery.\n */\n\nimport {\n importJWK,\n jwtVerify,\n SignJWT,\n type CryptoKey,\n type JWTPayload,\n type KeyObject,\n} from 'jose';\n\nexport const JWT_ALG = 'EdDSA' as const;\nexport const SESSION_COOKIE = 'oc_session' as const;\nexport const DEFAULT_ISSUER = 'https://ochk.io' as const;\n\nexport type AuthKey = CryptoKey | KeyObject;\n\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\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\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 },\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// ─── 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(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"]}
package/dist/index.mjs CHANGED
@@ -4,6 +4,7 @@ import { importJWK, SignJWT, jwtVerify } from 'jose';
4
4
  var JWT_ALG = "EdDSA";
5
5
  var SESSION_COOKIE = "oc_session";
6
6
  var DEFAULT_ISSUER = "https://ochk.io";
7
+ var DISPLAY_IDENTITY_KINDS = ["did", "btc", "email", "nostr"];
7
8
  function verifyStepUpClaim(payload, opts) {
8
9
  if (typeof payload.step_up_at !== "number") return false;
9
10
  if (!Number.isFinite(payload.step_up_at)) return false;
@@ -18,6 +19,13 @@ function verifySudoClaim(payload, opts) {
18
19
  const ageSec = Math.floor(Date.now() / 1e3) - payload.sudo_at;
19
20
  return ageSec >= 0 && ageSec < opts.max_age_secs;
20
21
  }
22
+ function resolveDisplayIdentity(payload) {
23
+ const claim = payload.display_identity;
24
+ if (claim && typeof claim === "object" && typeof claim.value === "string" && claim.value.length > 0 && DISPLAY_IDENTITY_KINDS.includes(claim.kind)) {
25
+ return { kind: claim.kind, value: claim.value };
26
+ }
27
+ return { kind: "did", value: payload.did_oc };
28
+ }
21
29
  function decodeJwk(value) {
22
30
  const json = typeof Buffer !== "undefined" ? Buffer.from(value, "base64url").toString("utf8") : new TextDecoder().decode(base64UrlDecode(value));
23
31
  const parsed = JSON.parse(json);
@@ -102,6 +110,40 @@ function readSessionCookie(cookieHeader) {
102
110
  }
103
111
  return null;
104
112
  }
113
+ function readAllSessionCookies(cookieHeader) {
114
+ if (!cookieHeader) return [];
115
+ const prefix = `${SESSION_COOKIE}=`;
116
+ const out = [];
117
+ for (const part of cookieHeader.split(";")) {
118
+ const trimmed = part.trim();
119
+ if (trimmed.startsWith(prefix)) {
120
+ const val = trimmed.slice(prefix.length);
121
+ if (val.length > 0) out.push(val);
122
+ }
123
+ }
124
+ return out;
125
+ }
126
+ var TAB_SESSION_HEADER = "x-oc-tab-session";
127
+ function headerValue(headers, name) {
128
+ if (isHeaders(headers)) return headers.get(name);
129
+ const raw = headers[name] ?? headers[name.toLowerCase()];
130
+ if (Array.isArray(raw)) return raw[0] ?? null;
131
+ return typeof raw === "string" ? raw : null;
132
+ }
133
+ async function resolveSessionFromRequest(headers, cfg) {
134
+ const tabToken = headerValue(headers, TAB_SESSION_HEADER);
135
+ if (tabToken) {
136
+ const payload = await verifySessionToken(tabToken, cfg);
137
+ if (payload) return { ok: true, payload, via: "tab" };
138
+ return { ok: false, reason: "tab_invalid" };
139
+ }
140
+ const cookieHeader = headerValue(headers, "cookie");
141
+ for (const token of readAllSessionCookies(cookieHeader)) {
142
+ const payload = await verifySessionToken(token, cfg);
143
+ if (payload) return { ok: true, payload, via: "cookie" };
144
+ }
145
+ return { ok: false, reason: "no_session" };
146
+ }
105
147
  var DEFAULT_JWKS_TTL_MS = 60 * 60 * 1e3;
106
148
  var jwksCache = /* @__PURE__ */ new Map();
107
149
  var inflight = /* @__PURE__ */ new Map();
@@ -203,6 +245,6 @@ function isHeaders(value) {
203
245
  return typeof Headers !== "undefined" && value !== null && typeof value === "object" && value instanceof Headers;
204
246
  }
205
247
 
206
- export { DEFAULT_ISSUER, JWT_ALG, SESSION_COOKIE, clearSessionCookieHeader, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readSessionCookie, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken, verifyStepUpClaim, verifySudoClaim };
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 };
207
249
  //# sourceMappingURL=index.mjs.map
208
250
  //# sourceMappingURL=index.mjs.map
@@ -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\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\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\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 },\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// ─── 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(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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangecheck/auth-core",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "OrangeCheck session JWT primitives. Verifies oc_session tokens against the auth host's published JWKS \u2014 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",
package/CHANGELOG.md DELETED
@@ -1,60 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to **`@orangecheck/auth-core`** will be documented in this file.
4
-
5
- This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
- and [Semantic Versioning](https://semver.org/). Wire-format / canonical-message
7
- changes are coordinated via the relevant `oc-*-protocol` spec repo's CHANGELOG;
8
- this file tracks the package's TS / Node / runtime API surface.
9
-
10
- ## [Unreleased]
11
-
12
- - _(no pending changes)_
13
-
14
- ## [2.2.0] — 2026-05-14 · Inline sudo-mode claim
15
-
16
- Additive · all v2.1.x consumers keep working unchanged. New
17
- `sudo_at?: number` claim on `SessionPayload` carries the unix seconds
18
- when the user last successfully re-authenticated inline (email-OTP for
19
- email-primary identities, BIP-322 challenge for btc-primary identities)
20
- on the auth host. New `verifySudoClaim(payload, { max_age_secs })`
21
- helper · same shape as `verifyStepUpClaim`, independent claim.
22
-
23
- Replaces the hostile 1-hour fresh-`iat` gate previously protecting
24
- WebAuthn key registration. Sensitive auth-graph-mutating operations
25
- (register an additional hardware key, revoke a key, link a new
26
- identity, generate recovery codes, change recovery method) gate on
27
- `verifySudoClaim` instead of a full re-signin.
28
-
29
- `signSession` claims now accept `sudo_at?: number` (omitted when
30
- undefined, so existing tokens mint byte-identically).
31
-
32
- ## [2.1.0] — 2026-05-14 · WebAuthn step-up surface
33
-
34
- Additive · all v2.0.0 consumers continue to verify unchanged. New
35
- `step_up_at?: number` claim on `SessionPayload` carries the unix
36
- seconds when the user last completed a WebAuthn assertion on the
37
- auth host. New `verifyStepUpClaim(payload, { max_age_secs })` helper
38
- is the single source of truth for "is this session freshly stepped
39
- up." Consumer subdomains gating sensitive actions (spend > 1M sats,
40
- project_key creation, etc.) read it both before triggering
41
- `useStepUpAuth()` and again server-side after JWT verify.
42
-
43
- `signSession`'s `claims` type now accepts `merged_from?: string[]`
44
- and `step_up_at?: number`. Existing callers that don't pass them
45
- mint identical tokens (omitted claims, not null-valued).
46
-
47
- See `AUTH-OVERVIEW.md` §6 + `AUTH-TODO.md` §6 in the workspace for
48
- the broader design. The matching auth-client hooks ship in
49
- `@orangecheck/auth-client` v2.1.0 the same day.
50
-
51
- ## [0.1.0] — Initial published state
52
-
53
- Initial public release. Crypto-only Ed25519 JWT verify + cookie helpers — the consumer half of the ochk.io auth host stack.
54
-
55
- The package passes its conformance harness in CI on every change. See the
56
- shared [conformance vectors](https://github.com/orangecheck/oc-packages#conformance)
57
- section in the monorepo README for the cross-impl byte-equality discipline.
58
-
59
- [Unreleased]: https://github.com/orangecheck/oc-packages/compare/orangecheck-auth-core-v0.1.0...HEAD
60
- [0.1.0]: https://github.com/orangecheck/oc-packages/releases/tag/orangecheck-auth-core-v0.1.0