@orangecheck/auth-core 0.2.0 → 0.3.1

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
@@ -42,5 +42,15 @@ interface CookieOptions {
42
42
  declare function serializeSessionCookie(token: string, opts?: CookieOptions): string;
43
43
  declare function clearSessionCookieHeader(opts?: Pick<CookieOptions, 'domain' | 'path' | 'secure'>): string;
44
44
  declare function readSessionCookie(cookieHeader: string | null | undefined): string | null;
45
+ interface VerifyOcOptions {
46
+ issuer?: string;
47
+ jwksCacheTtlMs?: number;
48
+ }
49
+ declare function verifyOcToken(token: string, options?: VerifyOcOptions): Promise<SessionPayload | null>;
50
+ interface SessionRequestHeaders {
51
+ cookie?: string | null;
52
+ authorization?: string | null;
53
+ }
54
+ declare function getOcSession(headers: SessionRequestHeaders | Headers, options?: VerifyOcOptions): Promise<SessionPayload | null>;
45
55
 
46
- export { type AuthKey, type CookieOptions, DEFAULT_ISSUER, JWT_ALG, SESSION_COOKIE, type SessionPayload, type SignConfig, type VerifyConfig, clearSessionCookieHeader, loadPrivateKey, loadPublicKey, parsePublicJwk, readSessionCookie, serializeSessionCookie, signSession, verifySessionToken };
56
+ 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 };
package/dist/index.d.ts CHANGED
@@ -42,5 +42,15 @@ interface CookieOptions {
42
42
  declare function serializeSessionCookie(token: string, opts?: CookieOptions): string;
43
43
  declare function clearSessionCookieHeader(opts?: Pick<CookieOptions, 'domain' | 'path' | 'secure'>): string;
44
44
  declare function readSessionCookie(cookieHeader: string | null | undefined): string | null;
45
+ interface VerifyOcOptions {
46
+ issuer?: string;
47
+ jwksCacheTtlMs?: number;
48
+ }
49
+ declare function verifyOcToken(token: string, options?: VerifyOcOptions): Promise<SessionPayload | null>;
50
+ interface SessionRequestHeaders {
51
+ cookie?: string | null;
52
+ authorization?: string | null;
53
+ }
54
+ declare function getOcSession(headers: SessionRequestHeaders | Headers, options?: VerifyOcOptions): Promise<SessionPayload | null>;
45
55
 
46
- export { type AuthKey, type CookieOptions, DEFAULT_ISSUER, JWT_ALG, SESSION_COOKIE, type SessionPayload, type SignConfig, type VerifyConfig, clearSessionCookieHeader, loadPrivateKey, loadPublicKey, parsePublicJwk, readSessionCookie, serializeSessionCookie, signSession, verifySessionToken };
56
+ 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 };
package/dist/index.js CHANGED
@@ -90,17 +90,119 @@ function readSessionCookie(cookieHeader) {
90
90
  }
91
91
  return null;
92
92
  }
93
+ var DEFAULT_JWKS_TTL_MS = 60 * 60 * 1e3;
94
+ var jwksCache = /* @__PURE__ */ new Map();
95
+ var inflight = /* @__PURE__ */ new Map();
96
+ async function fetchJwks(issuer, ttlMs) {
97
+ const cached = jwksCache.get(issuer);
98
+ if (cached && Date.now() - cached.fetchedAt < ttlMs) return cached;
99
+ const existing = inflight.get(issuer);
100
+ if (existing) return existing;
101
+ const p = (async () => {
102
+ try {
103
+ const url = `${issuer.replace(/\/$/, "")}/.well-known/jwks.json`;
104
+ const res = await fetch(url, { headers: { Accept: "application/json" } });
105
+ if (!res.ok) {
106
+ throw new Error(`[@orangecheck/auth-core] JWKS fetch ${url} returned ${res.status}`);
107
+ }
108
+ const json = await res.json();
109
+ if (!json || !Array.isArray(json.keys)) {
110
+ throw new Error(`[@orangecheck/auth-core] JWKS at ${url} did not return {keys:[\u2026]}`);
111
+ }
112
+ const keys = {};
113
+ for (const k of json.keys) {
114
+ if (typeof k.kid !== "string") continue;
115
+ try {
116
+ keys[k.kid] = await jose.importJWK(k, JWT_ALG);
117
+ } catch {
118
+ }
119
+ }
120
+ const entry = { keys, fetchedAt: Date.now() };
121
+ jwksCache.set(issuer, entry);
122
+ return entry;
123
+ } catch (err) {
124
+ const stale = jwksCache.get(issuer);
125
+ if (stale) return stale;
126
+ throw err;
127
+ }
128
+ })().finally(() => {
129
+ inflight.delete(issuer);
130
+ });
131
+ inflight.set(issuer, p);
132
+ return p;
133
+ }
134
+ function assertSafeIssuer(issuer) {
135
+ let url;
136
+ try {
137
+ url = new URL(issuer);
138
+ } catch {
139
+ throw new Error(
140
+ `[@orangecheck/auth-core] issuer must be an absolute URL, got ${issuer}`
141
+ );
142
+ }
143
+ const isLoopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
144
+ if (url.protocol !== "https:" && !isLoopback) {
145
+ throw new Error(
146
+ `[@orangecheck/auth-core] issuer must be https (got ${url.protocol}//${url.hostname}); plaintext JWKS is not safe`
147
+ );
148
+ }
149
+ }
150
+ async function verifyOcToken(token, options = {}) {
151
+ const issuer = options.issuer ?? DEFAULT_ISSUER;
152
+ const ttl = options.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;
153
+ try {
154
+ assertSafeIssuer(issuer);
155
+ let entry = await fetchJwks(issuer, ttl);
156
+ const headerB64 = token.split(".")[0];
157
+ if (!headerB64) return null;
158
+ const headerJson = typeof Buffer !== "undefined" ? Buffer.from(headerB64, "base64url").toString("utf8") : new TextDecoder().decode(base64UrlDecode(headerB64));
159
+ const header = JSON.parse(headerJson);
160
+ if (!header.kid) return null;
161
+ let key = entry.keys[header.kid];
162
+ if (!key) {
163
+ jwksCache.delete(issuer);
164
+ entry = await fetchJwks(issuer, ttl);
165
+ key = entry.keys[header.kid];
166
+ }
167
+ if (!key) return null;
168
+ const res = await jose.jwtVerify(token, key, {
169
+ algorithms: [JWT_ALG],
170
+ issuer
171
+ });
172
+ const p = res.payload;
173
+ if (!p.sub || !p.addr || !p.jti) return null;
174
+ return p;
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+ async function getOcSession(headers, options = {}) {
180
+ const cookie = isHeaders(headers) ? headers.get("cookie") : headers.cookie ?? null;
181
+ const authorization = isHeaders(headers) ? headers.get("authorization") : headers.authorization ?? null;
182
+ let token = readSessionCookie(cookie);
183
+ if (!token && typeof authorization === "string") {
184
+ const m = /^Bearer\s+(.+)$/i.exec(authorization);
185
+ if (m) token = m[1];
186
+ }
187
+ if (!token) return null;
188
+ return verifyOcToken(token, options);
189
+ }
190
+ function isHeaders(value) {
191
+ return typeof Headers !== "undefined" && value !== null && typeof value === "object" && value instanceof Headers;
192
+ }
93
193
 
94
194
  exports.DEFAULT_ISSUER = DEFAULT_ISSUER;
95
195
  exports.JWT_ALG = JWT_ALG;
96
196
  exports.SESSION_COOKIE = SESSION_COOKIE;
97
197
  exports.clearSessionCookieHeader = clearSessionCookieHeader;
198
+ exports.getOcSession = getOcSession;
98
199
  exports.loadPrivateKey = loadPrivateKey;
99
200
  exports.loadPublicKey = loadPublicKey;
100
201
  exports.parsePublicJwk = parsePublicJwk;
101
202
  exports.readSessionCookie = readSessionCookie;
102
203
  exports.serializeSessionCookie = serializeSessionCookie;
103
204
  exports.signSession = signSession;
205
+ exports.verifyOcToken = verifyOcToken;
104
206
  exports.verifySessionToken = verifySessionToken;
105
207
  //# sourceMappingURL=index.js.map
106
208
  //# sourceMappingURL=index.js.map
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 addr: 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\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 addr: string;\n jti: string;\n name?: string | null;\n npub?: string | null;\n },\n cfg: SignConfig,\n ttlSeconds: number\n): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\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.addr || !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"]}
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 addr: 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\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 addr: string;\n jti: string;\n name?: string | null;\n npub?: string | null;\n },\n cfg: SignConfig,\n ttlSeconds: number\n): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\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.addr || !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.addr || !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
@@ -88,7 +88,107 @@ function readSessionCookie(cookieHeader) {
88
88
  }
89
89
  return null;
90
90
  }
91
+ var DEFAULT_JWKS_TTL_MS = 60 * 60 * 1e3;
92
+ var jwksCache = /* @__PURE__ */ new Map();
93
+ var inflight = /* @__PURE__ */ new Map();
94
+ async function fetchJwks(issuer, ttlMs) {
95
+ const cached = jwksCache.get(issuer);
96
+ if (cached && Date.now() - cached.fetchedAt < ttlMs) return cached;
97
+ const existing = inflight.get(issuer);
98
+ if (existing) return existing;
99
+ const p = (async () => {
100
+ try {
101
+ const url = `${issuer.replace(/\/$/, "")}/.well-known/jwks.json`;
102
+ const res = await fetch(url, { headers: { Accept: "application/json" } });
103
+ if (!res.ok) {
104
+ throw new Error(`[@orangecheck/auth-core] JWKS fetch ${url} returned ${res.status}`);
105
+ }
106
+ const json = await res.json();
107
+ if (!json || !Array.isArray(json.keys)) {
108
+ throw new Error(`[@orangecheck/auth-core] JWKS at ${url} did not return {keys:[\u2026]}`);
109
+ }
110
+ const keys = {};
111
+ for (const k of json.keys) {
112
+ if (typeof k.kid !== "string") continue;
113
+ try {
114
+ keys[k.kid] = await importJWK(k, JWT_ALG);
115
+ } catch {
116
+ }
117
+ }
118
+ const entry = { keys, fetchedAt: Date.now() };
119
+ jwksCache.set(issuer, entry);
120
+ return entry;
121
+ } catch (err) {
122
+ const stale = jwksCache.get(issuer);
123
+ if (stale) return stale;
124
+ throw err;
125
+ }
126
+ })().finally(() => {
127
+ inflight.delete(issuer);
128
+ });
129
+ inflight.set(issuer, p);
130
+ return p;
131
+ }
132
+ function assertSafeIssuer(issuer) {
133
+ let url;
134
+ try {
135
+ url = new URL(issuer);
136
+ } catch {
137
+ throw new Error(
138
+ `[@orangecheck/auth-core] issuer must be an absolute URL, got ${issuer}`
139
+ );
140
+ }
141
+ const isLoopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
142
+ if (url.protocol !== "https:" && !isLoopback) {
143
+ throw new Error(
144
+ `[@orangecheck/auth-core] issuer must be https (got ${url.protocol}//${url.hostname}); plaintext JWKS is not safe`
145
+ );
146
+ }
147
+ }
148
+ async function verifyOcToken(token, options = {}) {
149
+ const issuer = options.issuer ?? DEFAULT_ISSUER;
150
+ const ttl = options.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;
151
+ try {
152
+ assertSafeIssuer(issuer);
153
+ let entry = await fetchJwks(issuer, ttl);
154
+ const headerB64 = token.split(".")[0];
155
+ if (!headerB64) return null;
156
+ const headerJson = typeof Buffer !== "undefined" ? Buffer.from(headerB64, "base64url").toString("utf8") : new TextDecoder().decode(base64UrlDecode(headerB64));
157
+ const header = JSON.parse(headerJson);
158
+ if (!header.kid) return null;
159
+ let key = entry.keys[header.kid];
160
+ if (!key) {
161
+ jwksCache.delete(issuer);
162
+ entry = await fetchJwks(issuer, ttl);
163
+ key = entry.keys[header.kid];
164
+ }
165
+ if (!key) return null;
166
+ const res = await jwtVerify(token, key, {
167
+ algorithms: [JWT_ALG],
168
+ issuer
169
+ });
170
+ const p = res.payload;
171
+ if (!p.sub || !p.addr || !p.jti) return null;
172
+ return p;
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+ async function getOcSession(headers, options = {}) {
178
+ const cookie = isHeaders(headers) ? headers.get("cookie") : headers.cookie ?? null;
179
+ const authorization = isHeaders(headers) ? headers.get("authorization") : headers.authorization ?? null;
180
+ let token = readSessionCookie(cookie);
181
+ if (!token && typeof authorization === "string") {
182
+ const m = /^Bearer\s+(.+)$/i.exec(authorization);
183
+ if (m) token = m[1];
184
+ }
185
+ if (!token) return null;
186
+ return verifyOcToken(token, options);
187
+ }
188
+ function isHeaders(value) {
189
+ return typeof Headers !== "undefined" && value !== null && typeof value === "object" && value instanceof Headers;
190
+ }
91
191
 
92
- export { DEFAULT_ISSUER, JWT_ALG, SESSION_COOKIE, clearSessionCookieHeader, loadPrivateKey, loadPublicKey, parsePublicJwk, readSessionCookie, serializeSessionCookie, signSession, verifySessionToken };
192
+ export { DEFAULT_ISSUER, JWT_ALG, SESSION_COOKIE, clearSessionCookieHeader, getOcSession, loadPrivateKey, loadPublicKey, parsePublicJwk, readSessionCookie, serializeSessionCookie, signSession, verifyOcToken, verifySessionToken };
93
193
  //# sourceMappingURL=index.mjs.map
94
194
  //# 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 addr: 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\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 addr: string;\n jti: string;\n name?: string | null;\n npub?: string | null;\n },\n cfg: SignConfig,\n ttlSeconds: number\n): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\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.addr || !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"]}
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 addr: 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\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 addr: string;\n jti: string;\n name?: string | null;\n npub?: string | null;\n },\n cfg: SignConfig,\n ttlSeconds: number\n): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\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.addr || !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.addr || !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,7 +1,7 @@
1
1
  {
2
2
  "name": "@orangecheck/auth-core",
3
- "version": "0.2.0",
4
- "description": "Cross-subdomain oc_session JWT verification and cookie helpers. The auth primitive every OrangeCheck app imports. Carries optional name+npub claims for cross-domain profile rendering.",
3
+ "version": "0.3.1",
4
+ "description": "OrangeCheck session JWT primitives. Verifies oc_session tokens against the auth host's published JWKS — no env vars, no JWK handling. Re-exported by @orangecheck/me-client for integrators; also used by family consumer subdomains directly.",
5
5
  "keywords": [
6
6
  "orangecheck",
7
7
  "bitcoin",