@vielzeug/archive 26.10.15 → 26.10.16

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.
Files changed (58) hide show
  1. package/dist/_crypto.cjs.map +1 -1
  2. package/dist/_crypto.js +5 -1
  3. package/dist/_crypto.js.map +1 -1
  4. package/dist/_json.cjs.map +1 -1
  5. package/dist/_json.js +51 -1
  6. package/dist/_json.js.map +1 -1
  7. package/dist/auth/_confirmation.cjs.map +1 -1
  8. package/dist/auth/_confirmation.js +33 -13
  9. package/dist/auth/_confirmation.js.map +1 -1
  10. package/dist/auth/config.cjs.map +1 -1
  11. package/dist/auth/config.js +156 -1
  12. package/dist/auth/config.js.map +1 -1
  13. package/dist/auth/errors.cjs.map +1 -1
  14. package/dist/auth/errors.js +18 -1
  15. package/dist/auth/errors.js.map +1 -1
  16. package/dist/auth/store.cjs.map +1 -1
  17. package/dist/auth/store.js +102 -18
  18. package/dist/auth/store.js.map +1 -1
  19. package/dist/auth/worker.cjs.map +1 -1
  20. package/dist/auth/worker.js +360 -1
  21. package/dist/auth/worker.js.map +1 -1
  22. package/dist/auth.js +1 -1
  23. package/dist/collection.cjs.map +1 -1
  24. package/dist/collection.js +108 -1
  25. package/dist/collection.js.map +1 -1
  26. package/dist/contract.cjs.map +1 -1
  27. package/dist/contract.js +144 -1
  28. package/dist/contract.js.map +1 -1
  29. package/dist/errors.cjs.map +1 -1
  30. package/dist/errors.js +14 -1
  31. package/dist/errors.js.map +1 -1
  32. package/dist/index.js +4 -1
  33. package/dist/worker/_pagination.cjs.map +1 -1
  34. package/dist/worker/_pagination.js +55 -1
  35. package/dist/worker/_pagination.js.map +1 -1
  36. package/dist/worker/_service.cjs.map +1 -1
  37. package/dist/worker/_service.js +16 -1
  38. package/dist/worker/_service.js.map +1 -1
  39. package/dist/worker/auth.cjs.map +1 -1
  40. package/dist/worker/auth.js +68 -1
  41. package/dist/worker/auth.js.map +1 -1
  42. package/dist/worker/env.cjs.map +1 -1
  43. package/dist/worker/env.js +76 -1
  44. package/dist/worker/env.js.map +1 -1
  45. package/dist/worker/http.cjs.map +1 -1
  46. package/dist/worker/http.js +118 -1
  47. package/dist/worker/http.js.map +1 -1
  48. package/dist/worker/publications.cjs.map +1 -1
  49. package/dist/worker/publications.js +470 -1
  50. package/dist/worker/publications.js.map +1 -1
  51. package/dist/worker/sql.cjs.map +1 -1
  52. package/dist/worker/sql.js +100 -56
  53. package/dist/worker/sql.js.map +1 -1
  54. package/dist/worker/worker.cjs.map +1 -1
  55. package/dist/worker/worker.js +352 -1
  56. package/dist/worker/worker.js.map +1 -1
  57. package/dist/worker.js +1 -1
  58. package/package.json +6 -6
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","names":[],"sources":["../../src/auth/config.ts"],"sourcesContent":["import {\n type CryptoKey,\n calculateJwkThumbprint,\n createLocalJWKSet,\n importJWK,\n type JWK_RSA_Private,\n type JWK_RSA_Public,\n jwtVerify,\n SignJWT,\n} from 'jose';\nimport type { D1Like } from '../worker/env';\nimport type { ApiError } from '../worker/http';\nimport { AuthOperationError } from './errors';\n\n/** The send-email binding subset the issuer calls; the runtime's SendEmail satisfies it. */\nexport interface SendEmail {\n send(message: { from: string; html?: string; subject: string; text: string; to: string }): Promise<unknown>;\n}\n\n/**\n * The catalog Worker as the issuer reaches it: a Cloudflare service binding (or any\n * `fetch`-shaped stub). Account erasure calls the catalog's `DELETE /api/v1/me/account`\n * through it before removing the issuer's own row.\n */\nexport interface AuthArchiveBinding {\n fetch(request: Request): Promise<Response>;\n}\n\n/**\n * The bindings the auth issuer reads. Declared structurally, like the catalog\n * Worker's Env: a deployment's generated `ExportedHandler<Env>` accepts the factory's\n * result as-is, with no `@cloudflare/workers-types` dependency. Every value is\n * validated on first use per bindings object; a missing or malformed one answers\n * 503 `unavailable` on every route, never a permissive fallback.\n */\nexport interface AuthWorkerEnv {\n /** Comma-separated exact origins allowed to call unsafe routes and read CORS responses. */\n ALLOWED_ORIGINS: string;\n /** Exactly `local` (loopback HTTP only) or `production` (non-loopback HTTPS, Secure cookies). */\n APP_ENV: string;\n /** The app's display name in magic-link email and escaped confirmation-page branding. */\n APP_NAME: string;\n AUDIENCE: string;\n /** Service binding to the sibling catalog Worker: account erasure removes the catalog profile through it. */\n AUTH_ARCHIVE: AuthArchiveBinding;\n /** The issuer's own D1: accounts, links, per-account send and singleton admission budgets. */\n AUTH_DB: D1Like;\n AUTH_EMAIL: SendEmail;\n /** The `iss` claim every token carries, exactly as the catalog Worker's `JWT_ISSUER` knows it. */\n AUTH_ISSUER: string;\n /**\n * Optional JWKS JSON (`{\"keys\":[...]}`) of retired PUBLIC RS256 keys, still served\n * from `/jwks.json` and accepted by the issuer so sessions minted before a rotation\n * keep verifying until they expire. Private members are rejected.\n */\n AUTH_JWT_PREVIOUS_PUBLIC_JWKS?: string;\n /** The active RS256 private key as a JWK JSON string (a wrangler secret): signs every new session. */\n AUTH_JWT_PRIVATE_JWK: string;\n /** Comma-separated emails whose sessions carry the operator role claim. */\n OPERATOR_EMAILS: string;\n /** The app's public origin: magic links, app recovery, signed-in navigation and catalog erasure point here. */\n PUBLIC_ORIGIN: string;\n /** The address emails come from: a domain onboarded to Cloudflare Email Service. */\n SENDER_ADDRESS: string;\n}\n\nexport type AuthAppEnv = 'local' | 'production';\n\nexport interface PublicSigningKey extends JWK_RSA_Public {\n alg: 'RS256';\n kid: string;\n kty: 'RSA';\n use: 'sig';\n}\n\nexport interface AuthConfig {\n allowedOrigins: ReadonlySet<string>;\n appEnv: AuthAppEnv;\n appName: string;\n audience: string;\n issuer: string;\n /** Every key a session may be verified with: the active key first, then retired ones. */\n jwks: readonly PublicSigningKey[];\n operatorEmails: ReadonlySet<string>;\n publicOrigin: string;\n senderAddress: string;\n signing: { key: CryptoKey; kid: string };\n verifier: ReturnType<typeof createLocalJWKSet>;\n}\n\n// Configuration faults never name the offending field: the response must not leak\n// Env names, only a generic temporary unavailability.\nconst configError = (): ApiError => new AuthOperationError(503, 'configuration');\n\nconst EMAIL_MAX = 320;\nconst APP_NAME_MAX = 64;\n// 2048-bit RSA modulus: 256 bytes, 342 base64url characters unpadded.\nconst MIN_MODULUS_CHARS = 342;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/;\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);\nconst PRIVATE_RSA_MEMBERS = ['d', 'p', 'q', 'dp', 'dq', 'qi', 'oth', 'k'] as const;\n\nexport const isWellFormedEmail = (value: string): boolean =>\n value.length <= EMAIL_MAX &&\n value === value.toLowerCase() &&\n /^[^\\s@<>\"',;]+@[^\\s@<>\"',;]+\\.[^\\s@<>\"',;]+$/.test(value);\n\nconst requireString = (value: unknown): string => {\n if (typeof value !== 'string' || value.length === 0) throw configError();\n return value;\n};\n\nconst requireBinding = (value: unknown, method: string): void => {\n if (typeof value !== 'object' || value === null) throw configError();\n if (typeof Reflect.get(value, method) !== 'function') throw configError();\n};\n\n/** Transport rules: local is loopback HTTP only, production is non-loopback HTTPS; no credentials or fragments. */\nconst validateUrl = (raw: string, appEnv: AuthAppEnv): URL => {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw configError();\n }\n if (url.username !== '' || url.password !== '' || url.hash !== '') throw configError();\n const loopback = LOOPBACK_HOSTS.has(url.hostname);\n if (appEnv === 'local' ? url.protocol !== 'http:' || !loopback : url.protocol !== 'https:' || loopback) {\n throw configError();\n }\n return url;\n};\n\n/** An exact canonical origin: scheme + host (+port), no path, query, wildcard or `null`. */\nconst validateOrigin = (raw: string, appEnv: AuthAppEnv): string => {\n const url = validateUrl(raw, appEnv);\n if (url.pathname !== '/' || url.search !== '' || url.origin !== raw) throw configError();\n return url.origin;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;\n\nconst parseJson = (raw: string): unknown => {\n try {\n return JSON.parse(raw);\n } catch {\n throw configError();\n }\n};\n\nconst b64urlMember = (jwk: Record<string, unknown>, name: string): string => {\n const value = jwk[name];\n if (typeof value !== 'string' || !BASE64URL.test(value)) throw configError();\n return value;\n};\n\n/** RS256 signing-key metadata, when present, must agree with how the issuer uses the key. */\nconst checkRsaMetadata = (jwk: Record<string, unknown>): void => {\n if (jwk.kty !== 'RSA') throw configError();\n if (jwk.alg !== undefined && jwk.alg !== 'RS256') throw configError();\n if (jwk.use !== undefined && jwk.use !== 'sig') throw configError();\n};\n\nconst publicMembers = (jwk: Record<string, unknown>): JWK_RSA_Public => {\n const n = b64urlMember(jwk, 'n');\n if (n.length < MIN_MODULUS_CHARS) throw configError();\n return { e: b64urlMember(jwk, 'e'), kty: 'RSA', n };\n};\n\nconst toSigningKey = async (pub: JWK_RSA_Public): Promise<PublicSigningKey> => ({\n alg: 'RS256',\n e: pub.e,\n kid: await calculateJwkThumbprint({ e: pub.e, kty: 'RSA', n: pub.n }),\n kty: 'RSA',\n n: pub.n,\n use: 'sig',\n});\n\nasync function activeKey(raw: string): Promise<{ key: CryptoKey; public: PublicSigningKey }> {\n const parsed = parseJson(raw);\n if (!isRecord(parsed)) throw configError();\n checkRsaMetadata(parsed);\n const pub = publicMembers(parsed);\n const jwk: JWK_RSA_Private = {\n ...pub,\n d: b64urlMember(parsed, 'd'),\n dp: b64urlMember(parsed, 'dp'),\n dq: b64urlMember(parsed, 'dq'),\n p: b64urlMember(parsed, 'p'),\n q: b64urlMember(parsed, 'q'),\n qi: b64urlMember(parsed, 'qi'),\n };\n let key: CryptoKey;\n try {\n const imported = await importJWK(jwk, 'RS256', { extractable: false });\n if (imported instanceof Uint8Array) throw configError();\n key = imported;\n } catch {\n throw configError();\n }\n return { key, public: await toSigningKey(pub) };\n}\n\n/** Retired verification keys: public RSA members only; any private member is a misconfiguration. */\nasync function previousKeys(raw: string | undefined): Promise<PublicSigningKey[]> {\n if (raw === undefined || raw.trim().length === 0) return [];\n const parsed = parseJson(raw);\n if (!isRecord(parsed) || !Array.isArray(parsed.keys)) throw configError();\n const keys: PublicSigningKey[] = [];\n for (const entry of parsed.keys) {\n if (!isRecord(entry)) throw configError();\n checkRsaMetadata(entry);\n if (PRIVATE_RSA_MEMBERS.some((member) => member in entry)) throw configError();\n keys.push(await toSigningKey(publicMembers(entry)));\n }\n return keys;\n}\n\nconst configs = new WeakMap<AuthWorkerEnv, Promise<AuthConfig>>();\n\nasync function buildConfig(env: AuthWorkerEnv): Promise<AuthConfig> {\n const appEnvRaw = requireString(env.APP_ENV);\n if (appEnvRaw !== 'local' && appEnvRaw !== 'production') throw configError();\n const appEnv: AuthAppEnv = appEnvRaw;\n\n const allowedOrigins = new Set(\n requireString(env.ALLOWED_ORIGINS)\n .split(',')\n .map((origin) => origin.trim())\n .filter((origin) => origin.length > 0)\n .map((origin) => validateOrigin(origin, appEnv)),\n );\n if (allowedOrigins.size === 0) throw configError();\n\n const publicOrigin = validateOrigin(requireString(env.PUBLIC_ORIGIN), appEnv);\n // The issuer is matched as the exact configured string (a trailing slash is meaningful).\n const issuer = requireString(env.AUTH_ISSUER);\n validateUrl(issuer, appEnv);\n const audience = requireString(env.AUDIENCE);\n const appName = requireString(env.APP_NAME);\n if (appName.length > APP_NAME_MAX || /[\\p{Cc}\\p{Cs}]/u.test(appName)) throw configError();\n const senderAddress = requireString(env.SENDER_ADDRESS);\n if (!isWellFormedEmail(senderAddress)) throw configError();\n if (typeof env.OPERATOR_EMAILS !== 'string') throw configError();\n const operatorEmails = new Set(\n env.OPERATOR_EMAILS.split(',')\n .map((entry) => entry.trim().toLowerCase())\n .filter((entry) => entry.length > 0),\n );\n for (const email of operatorEmails) if (!isWellFormedEmail(email)) throw configError();\n\n requireBinding(env.AUTH_DB, 'prepare');\n requireBinding(env.AUTH_DB, 'batch');\n requireBinding(env.AUTH_EMAIL, 'send');\n requireBinding(env.AUTH_ARCHIVE, 'fetch');\n\n const active = await activeKey(requireString(env.AUTH_JWT_PRIVATE_JWK));\n try {\n const proof = await new SignJWT({}).setProtectedHeader({ alg: 'RS256', kid: active.public.kid }).sign(active.key);\n await jwtVerify(proof, createLocalJWKSet({ keys: [active.public] }), { algorithms: ['RS256'] });\n } catch {\n throw configError();\n }\n const retired = (await previousKeys(env.AUTH_JWT_PREVIOUS_PUBLIC_JWKS)).filter(\n (key) => key.kid !== active.public.kid,\n );\n const jwks = [\n active.public,\n ...retired.filter((key, i) => retired.findIndex((other) => other.kid === key.kid) === i),\n ];\n\n return {\n allowedOrigins,\n appEnv,\n appName,\n audience,\n issuer,\n jwks,\n operatorEmails,\n publicOrigin,\n senderAddress,\n signing: { key: active.key, kid: active.public.kid },\n verifier: createLocalJWKSet({ keys: [...jwks] }),\n };\n}\n\n/**\n * Validates the env and imports its keys once per bindings object. A failed\n * validation is not cached, so a corrected binding set recovers without a restart.\n */\nexport function authConfig(env: AuthWorkerEnv): Promise<AuthConfig> {\n const cached = configs.get(env);\n if (cached !== undefined) return cached;\n const pending = buildConfig(env);\n configs.set(env, pending);\n pending.catch(() => configs.delete(env));\n return pending;\n}\n"],"mappings":"iKA4FA,IAAM,gBAA8B,IAAI,EAAmB,IAAK,eAAe,EAEzE,EAAY,IACZ,EAAe,GAEf,EAAoB,IACpB,EAAY,mBACZ,EAAiB,IAAI,IAAI,CAAC,YAAa,YAAa,OAAO,CAAC,EAC5D,EAAsB,CAAC,IAAK,IAAK,IAAK,KAAM,KAAM,KAAM,MAAO,GAAG,EAE3D,kBAAqB,GAChC,EAAM,QAAU,GAChB,IAAU,EAAM,YAAY,GAC5B,+CAA+C,KAAK,CAAK,EAErD,cAAiB,GAA2B,CAChD,GAAI,OAAO,GAAU,UAAY,EAAM,SAAW,EAAG,MAAM,YAAY,EACvE,OAAO,CACT,EAEM,gBAAkB,EAAgB,IAAyB,CAE/D,GADI,OAAO,GAAU,WAAY,GAC7B,OAAO,QAAQ,IAAI,EAAO,CAAM,GAAM,WAAY,MAAM,YAAY,CAC1E,EAGM,aAAe,EAAa,IAA4B,CAC5D,IAAI,EACJ,GAAI,CACF,EAAM,IAAI,IAAI,CAAG,CACnB,MAAQ,CACN,MAAM,YAAY,CACpB,CACA,GAAI,EAAI,WAAa,IAAM,EAAI,WAAa,IAAM,EAAI,OAAS,GAAI,MAAM,YAAY,EACrF,IAAM,EAAW,EAAe,IAAI,EAAI,QAAQ,EAChD,GAAI,IAAW,QAAU,EAAI,WAAa,SAAW,CAAC,EAAW,EAAI,WAAa,UAAY,EAC5F,MAAM,YAAY,EAEpB,OAAO,CACT,EAGM,gBAAkB,EAAa,IAA+B,CAClE,IAAM,EAAM,YAAY,EAAK,CAAM,EACnC,GAAI,EAAI,WAAa,KAAO,EAAI,SAAW,IAAM,EAAI,SAAW,EAAK,MAAM,YAAY,EACvF,OAAO,EAAI,MACb,EAEM,SAAY,GAAqD,OAAO,GAAU,YAAY,EAE9F,UAAa,GAAyB,CAC1C,GAAI,CACF,OAAO,KAAK,MAAM,CAAG,CACvB,MAAQ,CACN,MAAM,YAAY,CACpB,CACF,EAEM,cAAgB,EAA8B,IAAyB,CAC3E,IAAM,EAAQ,EAAI,GAClB,GAAI,OAAO,GAAU,UAAY,CAAC,EAAU,KAAK,CAAK,EAAG,MAAM,YAAY,EAC3E,OAAO,CACT,EAGM,iBAAoB,GAAuC,CAG/D,GAFI,EAAI,MAAQ,OACZ,EAAI,MAAQ,IAAA,IAAa,EAAI,MAAQ,SACrC,EAAI,MAAQ,IAAA,IAAa,EAAI,MAAQ,MAAO,MAAM,YAAY,CACpE,EAEM,cAAiB,GAAiD,CACtE,IAAM,EAAI,aAAa,EAAK,GAAG,EAC/B,GAAI,EAAE,OAAS,EAAmB,MAAM,YAAY,EACpD,MAAO,CAAE,EAAG,aAAa,EAAK,GAAG,EAAG,IAAK,MAAO,GAAE,CACpD,EAEM,aAAe,KAAO,KAAoD,CAC9E,IAAK,QACL,EAAG,EAAI,EACP,IAAK,MAAM,EAAuB,CAAE,EAAG,EAAI,EAAG,IAAK,MAAO,EAAG,EAAI,CAAE,CAAC,EACpE,IAAK,MACL,EAAG,EAAI,EACP,IAAK,KACP,GAEA,eAAe,UAAU,EAAoE,CAC3F,IAAM,EAAS,UAAU,CAAG,EAC5B,GAAI,CAAC,SAAS,CAAM,EAAG,MAAM,YAAY,EACzC,iBAAiB,CAAM,EACvB,IAAM,EAAM,cAAc,CAAM,EAC1B,EAAuB,CAC3B,GAAG,EACH,EAAG,aAAa,EAAQ,GAAG,EAC3B,GAAI,aAAa,EAAQ,IAAI,EAC7B,GAAI,aAAa,EAAQ,IAAI,EAC7B,EAAG,aAAa,EAAQ,GAAG,EAC3B,EAAG,aAAa,EAAQ,GAAG,EAC3B,GAAI,aAAa,EAAQ,IAAI,CAC/B,EACI,EACJ,GAAI,CACF,IAAM,EAAW,MAAM,EAAU,EAAK,QAAS,CAAE,YAAa,EAAM,CAAC,EACrE,GAAI,aAAoB,WAAY,MAAM,YAAY,EACtD,EAAM,CACR,MAAQ,CACN,MAAM,YAAY,CACpB,CACA,MAAO,CAAE,MAAK,OAAQ,MAAM,aAAa,CAAG,CAAE,CAChD,CAGA,eAAe,aAAa,EAAsD,CAChF,GAAI,IAAQ,IAAA,IAAa,EAAI,KAAK,CAAC,CAAC,SAAW,EAAG,MAAO,CAAC,EAC1D,IAAM,EAAS,UAAU,CAAG,EAC5B,GAAI,CAAC,SAAS,CAAM,GAAK,CAAC,MAAM,QAAQ,EAAO,IAAI,EAAG,MAAM,YAAY,EACxE,IAAM,EAA2B,CAAC,EAClC,IAAK,IAAM,KAAS,EAAO,KAAM,CAG/B,GAFI,CAAC,SAAS,CAAK,IACnB,iBAAiB,CAAK,EAClB,EAAoB,KAAM,GAAW,KAAU,CAAK,GAAG,MAAM,YAAY,EAC7E,EAAK,KAAK,MAAM,aAAa,cAAc,CAAK,CAAC,CAAC,CACpD,CACA,OAAO,CACT,CAEA,IAAM,EAAU,IAAI,QAEpB,eAAe,YAAY,EAAyC,CAClE,IAAM,EAAY,cAAc,EAAI,OAAO,EAC3C,GAAI,IAAc,SAAW,IAAc,aAAc,MAAM,YAAY,EAC3E,IAAM,EAAqB,EAErB,EAAiB,IAAI,IACzB,cAAc,EAAI,eAAe,CAAC,CAC/B,MAAM,GAAG,CAAC,CACV,IAAK,GAAW,EAAO,KAAK,CAAC,CAAC,CAC9B,OAAQ,GAAW,EAAO,OAAS,CAAC,CAAC,CACrC,IAAK,GAAW,eAAe,EAAQ,CAAM,CAAC,CACnD,EACA,GAAI,EAAe,OAAS,EAAG,MAAM,YAAY,EAEjD,IAAM,EAAe,eAAe,cAAc,EAAI,aAAa,EAAG,CAAM,EAEtE,EAAS,cAAc,EAAI,WAAW,EAC5C,YAAY,EAAQ,CAAM,EAC1B,IAAM,EAAW,cAAc,EAAI,QAAQ,EACrC,EAAU,cAAc,EAAI,QAAQ,EAC1C,GAAI,EAAQ,OAAS,GAAgB,kBAAkB,KAAK,CAAO,EAAG,MAAM,YAAY,EACxF,IAAM,EAAgB,cAAc,EAAI,cAAc,EAEtD,GADI,CAAC,kBAAkB,CAAa,GAChC,OAAO,EAAI,iBAAoB,SAAU,MAAM,YAAY,EAC/D,IAAM,EAAiB,IAAI,IACzB,EAAI,gBAAgB,MAAM,GAAG,CAAC,CAC3B,IAAK,GAAU,EAAM,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAC1C,OAAQ,GAAU,EAAM,OAAS,CAAC,CACvC,EACA,IAAK,IAAM,KAAS,EAAgB,GAAI,CAAC,kBAAkB,CAAK,EAAG,MAAM,YAAY,EAErF,eAAe,EAAI,QAAS,SAAS,EACrC,eAAe,EAAI,QAAS,OAAO,EACnC,eAAe,EAAI,WAAY,MAAM,EACrC,eAAe,EAAI,aAAc,OAAO,EAExC,IAAM,EAAS,MAAM,UAAU,cAAc,EAAI,oBAAoB,CAAC,EACtE,GAAI,CACF,IAAM,EAAQ,MAAM,IAAI,EAAQ,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAE,IAAK,QAAS,IAAK,EAAO,OAAO,GAAI,CAAC,CAAC,CAAC,KAAK,EAAO,GAAG,EAChH,MAAM,EAAU,EAAO,EAAkB,CAAE,KAAM,CAAC,EAAO,MAAM,CAAE,CAAC,EAAG,CAAE,WAAY,CAAC,OAAO,CAAE,CAAC,CAChG,MAAQ,CACN,MAAM,YAAY,CACpB,CACA,IAAM,GAAW,MAAM,aAAa,EAAI,6BAA6B,EAAA,CAAG,OACrE,GAAQ,EAAI,MAAQ,EAAO,OAAO,GACrC,EACM,EAAO,CACX,EAAO,OACP,GAAG,EAAQ,QAAQ,EAAK,IAAM,EAAQ,UAAW,GAAU,EAAM,MAAQ,EAAI,GAAG,IAAM,CAAC,CACzF,EAEA,MAAO,CACL,iBACA,SACA,UACA,WACA,SACA,OACA,iBACA,eACA,gBACA,QAAS,CAAE,IAAK,EAAO,IAAK,IAAK,EAAO,OAAO,GAAI,EACnD,SAAU,EAAkB,CAAE,KAAM,CAAC,GAAG,CAAI,CAAE,CAAC,CACjD,CACF,CAMA,SAAgB,WAAW,EAAyC,CAClE,IAAM,EAAS,EAAQ,IAAI,CAAG,EAC9B,GAAI,IAAW,IAAA,GAAW,OAAO,EACjC,IAAM,EAAU,YAAY,CAAG,EAG/B,OAFA,EAAQ,IAAI,EAAK,CAAO,EACxB,EAAQ,UAAY,EAAQ,OAAO,CAAG,CAAC,EAChC,CACT"}
1
+ {"version":3,"file":"config.js","names":["AuthOperationError","EMAIL_MAX","APP_NAME_MAX","MIN_MODULUS_CHARS","BASE64URL","LOOPBACK_HOSTS","PRIVATE_RSA_MEMBERS","value","method","url","raw","loopback","appEnv","jwk","name","n","pub","calculateJwkThumbprint","parsed","key","imported","importJWK","keys","entry","member","configs","appEnvRaw","env","allowedOrigins","origin","publicOrigin","issuer","audience","appName","senderAddress","operatorEmails","email","active","proof","SignJWT","jwtVerify","createLocalJWKSet","retired","jwks","other","i","cached","pending"],"sources":["../../src/auth/config.ts"],"sourcesContent":["import {\n type CryptoKey,\n calculateJwkThumbprint,\n createLocalJWKSet,\n importJWK,\n type JWK_RSA_Private,\n type JWK_RSA_Public,\n jwtVerify,\n SignJWT,\n} from 'jose';\nimport type { D1Like } from '../worker/env';\nimport type { ApiError } from '../worker/http';\nimport { AuthOperationError } from './errors';\n\n/** The send-email binding subset the issuer calls; the runtime's SendEmail satisfies it. */\nexport interface SendEmail {\n send(message: { from: string; html?: string; subject: string; text: string; to: string }): Promise<unknown>;\n}\n\n/**\n * The catalog Worker as the issuer reaches it: a Cloudflare service binding (or any\n * `fetch`-shaped stub). Account erasure calls the catalog's `DELETE /api/v1/me/account`\n * through it before removing the issuer's own row.\n */\nexport interface AuthArchiveBinding {\n fetch(request: Request): Promise<Response>;\n}\n\n/**\n * The bindings the auth issuer reads. Declared structurally, like the catalog\n * Worker's Env: a deployment's generated `ExportedHandler<Env>` accepts the factory's\n * result as-is, with no `@cloudflare/workers-types` dependency. Every value is\n * validated on first use per bindings object; a missing or malformed one answers\n * 503 `unavailable` on every route, never a permissive fallback.\n */\nexport interface AuthWorkerEnv {\n /** Comma-separated exact origins allowed to call unsafe routes and read CORS responses. */\n ALLOWED_ORIGINS: string;\n /** Exactly `local` (loopback HTTP only) or `production` (non-loopback HTTPS, Secure cookies). */\n APP_ENV: string;\n /** The app's display name in magic-link email and escaped confirmation-page branding. */\n APP_NAME: string;\n AUDIENCE: string;\n /** Service binding to the sibling catalog Worker: account erasure removes the catalog profile through it. */\n AUTH_ARCHIVE: AuthArchiveBinding;\n /** The issuer's own D1: accounts, links, per-account send and singleton admission budgets. */\n AUTH_DB: D1Like;\n AUTH_EMAIL: SendEmail;\n /** The `iss` claim every token carries, exactly as the catalog Worker's `JWT_ISSUER` knows it. */\n AUTH_ISSUER: string;\n /**\n * Optional JWKS JSON (`{\"keys\":[...]}`) of retired PUBLIC RS256 keys, still served\n * from `/jwks.json` and accepted by the issuer so sessions minted before a rotation\n * keep verifying until they expire. Private members are rejected.\n */\n AUTH_JWT_PREVIOUS_PUBLIC_JWKS?: string;\n /** The active RS256 private key as a JWK JSON string (a wrangler secret): signs every new session. */\n AUTH_JWT_PRIVATE_JWK: string;\n /** Comma-separated emails whose sessions carry the operator role claim. */\n OPERATOR_EMAILS: string;\n /** The app's public origin: magic links, app recovery, signed-in navigation and catalog erasure point here. */\n PUBLIC_ORIGIN: string;\n /** The address emails come from: a domain onboarded to Cloudflare Email Service. */\n SENDER_ADDRESS: string;\n}\n\nexport type AuthAppEnv = 'local' | 'production';\n\nexport interface PublicSigningKey extends JWK_RSA_Public {\n alg: 'RS256';\n kid: string;\n kty: 'RSA';\n use: 'sig';\n}\n\nexport interface AuthConfig {\n allowedOrigins: ReadonlySet<string>;\n appEnv: AuthAppEnv;\n appName: string;\n audience: string;\n issuer: string;\n /** Every key a session may be verified with: the active key first, then retired ones. */\n jwks: readonly PublicSigningKey[];\n operatorEmails: ReadonlySet<string>;\n publicOrigin: string;\n senderAddress: string;\n signing: { key: CryptoKey; kid: string };\n verifier: ReturnType<typeof createLocalJWKSet>;\n}\n\n// Configuration faults never name the offending field: the response must not leak\n// Env names, only a generic temporary unavailability.\nconst configError = (): ApiError => new AuthOperationError(503, 'configuration');\n\nconst EMAIL_MAX = 320;\nconst APP_NAME_MAX = 64;\n// 2048-bit RSA modulus: 256 bytes, 342 base64url characters unpadded.\nconst MIN_MODULUS_CHARS = 342;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/;\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);\nconst PRIVATE_RSA_MEMBERS = ['d', 'p', 'q', 'dp', 'dq', 'qi', 'oth', 'k'] as const;\n\nexport const isWellFormedEmail = (value: string): boolean =>\n value.length <= EMAIL_MAX &&\n value === value.toLowerCase() &&\n /^[^\\s@<>\"',;]+@[^\\s@<>\"',;]+\\.[^\\s@<>\"',;]+$/.test(value);\n\nconst requireString = (value: unknown): string => {\n if (typeof value !== 'string' || value.length === 0) throw configError();\n return value;\n};\n\nconst requireBinding = (value: unknown, method: string): void => {\n if (typeof value !== 'object' || value === null) throw configError();\n if (typeof Reflect.get(value, method) !== 'function') throw configError();\n};\n\n/** Transport rules: local is loopback HTTP only, production is non-loopback HTTPS; no credentials or fragments. */\nconst validateUrl = (raw: string, appEnv: AuthAppEnv): URL => {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw configError();\n }\n if (url.username !== '' || url.password !== '' || url.hash !== '') throw configError();\n const loopback = LOOPBACK_HOSTS.has(url.hostname);\n if (appEnv === 'local' ? url.protocol !== 'http:' || !loopback : url.protocol !== 'https:' || loopback) {\n throw configError();\n }\n return url;\n};\n\n/** An exact canonical origin: scheme + host (+port), no path, query, wildcard or `null`. */\nconst validateOrigin = (raw: string, appEnv: AuthAppEnv): string => {\n const url = validateUrl(raw, appEnv);\n if (url.pathname !== '/' || url.search !== '' || url.origin !== raw) throw configError();\n return url.origin;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;\n\nconst parseJson = (raw: string): unknown => {\n try {\n return JSON.parse(raw);\n } catch {\n throw configError();\n }\n};\n\nconst b64urlMember = (jwk: Record<string, unknown>, name: string): string => {\n const value = jwk[name];\n if (typeof value !== 'string' || !BASE64URL.test(value)) throw configError();\n return value;\n};\n\n/** RS256 signing-key metadata, when present, must agree with how the issuer uses the key. */\nconst checkRsaMetadata = (jwk: Record<string, unknown>): void => {\n if (jwk.kty !== 'RSA') throw configError();\n if (jwk.alg !== undefined && jwk.alg !== 'RS256') throw configError();\n if (jwk.use !== undefined && jwk.use !== 'sig') throw configError();\n};\n\nconst publicMembers = (jwk: Record<string, unknown>): JWK_RSA_Public => {\n const n = b64urlMember(jwk, 'n');\n if (n.length < MIN_MODULUS_CHARS) throw configError();\n return { e: b64urlMember(jwk, 'e'), kty: 'RSA', n };\n};\n\nconst toSigningKey = async (pub: JWK_RSA_Public): Promise<PublicSigningKey> => ({\n alg: 'RS256',\n e: pub.e,\n kid: await calculateJwkThumbprint({ e: pub.e, kty: 'RSA', n: pub.n }),\n kty: 'RSA',\n n: pub.n,\n use: 'sig',\n});\n\nasync function activeKey(raw: string): Promise<{ key: CryptoKey; public: PublicSigningKey }> {\n const parsed = parseJson(raw);\n if (!isRecord(parsed)) throw configError();\n checkRsaMetadata(parsed);\n const pub = publicMembers(parsed);\n const jwk: JWK_RSA_Private = {\n ...pub,\n d: b64urlMember(parsed, 'd'),\n dp: b64urlMember(parsed, 'dp'),\n dq: b64urlMember(parsed, 'dq'),\n p: b64urlMember(parsed, 'p'),\n q: b64urlMember(parsed, 'q'),\n qi: b64urlMember(parsed, 'qi'),\n };\n let key: CryptoKey;\n try {\n const imported = await importJWK(jwk, 'RS256', { extractable: false });\n if (imported instanceof Uint8Array) throw configError();\n key = imported;\n } catch {\n throw configError();\n }\n return { key, public: await toSigningKey(pub) };\n}\n\n/** Retired verification keys: public RSA members only; any private member is a misconfiguration. */\nasync function previousKeys(raw: string | undefined): Promise<PublicSigningKey[]> {\n if (raw === undefined || raw.trim().length === 0) return [];\n const parsed = parseJson(raw);\n if (!isRecord(parsed) || !Array.isArray(parsed.keys)) throw configError();\n const keys: PublicSigningKey[] = [];\n for (const entry of parsed.keys) {\n if (!isRecord(entry)) throw configError();\n checkRsaMetadata(entry);\n if (PRIVATE_RSA_MEMBERS.some((member) => member in entry)) throw configError();\n keys.push(await toSigningKey(publicMembers(entry)));\n }\n return keys;\n}\n\nconst configs = new WeakMap<AuthWorkerEnv, Promise<AuthConfig>>();\n\nasync function buildConfig(env: AuthWorkerEnv): Promise<AuthConfig> {\n const appEnvRaw = requireString(env.APP_ENV);\n if (appEnvRaw !== 'local' && appEnvRaw !== 'production') throw configError();\n const appEnv: AuthAppEnv = appEnvRaw;\n\n const allowedOrigins = new Set(\n requireString(env.ALLOWED_ORIGINS)\n .split(',')\n .map((origin) => origin.trim())\n .filter((origin) => origin.length > 0)\n .map((origin) => validateOrigin(origin, appEnv)),\n );\n if (allowedOrigins.size === 0) throw configError();\n\n const publicOrigin = validateOrigin(requireString(env.PUBLIC_ORIGIN), appEnv);\n // The issuer is matched as the exact configured string (a trailing slash is meaningful).\n const issuer = requireString(env.AUTH_ISSUER);\n validateUrl(issuer, appEnv);\n const audience = requireString(env.AUDIENCE);\n const appName = requireString(env.APP_NAME);\n if (appName.length > APP_NAME_MAX || /[\\p{Cc}\\p{Cs}]/u.test(appName)) throw configError();\n const senderAddress = requireString(env.SENDER_ADDRESS);\n if (!isWellFormedEmail(senderAddress)) throw configError();\n if (typeof env.OPERATOR_EMAILS !== 'string') throw configError();\n const operatorEmails = new Set(\n env.OPERATOR_EMAILS.split(',')\n .map((entry) => entry.trim().toLowerCase())\n .filter((entry) => entry.length > 0),\n );\n for (const email of operatorEmails) if (!isWellFormedEmail(email)) throw configError();\n\n requireBinding(env.AUTH_DB, 'prepare');\n requireBinding(env.AUTH_DB, 'batch');\n requireBinding(env.AUTH_EMAIL, 'send');\n requireBinding(env.AUTH_ARCHIVE, 'fetch');\n\n const active = await activeKey(requireString(env.AUTH_JWT_PRIVATE_JWK));\n try {\n const proof = await new SignJWT({}).setProtectedHeader({ alg: 'RS256', kid: active.public.kid }).sign(active.key);\n await jwtVerify(proof, createLocalJWKSet({ keys: [active.public] }), { algorithms: ['RS256'] });\n } catch {\n throw configError();\n }\n const retired = (await previousKeys(env.AUTH_JWT_PREVIOUS_PUBLIC_JWKS)).filter(\n (key) => key.kid !== active.public.kid,\n );\n const jwks = [\n active.public,\n ...retired.filter((key, i) => retired.findIndex((other) => other.kid === key.kid) === i),\n ];\n\n return {\n allowedOrigins,\n appEnv,\n appName,\n audience,\n issuer,\n jwks,\n operatorEmails,\n publicOrigin,\n senderAddress,\n signing: { key: active.key, kid: active.public.kid },\n verifier: createLocalJWKSet({ keys: [...jwks] }),\n };\n}\n\n/**\n * Validates the env and imports its keys once per bindings object. A failed\n * validation is not cached, so a corrected binding set recovers without a restart.\n */\nexport function authConfig(env: AuthWorkerEnv): Promise<AuthConfig> {\n const cached = configs.get(env);\n if (cached !== undefined) return cached;\n const pending = buildConfig(env);\n configs.set(env, pending);\n pending.catch(() => configs.delete(env));\n return pending;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.cjs","names":[],"sources":["../../src/auth/errors.ts"],"sourcesContent":["import { ApiError } from '../worker/http';\n\nexport type AuthDiagnostic = {\n readonly kind: 'configuration' | 'database' | 'schema' | 'email' | 'catalog' | 'internal';\n};\n\nexport class AuthOperationError extends ApiError {\n readonly authDiagnostic: AuthDiagnostic;\n\n constructor(status: number, kind: AuthDiagnostic['kind'], opts?: ErrorOptions) {\n super(\n status,\n status === 500 ? 'internal' : 'unavailable',\n status === 500 ? 'Something went wrong.' : 'The service is temporarily unavailable.',\n opts,\n );\n this.authDiagnostic = { kind };\n }\n}\n\nexport async function databaseOperation<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation();\n } catch (cause) {\n throw new AuthOperationError(500, 'database', { cause });\n }\n}\n"],"mappings":"sCAMA,IAAa,mBAAb,cAAwC,EAAA,QAAS,CAC/C,eAEA,YAAY,EAAgB,EAA8B,EAAqB,CAC7E,MACE,EACA,IAAW,IAAM,WAAa,cAC9B,IAAW,IAAM,wBAA0B,0CAC3C,CACF,EACA,KAAK,eAAiB,CAAE,MAAK,CAC/B,CACF,EAEA,eAAsB,kBAAqB,EAAyC,CAClF,GAAI,CACF,OAAO,MAAM,EAAU,CACzB,OAAS,EAAO,CACd,MAAM,IAAI,mBAAmB,IAAK,WAAY,CAAE,OAAM,CAAC,CACzD,CACF"}
1
+ {"version":3,"file":"errors.cjs","names":["ApiError","require_http","status","opts","operation","cause"],"sources":["../../src/auth/errors.ts"],"sourcesContent":["import { ApiError } from '../worker/http';\n\nexport type AuthDiagnostic = {\n readonly kind: 'configuration' | 'database' | 'schema' | 'email' | 'catalog' | 'internal';\n};\n\nexport class AuthOperationError extends ApiError {\n readonly authDiagnostic: AuthDiagnostic;\n\n constructor(status: number, kind: AuthDiagnostic['kind'], opts?: ErrorOptions) {\n super(\n status,\n status === 500 ? 'internal' : 'unavailable',\n status === 500 ? 'Something went wrong.' : 'The service is temporarily unavailable.',\n opts,\n );\n this.authDiagnostic = { kind };\n }\n}\n\nexport async function databaseOperation<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation();\n } catch (cause) {\n throw new AuthOperationError(500, 'database', { cause });\n }\n}\n"],"mappings":"A,sCAMA,IAAa,mBAAb,cAAwCA,EAAAA,QAAS,CAC/C,eAEA,YAAY,EAAgB,EAA8B,EAAqB,CAC7E,MACEE,EACAA,IAAW,IAAM,WAAa,cAC9BA,IAAW,IAAM,wBAA0B,0CAC3CC,CACF,EACA,KAAK,eAAiB,CAAE,MAAK,CAC/B,CACF,EAEA,eAAsB,kBAAqB,EAAyC,CAClF,GAAI,CACF,OAAO,MAAMC,EAAU,CACzB,OAASC,EAAO,CACd,MAAM,IAAI,mBAAmB,IAAK,WAAY,CAAE,OAAM,CAAC,CACzD,CACF,C"}
@@ -1,2 +1,19 @@
1
- import{ApiError as e}from"../worker/http.js";var AuthOperationError=class extends e{authDiagnostic;constructor(e,t,n){super(e,e===500?`internal`:`unavailable`,e===500?`Something went wrong.`:`The service is temporarily unavailable.`,n),this.authDiagnostic={kind:t}}};async function databaseOperation(e){try{return await e()}catch(e){throw new AuthOperationError(500,`database`,{cause:e})}}export{AuthOperationError,databaseOperation};
1
+ import { ApiError as e } from "../worker/http.js";
2
+ //#region src/auth/errors.ts
3
+ var AuthOperationError = class extends e {
4
+ authDiagnostic;
5
+ constructor(e, t, n) {
6
+ super(e, e === 500 ? "internal" : "unavailable", e === 500 ? "Something went wrong." : "The service is temporarily unavailable.", n), this.authDiagnostic = { kind: t };
7
+ }
8
+ };
9
+ async function databaseOperation(e) {
10
+ try {
11
+ return await e();
12
+ } catch (e) {
13
+ throw new AuthOperationError(500, "database", { cause: e });
14
+ }
15
+ }
16
+ //#endregion
17
+ export { AuthOperationError, databaseOperation };
18
+
2
19
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":[],"sources":["../../src/auth/errors.ts"],"sourcesContent":["import { ApiError } from '../worker/http';\n\nexport type AuthDiagnostic = {\n readonly kind: 'configuration' | 'database' | 'schema' | 'email' | 'catalog' | 'internal';\n};\n\nexport class AuthOperationError extends ApiError {\n readonly authDiagnostic: AuthDiagnostic;\n\n constructor(status: number, kind: AuthDiagnostic['kind'], opts?: ErrorOptions) {\n super(\n status,\n status === 500 ? 'internal' : 'unavailable',\n status === 500 ? 'Something went wrong.' : 'The service is temporarily unavailable.',\n opts,\n );\n this.authDiagnostic = { kind };\n }\n}\n\nexport async function databaseOperation<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation();\n } catch (cause) {\n throw new AuthOperationError(500, 'database', { cause });\n }\n}\n"],"mappings":"6CAMA,IAAa,mBAAb,cAAwC,CAAS,CAC/C,eAEA,YAAY,EAAgB,EAA8B,EAAqB,CAC7E,MACE,EACA,IAAW,IAAM,WAAa,cAC9B,IAAW,IAAM,wBAA0B,0CAC3C,CACF,EACA,KAAK,eAAiB,CAAE,MAAK,CAC/B,CACF,EAEA,eAAsB,kBAAqB,EAAyC,CAClF,GAAI,CACF,OAAO,MAAM,EAAU,CACzB,OAAS,EAAO,CACd,MAAM,IAAI,mBAAmB,IAAK,WAAY,CAAE,OAAM,CAAC,CACzD,CACF"}
1
+ {"version":3,"file":"errors.js","names":["ApiError","status","opts","operation","cause"],"sources":["../../src/auth/errors.ts"],"sourcesContent":["import { ApiError } from '../worker/http';\n\nexport type AuthDiagnostic = {\n readonly kind: 'configuration' | 'database' | 'schema' | 'email' | 'catalog' | 'internal';\n};\n\nexport class AuthOperationError extends ApiError {\n readonly authDiagnostic: AuthDiagnostic;\n\n constructor(status: number, kind: AuthDiagnostic['kind'], opts?: ErrorOptions) {\n super(\n status,\n status === 500 ? 'internal' : 'unavailable',\n status === 500 ? 'Something went wrong.' : 'The service is temporarily unavailable.',\n opts,\n );\n this.authDiagnostic = { kind };\n }\n}\n\nexport async function databaseOperation<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation();\n } catch (cause) {\n throw new AuthOperationError(500, 'database', { cause });\n }\n}\n"],"mappings":";;AAMA,IAAa,qBAAb,cAAwCA,EAAS;CAC/C;CAEA,YAAY,GAAgB,GAA8B,GAAqB;EAO7E,AANA,MACEC,GACAA,MAAW,MAAM,aAAa,eAC9BA,MAAW,MAAM,0BAA0B,2CAC3CC,CACF,GACA,KAAK,iBAAiB,EAAE,QAAK;CAC/B;AACF;AAEA,eAAsB,kBAAqB,GAAyC;CAClF,IAAI;EACF,OAAO,MAAMC,EAAU;CACzB,SAASC,GAAO;EACd,MAAM,IAAI,mBAAmB,KAAK,YAAY,EAAE,SAAM,CAAC;CACzD;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"store.cjs","names":[],"sources":["../../src/auth/store.ts"],"sourcesContent":["import type { D1Like } from '../worker/env';\nimport { databaseOperation } from './errors';\n\n/**\n * The issuer's storage, written against exactly `migrations/auth/0001_auth.sql`.\n * Token rows are single-use credentials only; the send allowance lives in its own\n * durable table, so consuming or expiring a link never refunds the budget.\n */\nexport const LINK_TTL_MS = 15 * 60 * 1000;\nexport const LINK_WINDOW_MS = 60 * 60 * 1000;\nexport const LINKS_PER_WINDOW = 3;\nexport const SIGN_IN_WINDOW_MS = 60_000;\nexport const SIGN_INS_PER_WINDOW = 20;\n\nexport interface AccountRow {\n created_at: string;\n email: string;\n id: string;\n last_sign_in_at: string | null;\n session_version: number;\n}\n\nconst SQL = {\n account: 'SELECT id, email, created_at, last_sign_in_at, session_version FROM auth_users WHERE id = ?',\n budgetWindow: 'SELECT window_start FROM auth_link_budget WHERE user_id = ?',\n consumeToken: `UPDATE auth_tokens SET redeemed_at = ?1 WHERE token_hash = ?2 AND expires_at > ?3\nRETURNING user_id AS id, (SELECT email FROM auth_users WHERE id = user_id) AS email,\n (SELECT session_version FROM auth_users WHERE id = user_id) AS sessionVersion`,\n deleteAccount: 'DELETE FROM auth_users WHERE id = ?',\n deleteToken: 'DELETE FROM auth_tokens WHERE token_hash = ?',\n // The no-op update makes RETURNING answer for an existing row too.\n ensureUser:\n 'INSERT INTO auth_users (id, email, created_at, last_sign_in_at) VALUES (?, ?, ?, NULL) ON CONFLICT (email) DO UPDATE SET email = excluded.email RETURNING id, email',\n insertToken: 'INSERT INTO auth_tokens (token_hash, user_id, expires_at) VALUES (?, ?, ?)',\n revokeSessions: `UPDATE auth_users SET session_version = session_version + 1\nWHERE id = ? AND session_version = ? RETURNING session_version`,\n signInWindow: 'SELECT window_start FROM auth_sign_in_budget WHERE id = 1',\n // One atomic statement opens a window, counts within it, or refuses: no row back\n // means the allowance is spent. Concurrent requests cannot both pass the bound.\n spendBudget: `INSERT INTO auth_link_budget (user_id, window_start, sent) VALUES (?1, ?2, 1)\nON CONFLICT (user_id) DO UPDATE SET\n window_start = CASE WHEN auth_link_budget.window_start <= ?3 THEN ?2 ELSE auth_link_budget.window_start END,\n sent = CASE WHEN auth_link_budget.window_start <= ?3 THEN 1 ELSE auth_link_budget.sent + 1 END\nWHERE auth_link_budget.window_start <= ?3 OR auth_link_budget.sent < ?4\nRETURNING sent`,\n spendSignInBudget: `INSERT INTO auth_sign_in_budget (id, window_start, admitted) VALUES (1, ?1, 1)\nON CONFLICT (id) DO UPDATE SET\n window_start = CASE WHEN auth_sign_in_budget.window_start <= ?2 THEN ?1 ELSE auth_sign_in_budget.window_start END,\n admitted = CASE WHEN auth_sign_in_budget.window_start <= ?2 THEN 1 ELSE auth_sign_in_budget.admitted + 1 END\nWHERE auth_sign_in_budget.window_start <= ?2 OR auth_sign_in_budget.admitted < ?3\nRETURNING admitted`,\n sweepBudget: 'DELETE FROM auth_link_budget WHERE window_start <= ?',\n sweepTokens: 'DELETE FROM auth_tokens WHERE expires_at <= ?',\n // Rows for emails that never completed a sign-in leave once nothing references them.\n sweepUnverified:\n 'DELETE FROM auth_users WHERE last_sign_in_at IS NULL AND NOT EXISTS (SELECT 1 FROM auth_tokens t WHERE t.user_id = auth_users.id) AND NOT EXISTS (SELECT 1 FROM auth_link_budget b WHERE b.user_id = auth_users.id)',\n} as const;\n\nexport async function ensureUser(db: D1Like, email: string): Promise<{ email: string; id: string }> {\n const row = await databaseOperation(() =>\n db\n .prepare(SQL.ensureUser)\n .bind(crypto.randomUUID(), email, new Date().toISOString())\n .first<{ email: string; id: string }>(),\n );\n if (row === null) throw new Error('The account row could not be ensured.');\n return row;\n}\n\n/** The deployment-wide admission budget is spent before account creation or delivery. */\nexport async function spendSignInBudget(db: D1Like, nowMs: number): Promise<number | null> {\n return databaseOperation(async () => {\n const spent = await db\n .prepare(SQL.spendSignInBudget)\n .bind(nowMs, nowMs - SIGN_IN_WINDOW_MS, SIGN_INS_PER_WINDOW)\n .first<{ admitted: number }>();\n if (spent !== null) return null;\n const open = await db.prepare(SQL.signInWindow).first<{ window_start: number }>();\n return Math.max(1, Math.ceil(((open?.window_start ?? nowMs) + SIGN_IN_WINDOW_MS - nowMs) / 1000));\n });\n}\n\n/** Spends one send from the hourly allowance; returns the seconds until it reopens when spent. */\nexport async function spendLinkBudget(db: D1Like, userId: string, nowMs: number): Promise<number | null> {\n return databaseOperation(async () => {\n const windowFloor = nowMs - LINK_WINDOW_MS;\n const spent = await db\n .prepare(SQL.spendBudget)\n .bind(userId, nowMs, windowFloor, LINKS_PER_WINDOW)\n .first<{ sent: number }>();\n if (spent !== null) return null;\n const open = await db.prepare(SQL.budgetWindow).bind(userId).first<{ window_start: number }>();\n const reopensAt = (open?.window_start ?? nowMs) + LINK_WINDOW_MS;\n return Math.max(1, Math.ceil((reopensAt - nowMs) / 1000));\n });\n}\n\nexport async function insertToken(db: D1Like, tokenHash: string, userId: string, nowMs: number): Promise<void> {\n await databaseOperation(() =>\n db\n .prepare(SQL.insertToken)\n .bind(tokenHash, userId, nowMs + LINK_TTL_MS)\n .run(),\n );\n}\n\nexport async function deleteToken(db: D1Like, tokenHash: string): Promise<void> {\n await databaseOperation(() => db.prepare(SQL.deleteToken).bind(tokenHash).run());\n}\n\n/** Single-use consume: the unexpired row answers once and is gone after; the account is stamped signed in. */\nexport async function consumeToken(\n db: D1Like,\n tokenHash: string,\n nowMs: number,\n): Promise<{ email: string; id: string; sessionVersion: number } | null> {\n return databaseOperation(() =>\n db\n .prepare(SQL.consumeToken)\n .bind(new Date(nowMs).toISOString(), tokenHash, nowMs)\n .first<{ email: string; id: string; sessionVersion: number }>(),\n );\n}\n\n/** The trigger deletes outstanding links within this same atomic version change. */\nexport function revokeSessions(\n db: D1Like,\n subject: string,\n version: number,\n): Promise<{ session_version: number } | null> {\n return databaseOperation(() =>\n db.prepare(SQL.revokeSessions).bind(subject, version).first<{ session_version: number }>(),\n );\n}\n\nexport async function authReadiness(db: D1Like): Promise<number | null> {\n return databaseOperation(async () => {\n const columns = {\n auth_link_budget: ['user_id', 'window_start', 'sent'],\n auth_schema_meta: ['id', 'applied'],\n auth_sign_in_budget: ['id', 'window_start', 'admitted'],\n auth_tokens: ['token_hash', 'user_id', 'expires_at', 'redeemed_at'],\n auth_users: ['id', 'email', 'created_at', 'last_sign_in_at', 'session_version'],\n } as const;\n for (const [table, expected] of Object.entries(columns)) {\n const actual = await db.prepare(`PRAGMA table_info(${table})`).all<{ name: string }>();\n if (!expected.every((name) => actual.results?.some((column) => column.name === name))) return null;\n }\n const triggers = await db\n .prepare(\n \"SELECT name FROM sqlite_master WHERE type = 'trigger' AND name IN ('auth_token_redeemed', 'auth_sessions_revoked')\",\n )\n .all<{ name: string }>();\n if (triggers.results?.length !== 2) return null;\n const stamp = await db.prepare('SELECT applied FROM auth_schema_meta WHERE id = 1').first<{ applied: number }>();\n await db\n .prepare(`SELECT u.id, u.email, u.created_at, u.last_sign_in_at, u.session_version,\n t.token_hash, t.user_id, t.expires_at, t.redeemed_at,\n b.user_id, b.window_start, b.sent, s.id, s.window_start, s.admitted\n FROM auth_users u LEFT JOIN auth_tokens t ON 0 LEFT JOIN auth_link_budget b ON 0\n LEFT JOIN auth_sign_in_budget s ON 0 LIMIT 1`)\n .first();\n return typeof stamp?.applied === 'number' && Number.isSafeInteger(stamp.applied) && stamp.applied >= 0\n ? stamp.applied\n : null;\n });\n}\n\nexport function readAccount(db: D1Like, userId: string): Promise<AccountRow | null> {\n return databaseOperation(() => db.prepare(SQL.account).bind(userId).first<AccountRow>());\n}\n\n/** Removes the account; FK cascades take its tokens and send budget. Idempotent. */\nexport async function deleteAccount(db: D1Like, userId: string): Promise<void> {\n await databaseOperation(() => db.prepare(SQL.deleteAccount).bind(userId).run());\n}\n\n/** The scheduled sweep: expired links, closed send windows, then never-verified accounts left unreferenced. */\nexport async function sweep(db: D1Like, nowMs: number): Promise<number> {\n const results = await databaseOperation(() =>\n db.batch([\n db.prepare(SQL.sweepTokens).bind(nowMs),\n db.prepare(SQL.sweepBudget).bind(nowMs - LINK_WINDOW_MS),\n db.prepare(SQL.sweepUnverified),\n ]),\n );\n return results.reduce((total, result) => total + (result.meta?.changes ?? 0), 0);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"store.cjs","names":["databaseOperation","require_errors","LINK_TTL_MS","LINK_WINDOW_MS","SIGN_IN_WINDOW_MS","SQL","row","db","email","nowMs","open","windowFloor","userId","reopensAt","tokenHash","subject","version","table","expected","actual","column","name","stamp","total","result"],"sources":["../../src/auth/store.ts"],"sourcesContent":["import type { D1Like } from '../worker/env';\nimport { databaseOperation } from './errors';\n\n/**\n * The issuer's storage, written against exactly `migrations/auth/0001_auth.sql`.\n * Token rows are single-use credentials only; the send allowance lives in its own\n * durable table, so consuming or expiring a link never refunds the budget.\n */\nexport const LINK_TTL_MS = 15 * 60 * 1000;\nexport const LINK_WINDOW_MS = 60 * 60 * 1000;\nexport const LINKS_PER_WINDOW = 3;\nexport const SIGN_IN_WINDOW_MS = 60_000;\nexport const SIGN_INS_PER_WINDOW = 20;\n\nexport interface AccountRow {\n created_at: string;\n email: string;\n id: string;\n last_sign_in_at: string | null;\n session_version: number;\n}\n\nconst SQL = {\n account: 'SELECT id, email, created_at, last_sign_in_at, session_version FROM auth_users WHERE id = ?',\n budgetWindow: 'SELECT window_start FROM auth_link_budget WHERE user_id = ?',\n consumeToken: `UPDATE auth_tokens SET redeemed_at = ?1 WHERE token_hash = ?2 AND expires_at > ?3\nRETURNING user_id AS id, (SELECT email FROM auth_users WHERE id = user_id) AS email,\n (SELECT session_version FROM auth_users WHERE id = user_id) AS sessionVersion`,\n deleteAccount: 'DELETE FROM auth_users WHERE id = ?',\n deleteToken: 'DELETE FROM auth_tokens WHERE token_hash = ?',\n // The no-op update makes RETURNING answer for an existing row too.\n ensureUser:\n 'INSERT INTO auth_users (id, email, created_at, last_sign_in_at) VALUES (?, ?, ?, NULL) ON CONFLICT (email) DO UPDATE SET email = excluded.email RETURNING id, email',\n insertToken: 'INSERT INTO auth_tokens (token_hash, user_id, expires_at) VALUES (?, ?, ?)',\n revokeSessions: `UPDATE auth_users SET session_version = session_version + 1\nWHERE id = ? AND session_version = ? RETURNING session_version`,\n signInWindow: 'SELECT window_start FROM auth_sign_in_budget WHERE id = 1',\n // One atomic statement opens a window, counts within it, or refuses: no row back\n // means the allowance is spent. Concurrent requests cannot both pass the bound.\n spendBudget: `INSERT INTO auth_link_budget (user_id, window_start, sent) VALUES (?1, ?2, 1)\nON CONFLICT (user_id) DO UPDATE SET\n window_start = CASE WHEN auth_link_budget.window_start <= ?3 THEN ?2 ELSE auth_link_budget.window_start END,\n sent = CASE WHEN auth_link_budget.window_start <= ?3 THEN 1 ELSE auth_link_budget.sent + 1 END\nWHERE auth_link_budget.window_start <= ?3 OR auth_link_budget.sent < ?4\nRETURNING sent`,\n spendSignInBudget: `INSERT INTO auth_sign_in_budget (id, window_start, admitted) VALUES (1, ?1, 1)\nON CONFLICT (id) DO UPDATE SET\n window_start = CASE WHEN auth_sign_in_budget.window_start <= ?2 THEN ?1 ELSE auth_sign_in_budget.window_start END,\n admitted = CASE WHEN auth_sign_in_budget.window_start <= ?2 THEN 1 ELSE auth_sign_in_budget.admitted + 1 END\nWHERE auth_sign_in_budget.window_start <= ?2 OR auth_sign_in_budget.admitted < ?3\nRETURNING admitted`,\n sweepBudget: 'DELETE FROM auth_link_budget WHERE window_start <= ?',\n sweepTokens: 'DELETE FROM auth_tokens WHERE expires_at <= ?',\n // Rows for emails that never completed a sign-in leave once nothing references them.\n sweepUnverified:\n 'DELETE FROM auth_users WHERE last_sign_in_at IS NULL AND NOT EXISTS (SELECT 1 FROM auth_tokens t WHERE t.user_id = auth_users.id) AND NOT EXISTS (SELECT 1 FROM auth_link_budget b WHERE b.user_id = auth_users.id)',\n} as const;\n\nexport async function ensureUser(db: D1Like, email: string): Promise<{ email: string; id: string }> {\n const row = await databaseOperation(() =>\n db\n .prepare(SQL.ensureUser)\n .bind(crypto.randomUUID(), email, new Date().toISOString())\n .first<{ email: string; id: string }>(),\n );\n if (row === null) throw new Error('The account row could not be ensured.');\n return row;\n}\n\n/** The deployment-wide admission budget is spent before account creation or delivery. */\nexport async function spendSignInBudget(db: D1Like, nowMs: number): Promise<number | null> {\n return databaseOperation(async () => {\n const spent = await db\n .prepare(SQL.spendSignInBudget)\n .bind(nowMs, nowMs - SIGN_IN_WINDOW_MS, SIGN_INS_PER_WINDOW)\n .first<{ admitted: number }>();\n if (spent !== null) return null;\n const open = await db.prepare(SQL.signInWindow).first<{ window_start: number }>();\n return Math.max(1, Math.ceil(((open?.window_start ?? nowMs) + SIGN_IN_WINDOW_MS - nowMs) / 1000));\n });\n}\n\n/** Spends one send from the hourly allowance; returns the seconds until it reopens when spent. */\nexport async function spendLinkBudget(db: D1Like, userId: string, nowMs: number): Promise<number | null> {\n return databaseOperation(async () => {\n const windowFloor = nowMs - LINK_WINDOW_MS;\n const spent = await db\n .prepare(SQL.spendBudget)\n .bind(userId, nowMs, windowFloor, LINKS_PER_WINDOW)\n .first<{ sent: number }>();\n if (spent !== null) return null;\n const open = await db.prepare(SQL.budgetWindow).bind(userId).first<{ window_start: number }>();\n const reopensAt = (open?.window_start ?? nowMs) + LINK_WINDOW_MS;\n return Math.max(1, Math.ceil((reopensAt - nowMs) / 1000));\n });\n}\n\nexport async function insertToken(db: D1Like, tokenHash: string, userId: string, nowMs: number): Promise<void> {\n await databaseOperation(() =>\n db\n .prepare(SQL.insertToken)\n .bind(tokenHash, userId, nowMs + LINK_TTL_MS)\n .run(),\n );\n}\n\nexport async function deleteToken(db: D1Like, tokenHash: string): Promise<void> {\n await databaseOperation(() => db.prepare(SQL.deleteToken).bind(tokenHash).run());\n}\n\n/** Single-use consume: the unexpired row answers once and is gone after; the account is stamped signed in. */\nexport async function consumeToken(\n db: D1Like,\n tokenHash: string,\n nowMs: number,\n): Promise<{ email: string; id: string; sessionVersion: number } | null> {\n return databaseOperation(() =>\n db\n .prepare(SQL.consumeToken)\n .bind(new Date(nowMs).toISOString(), tokenHash, nowMs)\n .first<{ email: string; id: string; sessionVersion: number }>(),\n );\n}\n\n/** The trigger deletes outstanding links within this same atomic version change. */\nexport function revokeSessions(\n db: D1Like,\n subject: string,\n version: number,\n): Promise<{ session_version: number } | null> {\n return databaseOperation(() =>\n db.prepare(SQL.revokeSessions).bind(subject, version).first<{ session_version: number }>(),\n );\n}\n\nexport async function authReadiness(db: D1Like): Promise<number | null> {\n return databaseOperation(async () => {\n const columns = {\n auth_link_budget: ['user_id', 'window_start', 'sent'],\n auth_schema_meta: ['id', 'applied'],\n auth_sign_in_budget: ['id', 'window_start', 'admitted'],\n auth_tokens: ['token_hash', 'user_id', 'expires_at', 'redeemed_at'],\n auth_users: ['id', 'email', 'created_at', 'last_sign_in_at', 'session_version'],\n } as const;\n for (const [table, expected] of Object.entries(columns)) {\n const actual = await db.prepare(`PRAGMA table_info(${table})`).all<{ name: string }>();\n if (!expected.every((name) => actual.results?.some((column) => column.name === name))) return null;\n }\n const triggers = await db\n .prepare(\n \"SELECT name FROM sqlite_master WHERE type = 'trigger' AND name IN ('auth_token_redeemed', 'auth_sessions_revoked')\",\n )\n .all<{ name: string }>();\n if (triggers.results?.length !== 2) return null;\n const stamp = await db.prepare('SELECT applied FROM auth_schema_meta WHERE id = 1').first<{ applied: number }>();\n await db\n .prepare(`SELECT u.id, u.email, u.created_at, u.last_sign_in_at, u.session_version,\n t.token_hash, t.user_id, t.expires_at, t.redeemed_at,\n b.user_id, b.window_start, b.sent, s.id, s.window_start, s.admitted\n FROM auth_users u LEFT JOIN auth_tokens t ON 0 LEFT JOIN auth_link_budget b ON 0\n LEFT JOIN auth_sign_in_budget s ON 0 LIMIT 1`)\n .first();\n return typeof stamp?.applied === 'number' && Number.isSafeInteger(stamp.applied) && stamp.applied >= 0\n ? stamp.applied\n : null;\n });\n}\n\nexport function readAccount(db: D1Like, userId: string): Promise<AccountRow | null> {\n return databaseOperation(() => db.prepare(SQL.account).bind(userId).first<AccountRow>());\n}\n\n/** Removes the account; FK cascades take its tokens and send budget. Idempotent. */\nexport async function deleteAccount(db: D1Like, userId: string): Promise<void> {\n await databaseOperation(() => db.prepare(SQL.deleteAccount).bind(userId).run());\n}\n\n/** The scheduled sweep: expired links, closed send windows, then never-verified accounts left unreferenced. */\nexport async function sweep(db: D1Like, nowMs: number): Promise<number> {\n const results = await databaseOperation(() =>\n db.batch([\n db.prepare(SQL.sweepTokens).bind(nowMs),\n db.prepare(SQL.sweepBudget).bind(nowMs - LINK_WINDOW_MS),\n db.prepare(SQL.sweepUnverified),\n ]),\n );\n return results.reduce((total, result) => total + (result.meta?.changes ?? 0), 0);\n}\n"],"mappings":"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"}
@@ -1,19 +1,103 @@
1
- import{databaseOperation as e}from"./errors.js";var t=9e5,n=36e5,r=6e4,i={account:`SELECT id, email, created_at, last_sign_in_at, session_version FROM auth_users WHERE id = ?`,budgetWindow:`SELECT window_start FROM auth_link_budget WHERE user_id = ?`,consumeToken:`UPDATE auth_tokens SET redeemed_at = ?1 WHERE token_hash = ?2 AND expires_at > ?3
2
- RETURNING user_id AS id, (SELECT email FROM auth_users WHERE id = user_id) AS email,
3
- (SELECT session_version FROM auth_users WHERE id = user_id) AS sessionVersion`,deleteAccount:`DELETE FROM auth_users WHERE id = ?`,deleteToken:`DELETE FROM auth_tokens WHERE token_hash = ?`,ensureUser:`INSERT INTO auth_users (id, email, created_at, last_sign_in_at) VALUES (?, ?, ?, NULL) ON CONFLICT (email) DO UPDATE SET email = excluded.email RETURNING id, email`,insertToken:`INSERT INTO auth_tokens (token_hash, user_id, expires_at) VALUES (?, ?, ?)`,revokeSessions:`UPDATE auth_users SET session_version = session_version + 1
4
- WHERE id = ? AND session_version = ? RETURNING session_version`,signInWindow:`SELECT window_start FROM auth_sign_in_budget WHERE id = 1`,spendBudget:`INSERT INTO auth_link_budget (user_id, window_start, sent) VALUES (?1, ?2, 1)
5
- ON CONFLICT (user_id) DO UPDATE SET
6
- window_start = CASE WHEN auth_link_budget.window_start <= ?3 THEN ?2 ELSE auth_link_budget.window_start END,
7
- sent = CASE WHEN auth_link_budget.window_start <= ?3 THEN 1 ELSE auth_link_budget.sent + 1 END
8
- WHERE auth_link_budget.window_start <= ?3 OR auth_link_budget.sent < ?4
9
- RETURNING sent`,spendSignInBudget:`INSERT INTO auth_sign_in_budget (id, window_start, admitted) VALUES (1, ?1, 1)
10
- ON CONFLICT (id) DO UPDATE SET
11
- window_start = CASE WHEN auth_sign_in_budget.window_start <= ?2 THEN ?1 ELSE auth_sign_in_budget.window_start END,
12
- admitted = CASE WHEN auth_sign_in_budget.window_start <= ?2 THEN 1 ELSE auth_sign_in_budget.admitted + 1 END
13
- WHERE auth_sign_in_budget.window_start <= ?2 OR auth_sign_in_budget.admitted < ?3
14
- RETURNING admitted`,sweepBudget:`DELETE FROM auth_link_budget WHERE window_start <= ?`,sweepTokens:`DELETE FROM auth_tokens WHERE expires_at <= ?`,sweepUnverified:`DELETE FROM auth_users WHERE last_sign_in_at IS NULL AND NOT EXISTS (SELECT 1 FROM auth_tokens t WHERE t.user_id = auth_users.id) AND NOT EXISTS (SELECT 1 FROM auth_link_budget b WHERE b.user_id = auth_users.id)`};async function ensureUser(t,n){let r=await e(()=>t.prepare(i.ensureUser).bind(crypto.randomUUID(),n,new Date().toISOString()).first());if(r===null)throw Error(`The account row could not be ensured.`);return r}async function spendSignInBudget(t,n){return e(async()=>{if(await t.prepare(i.spendSignInBudget).bind(n,n-6e4,20).first()!==null)return null;let e=await t.prepare(i.signInWindow).first();return Math.max(1,Math.ceil(((e?.window_start??n)+r-n)/1e3))})}async function spendLinkBudget(t,r,a){return e(async()=>{let e=a-n;if(await t.prepare(i.spendBudget).bind(r,a,e,3).first()!==null)return null;let o=((await t.prepare(i.budgetWindow).bind(r).first())?.window_start??a)+n;return Math.max(1,Math.ceil((o-a)/1e3))})}async function insertToken(n,r,a,o){await e(()=>n.prepare(i.insertToken).bind(r,a,o+t).run())}async function deleteToken(t,n){await e(()=>t.prepare(i.deleteToken).bind(n).run())}async function consumeToken(t,n,r){return e(()=>t.prepare(i.consumeToken).bind(new Date(r).toISOString(),n,r).first())}function revokeSessions(t,n,r){return e(()=>t.prepare(i.revokeSessions).bind(n,r).first())}async function authReadiness(t){return e(async()=>{for(let[e,n]of Object.entries({auth_link_budget:[`user_id`,`window_start`,`sent`],auth_schema_meta:[`id`,`applied`],auth_sign_in_budget:[`id`,`window_start`,`admitted`],auth_tokens:[`token_hash`,`user_id`,`expires_at`,`redeemed_at`],auth_users:[`id`,`email`,`created_at`,`last_sign_in_at`,`session_version`]})){let r=await t.prepare(`PRAGMA table_info(${e})`).all();if(!n.every(e=>r.results?.some(t=>t.name===e)))return null}if((await t.prepare(`SELECT name FROM sqlite_master WHERE type = 'trigger' AND name IN ('auth_token_redeemed', 'auth_sessions_revoked')`).all()).results?.length!==2)return null;let e=await t.prepare(`SELECT applied FROM auth_schema_meta WHERE id = 1`).first();return await t.prepare(`SELECT u.id, u.email, u.created_at, u.last_sign_in_at, u.session_version,
15
- t.token_hash, t.user_id, t.expires_at, t.redeemed_at,
16
- b.user_id, b.window_start, b.sent, s.id, s.window_start, s.admitted
17
- FROM auth_users u LEFT JOIN auth_tokens t ON 0 LEFT JOIN auth_link_budget b ON 0
18
- LEFT JOIN auth_sign_in_budget s ON 0 LIMIT 1`).first(),typeof e?.applied==`number`&&Number.isSafeInteger(e.applied)&&e.applied>=0?e.applied:null})}function readAccount(t,n){return e(()=>t.prepare(i.account).bind(n).first())}async function deleteAccount(t,n){await e(()=>t.prepare(i.deleteAccount).bind(n).run())}async function sweep(t,r){return(await e(()=>t.batch([t.prepare(i.sweepTokens).bind(r),t.prepare(i.sweepBudget).bind(r-n),t.prepare(i.sweepUnverified)]))).reduce((e,t)=>e+(t.meta?.changes??0),0)}export{t as LINK_TTL_MS,n as LINK_WINDOW_MS,r as SIGN_IN_WINDOW_MS,authReadiness,consumeToken,deleteAccount,deleteToken,ensureUser,insertToken,readAccount,revokeSessions,spendLinkBudget,spendSignInBudget,sweep};
1
+ import { databaseOperation as e } from "./errors.js";
2
+ //#region src/auth/store.ts
3
+ var t = 9e5, n = 36e5, r = 6e4, i = {
4
+ account: "SELECT id, email, created_at, last_sign_in_at, session_version FROM auth_users WHERE id = ?",
5
+ budgetWindow: "SELECT window_start FROM auth_link_budget WHERE user_id = ?",
6
+ consumeToken: "UPDATE auth_tokens SET redeemed_at = ?1 WHERE token_hash = ?2 AND expires_at > ?3\nRETURNING user_id AS id, (SELECT email FROM auth_users WHERE id = user_id) AS email,\n (SELECT session_version FROM auth_users WHERE id = user_id) AS sessionVersion",
7
+ deleteAccount: "DELETE FROM auth_users WHERE id = ?",
8
+ deleteToken: "DELETE FROM auth_tokens WHERE token_hash = ?",
9
+ ensureUser: "INSERT INTO auth_users (id, email, created_at, last_sign_in_at) VALUES (?, ?, ?, NULL) ON CONFLICT (email) DO UPDATE SET email = excluded.email RETURNING id, email",
10
+ insertToken: "INSERT INTO auth_tokens (token_hash, user_id, expires_at) VALUES (?, ?, ?)",
11
+ revokeSessions: "UPDATE auth_users SET session_version = session_version + 1\nWHERE id = ? AND session_version = ? RETURNING session_version",
12
+ signInWindow: "SELECT window_start FROM auth_sign_in_budget WHERE id = 1",
13
+ spendBudget: "INSERT INTO auth_link_budget (user_id, window_start, sent) VALUES (?1, ?2, 1)\nON CONFLICT (user_id) DO UPDATE SET\n window_start = CASE WHEN auth_link_budget.window_start <= ?3 THEN ?2 ELSE auth_link_budget.window_start END,\n sent = CASE WHEN auth_link_budget.window_start <= ?3 THEN 1 ELSE auth_link_budget.sent + 1 END\nWHERE auth_link_budget.window_start <= ?3 OR auth_link_budget.sent < ?4\nRETURNING sent",
14
+ spendSignInBudget: "INSERT INTO auth_sign_in_budget (id, window_start, admitted) VALUES (1, ?1, 1)\nON CONFLICT (id) DO UPDATE SET\n window_start = CASE WHEN auth_sign_in_budget.window_start <= ?2 THEN ?1 ELSE auth_sign_in_budget.window_start END,\n admitted = CASE WHEN auth_sign_in_budget.window_start <= ?2 THEN 1 ELSE auth_sign_in_budget.admitted + 1 END\nWHERE auth_sign_in_budget.window_start <= ?2 OR auth_sign_in_budget.admitted < ?3\nRETURNING admitted",
15
+ sweepBudget: "DELETE FROM auth_link_budget WHERE window_start <= ?",
16
+ sweepTokens: "DELETE FROM auth_tokens WHERE expires_at <= ?",
17
+ sweepUnverified: "DELETE FROM auth_users WHERE last_sign_in_at IS NULL AND NOT EXISTS (SELECT 1 FROM auth_tokens t WHERE t.user_id = auth_users.id) AND NOT EXISTS (SELECT 1 FROM auth_link_budget b WHERE b.user_id = auth_users.id)"
18
+ };
19
+ async function ensureUser(t, n) {
20
+ let r = await e(() => t.prepare(i.ensureUser).bind(crypto.randomUUID(), n, (/* @__PURE__ */ new Date()).toISOString()).first());
21
+ if (r === null) throw Error("The account row could not be ensured.");
22
+ return r;
23
+ }
24
+ async function spendSignInBudget(t, n) {
25
+ return e(async () => {
26
+ if (await t.prepare(i.spendSignInBudget).bind(n, n - 6e4, 20).first() !== null) return null;
27
+ let e = await t.prepare(i.signInWindow).first();
28
+ return Math.max(1, Math.ceil(((e?.window_start ?? n) + r - n) / 1e3));
29
+ });
30
+ }
31
+ async function spendLinkBudget(t, r, a) {
32
+ return e(async () => {
33
+ let e = a - n;
34
+ if (await t.prepare(i.spendBudget).bind(r, a, e, 3).first() !== null) return null;
35
+ let o = ((await t.prepare(i.budgetWindow).bind(r).first())?.window_start ?? a) + n;
36
+ return Math.max(1, Math.ceil((o - a) / 1e3));
37
+ });
38
+ }
39
+ async function insertToken(n, r, a, o) {
40
+ await e(() => n.prepare(i.insertToken).bind(r, a, o + t).run());
41
+ }
42
+ async function deleteToken(t, n) {
43
+ await e(() => t.prepare(i.deleteToken).bind(n).run());
44
+ }
45
+ async function consumeToken(t, n, r) {
46
+ return e(() => t.prepare(i.consumeToken).bind(new Date(r).toISOString(), n, r).first());
47
+ }
48
+ function revokeSessions(t, n, r) {
49
+ return e(() => t.prepare(i.revokeSessions).bind(n, r).first());
50
+ }
51
+ async function authReadiness(t) {
52
+ return e(async () => {
53
+ for (let [e, n] of Object.entries({
54
+ auth_link_budget: [
55
+ "user_id",
56
+ "window_start",
57
+ "sent"
58
+ ],
59
+ auth_schema_meta: ["id", "applied"],
60
+ auth_sign_in_budget: [
61
+ "id",
62
+ "window_start",
63
+ "admitted"
64
+ ],
65
+ auth_tokens: [
66
+ "token_hash",
67
+ "user_id",
68
+ "expires_at",
69
+ "redeemed_at"
70
+ ],
71
+ auth_users: [
72
+ "id",
73
+ "email",
74
+ "created_at",
75
+ "last_sign_in_at",
76
+ "session_version"
77
+ ]
78
+ })) {
79
+ let r = await t.prepare(`PRAGMA table_info(${e})`).all();
80
+ if (!n.every((e) => r.results?.some((t) => t.name === e))) return null;
81
+ }
82
+ if ((await t.prepare("SELECT name FROM sqlite_master WHERE type = 'trigger' AND name IN ('auth_token_redeemed', 'auth_sessions_revoked')").all()).results?.length !== 2) return null;
83
+ let e = await t.prepare("SELECT applied FROM auth_schema_meta WHERE id = 1").first();
84
+ return await t.prepare("SELECT u.id, u.email, u.created_at, u.last_sign_in_at, u.session_version,\n t.token_hash, t.user_id, t.expires_at, t.redeemed_at,\n b.user_id, b.window_start, b.sent, s.id, s.window_start, s.admitted\n FROM auth_users u LEFT JOIN auth_tokens t ON 0 LEFT JOIN auth_link_budget b ON 0\n LEFT JOIN auth_sign_in_budget s ON 0 LIMIT 1").first(), typeof e?.applied == "number" && Number.isSafeInteger(e.applied) && e.applied >= 0 ? e.applied : null;
85
+ });
86
+ }
87
+ function readAccount(t, n) {
88
+ return e(() => t.prepare(i.account).bind(n).first());
89
+ }
90
+ async function deleteAccount(t, n) {
91
+ await e(() => t.prepare(i.deleteAccount).bind(n).run());
92
+ }
93
+ async function sweep(t, r) {
94
+ return (await e(() => t.batch([
95
+ t.prepare(i.sweepTokens).bind(r),
96
+ t.prepare(i.sweepBudget).bind(r - n),
97
+ t.prepare(i.sweepUnverified)
98
+ ]))).reduce((e, t) => e + (t.meta?.changes ?? 0), 0);
99
+ }
100
+ //#endregion
101
+ export { t as LINK_TTL_MS, n as LINK_WINDOW_MS, r as SIGN_IN_WINDOW_MS, authReadiness, consumeToken, deleteAccount, deleteToken, ensureUser, insertToken, readAccount, revokeSessions, spendLinkBudget, spendSignInBudget, sweep };
102
+
19
103
  //# sourceMappingURL=store.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","names":[],"sources":["../../src/auth/store.ts"],"sourcesContent":["import type { D1Like } from '../worker/env';\nimport { databaseOperation } from './errors';\n\n/**\n * The issuer's storage, written against exactly `migrations/auth/0001_auth.sql`.\n * Token rows are single-use credentials only; the send allowance lives in its own\n * durable table, so consuming or expiring a link never refunds the budget.\n */\nexport const LINK_TTL_MS = 15 * 60 * 1000;\nexport const LINK_WINDOW_MS = 60 * 60 * 1000;\nexport const LINKS_PER_WINDOW = 3;\nexport const SIGN_IN_WINDOW_MS = 60_000;\nexport const SIGN_INS_PER_WINDOW = 20;\n\nexport interface AccountRow {\n created_at: string;\n email: string;\n id: string;\n last_sign_in_at: string | null;\n session_version: number;\n}\n\nconst SQL = {\n account: 'SELECT id, email, created_at, last_sign_in_at, session_version FROM auth_users WHERE id = ?',\n budgetWindow: 'SELECT window_start FROM auth_link_budget WHERE user_id = ?',\n consumeToken: `UPDATE auth_tokens SET redeemed_at = ?1 WHERE token_hash = ?2 AND expires_at > ?3\nRETURNING user_id AS id, (SELECT email FROM auth_users WHERE id = user_id) AS email,\n (SELECT session_version FROM auth_users WHERE id = user_id) AS sessionVersion`,\n deleteAccount: 'DELETE FROM auth_users WHERE id = ?',\n deleteToken: 'DELETE FROM auth_tokens WHERE token_hash = ?',\n // The no-op update makes RETURNING answer for an existing row too.\n ensureUser:\n 'INSERT INTO auth_users (id, email, created_at, last_sign_in_at) VALUES (?, ?, ?, NULL) ON CONFLICT (email) DO UPDATE SET email = excluded.email RETURNING id, email',\n insertToken: 'INSERT INTO auth_tokens (token_hash, user_id, expires_at) VALUES (?, ?, ?)',\n revokeSessions: `UPDATE auth_users SET session_version = session_version + 1\nWHERE id = ? AND session_version = ? RETURNING session_version`,\n signInWindow: 'SELECT window_start FROM auth_sign_in_budget WHERE id = 1',\n // One atomic statement opens a window, counts within it, or refuses: no row back\n // means the allowance is spent. Concurrent requests cannot both pass the bound.\n spendBudget: `INSERT INTO auth_link_budget (user_id, window_start, sent) VALUES (?1, ?2, 1)\nON CONFLICT (user_id) DO UPDATE SET\n window_start = CASE WHEN auth_link_budget.window_start <= ?3 THEN ?2 ELSE auth_link_budget.window_start END,\n sent = CASE WHEN auth_link_budget.window_start <= ?3 THEN 1 ELSE auth_link_budget.sent + 1 END\nWHERE auth_link_budget.window_start <= ?3 OR auth_link_budget.sent < ?4\nRETURNING sent`,\n spendSignInBudget: `INSERT INTO auth_sign_in_budget (id, window_start, admitted) VALUES (1, ?1, 1)\nON CONFLICT (id) DO UPDATE SET\n window_start = CASE WHEN auth_sign_in_budget.window_start <= ?2 THEN ?1 ELSE auth_sign_in_budget.window_start END,\n admitted = CASE WHEN auth_sign_in_budget.window_start <= ?2 THEN 1 ELSE auth_sign_in_budget.admitted + 1 END\nWHERE auth_sign_in_budget.window_start <= ?2 OR auth_sign_in_budget.admitted < ?3\nRETURNING admitted`,\n sweepBudget: 'DELETE FROM auth_link_budget WHERE window_start <= ?',\n sweepTokens: 'DELETE FROM auth_tokens WHERE expires_at <= ?',\n // Rows for emails that never completed a sign-in leave once nothing references them.\n sweepUnverified:\n 'DELETE FROM auth_users WHERE last_sign_in_at IS NULL AND NOT EXISTS (SELECT 1 FROM auth_tokens t WHERE t.user_id = auth_users.id) AND NOT EXISTS (SELECT 1 FROM auth_link_budget b WHERE b.user_id = auth_users.id)',\n} as const;\n\nexport async function ensureUser(db: D1Like, email: string): Promise<{ email: string; id: string }> {\n const row = await databaseOperation(() =>\n db\n .prepare(SQL.ensureUser)\n .bind(crypto.randomUUID(), email, new Date().toISOString())\n .first<{ email: string; id: string }>(),\n );\n if (row === null) throw new Error('The account row could not be ensured.');\n return row;\n}\n\n/** The deployment-wide admission budget is spent before account creation or delivery. */\nexport async function spendSignInBudget(db: D1Like, nowMs: number): Promise<number | null> {\n return databaseOperation(async () => {\n const spent = await db\n .prepare(SQL.spendSignInBudget)\n .bind(nowMs, nowMs - SIGN_IN_WINDOW_MS, SIGN_INS_PER_WINDOW)\n .first<{ admitted: number }>();\n if (spent !== null) return null;\n const open = await db.prepare(SQL.signInWindow).first<{ window_start: number }>();\n return Math.max(1, Math.ceil(((open?.window_start ?? nowMs) + SIGN_IN_WINDOW_MS - nowMs) / 1000));\n });\n}\n\n/** Spends one send from the hourly allowance; returns the seconds until it reopens when spent. */\nexport async function spendLinkBudget(db: D1Like, userId: string, nowMs: number): Promise<number | null> {\n return databaseOperation(async () => {\n const windowFloor = nowMs - LINK_WINDOW_MS;\n const spent = await db\n .prepare(SQL.spendBudget)\n .bind(userId, nowMs, windowFloor, LINKS_PER_WINDOW)\n .first<{ sent: number }>();\n if (spent !== null) return null;\n const open = await db.prepare(SQL.budgetWindow).bind(userId).first<{ window_start: number }>();\n const reopensAt = (open?.window_start ?? nowMs) + LINK_WINDOW_MS;\n return Math.max(1, Math.ceil((reopensAt - nowMs) / 1000));\n });\n}\n\nexport async function insertToken(db: D1Like, tokenHash: string, userId: string, nowMs: number): Promise<void> {\n await databaseOperation(() =>\n db\n .prepare(SQL.insertToken)\n .bind(tokenHash, userId, nowMs + LINK_TTL_MS)\n .run(),\n );\n}\n\nexport async function deleteToken(db: D1Like, tokenHash: string): Promise<void> {\n await databaseOperation(() => db.prepare(SQL.deleteToken).bind(tokenHash).run());\n}\n\n/** Single-use consume: the unexpired row answers once and is gone after; the account is stamped signed in. */\nexport async function consumeToken(\n db: D1Like,\n tokenHash: string,\n nowMs: number,\n): Promise<{ email: string; id: string; sessionVersion: number } | null> {\n return databaseOperation(() =>\n db\n .prepare(SQL.consumeToken)\n .bind(new Date(nowMs).toISOString(), tokenHash, nowMs)\n .first<{ email: string; id: string; sessionVersion: number }>(),\n );\n}\n\n/** The trigger deletes outstanding links within this same atomic version change. */\nexport function revokeSessions(\n db: D1Like,\n subject: string,\n version: number,\n): Promise<{ session_version: number } | null> {\n return databaseOperation(() =>\n db.prepare(SQL.revokeSessions).bind(subject, version).first<{ session_version: number }>(),\n );\n}\n\nexport async function authReadiness(db: D1Like): Promise<number | null> {\n return databaseOperation(async () => {\n const columns = {\n auth_link_budget: ['user_id', 'window_start', 'sent'],\n auth_schema_meta: ['id', 'applied'],\n auth_sign_in_budget: ['id', 'window_start', 'admitted'],\n auth_tokens: ['token_hash', 'user_id', 'expires_at', 'redeemed_at'],\n auth_users: ['id', 'email', 'created_at', 'last_sign_in_at', 'session_version'],\n } as const;\n for (const [table, expected] of Object.entries(columns)) {\n const actual = await db.prepare(`PRAGMA table_info(${table})`).all<{ name: string }>();\n if (!expected.every((name) => actual.results?.some((column) => column.name === name))) return null;\n }\n const triggers = await db\n .prepare(\n \"SELECT name FROM sqlite_master WHERE type = 'trigger' AND name IN ('auth_token_redeemed', 'auth_sessions_revoked')\",\n )\n .all<{ name: string }>();\n if (triggers.results?.length !== 2) return null;\n const stamp = await db.prepare('SELECT applied FROM auth_schema_meta WHERE id = 1').first<{ applied: number }>();\n await db\n .prepare(`SELECT u.id, u.email, u.created_at, u.last_sign_in_at, u.session_version,\n t.token_hash, t.user_id, t.expires_at, t.redeemed_at,\n b.user_id, b.window_start, b.sent, s.id, s.window_start, s.admitted\n FROM auth_users u LEFT JOIN auth_tokens t ON 0 LEFT JOIN auth_link_budget b ON 0\n LEFT JOIN auth_sign_in_budget s ON 0 LIMIT 1`)\n .first();\n return typeof stamp?.applied === 'number' && Number.isSafeInteger(stamp.applied) && stamp.applied >= 0\n ? stamp.applied\n : null;\n });\n}\n\nexport function readAccount(db: D1Like, userId: string): Promise<AccountRow | null> {\n return databaseOperation(() => db.prepare(SQL.account).bind(userId).first<AccountRow>());\n}\n\n/** Removes the account; FK cascades take its tokens and send budget. Idempotent. */\nexport async function deleteAccount(db: D1Like, userId: string): Promise<void> {\n await databaseOperation(() => db.prepare(SQL.deleteAccount).bind(userId).run());\n}\n\n/** The scheduled sweep: expired links, closed send windows, then never-verified accounts left unreferenced. */\nexport async function sweep(db: D1Like, nowMs: number): Promise<number> {\n const results = await databaseOperation(() =>\n db.batch([\n db.prepare(SQL.sweepTokens).bind(nowMs),\n db.prepare(SQL.sweepBudget).bind(nowMs - LINK_WINDOW_MS),\n db.prepare(SQL.sweepUnverified),\n ]),\n );\n return results.reduce((total, result) => total + (result.meta?.changes ?? 0), 0);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"store.js","names":["LINK_TTL_MS","LINK_WINDOW_MS","SIGN_IN_WINDOW_MS","SQL","row","databaseOperation","db","email","nowMs","open","windowFloor","userId","reopensAt","tokenHash","subject","version","table","expected","actual","column","name","stamp","total","result"],"sources":["../../src/auth/store.ts"],"sourcesContent":["import type { D1Like } from '../worker/env';\nimport { databaseOperation } from './errors';\n\n/**\n * The issuer's storage, written against exactly `migrations/auth/0001_auth.sql`.\n * Token rows are single-use credentials only; the send allowance lives in its own\n * durable table, so consuming or expiring a link never refunds the budget.\n */\nexport const LINK_TTL_MS = 15 * 60 * 1000;\nexport const LINK_WINDOW_MS = 60 * 60 * 1000;\nexport const LINKS_PER_WINDOW = 3;\nexport const SIGN_IN_WINDOW_MS = 60_000;\nexport const SIGN_INS_PER_WINDOW = 20;\n\nexport interface AccountRow {\n created_at: string;\n email: string;\n id: string;\n last_sign_in_at: string | null;\n session_version: number;\n}\n\nconst SQL = {\n account: 'SELECT id, email, created_at, last_sign_in_at, session_version FROM auth_users WHERE id = ?',\n budgetWindow: 'SELECT window_start FROM auth_link_budget WHERE user_id = ?',\n consumeToken: `UPDATE auth_tokens SET redeemed_at = ?1 WHERE token_hash = ?2 AND expires_at > ?3\nRETURNING user_id AS id, (SELECT email FROM auth_users WHERE id = user_id) AS email,\n (SELECT session_version FROM auth_users WHERE id = user_id) AS sessionVersion`,\n deleteAccount: 'DELETE FROM auth_users WHERE id = ?',\n deleteToken: 'DELETE FROM auth_tokens WHERE token_hash = ?',\n // The no-op update makes RETURNING answer for an existing row too.\n ensureUser:\n 'INSERT INTO auth_users (id, email, created_at, last_sign_in_at) VALUES (?, ?, ?, NULL) ON CONFLICT (email) DO UPDATE SET email = excluded.email RETURNING id, email',\n insertToken: 'INSERT INTO auth_tokens (token_hash, user_id, expires_at) VALUES (?, ?, ?)',\n revokeSessions: `UPDATE auth_users SET session_version = session_version + 1\nWHERE id = ? AND session_version = ? RETURNING session_version`,\n signInWindow: 'SELECT window_start FROM auth_sign_in_budget WHERE id = 1',\n // One atomic statement opens a window, counts within it, or refuses: no row back\n // means the allowance is spent. Concurrent requests cannot both pass the bound.\n spendBudget: `INSERT INTO auth_link_budget (user_id, window_start, sent) VALUES (?1, ?2, 1)\nON CONFLICT (user_id) DO UPDATE SET\n window_start = CASE WHEN auth_link_budget.window_start <= ?3 THEN ?2 ELSE auth_link_budget.window_start END,\n sent = CASE WHEN auth_link_budget.window_start <= ?3 THEN 1 ELSE auth_link_budget.sent + 1 END\nWHERE auth_link_budget.window_start <= ?3 OR auth_link_budget.sent < ?4\nRETURNING sent`,\n spendSignInBudget: `INSERT INTO auth_sign_in_budget (id, window_start, admitted) VALUES (1, ?1, 1)\nON CONFLICT (id) DO UPDATE SET\n window_start = CASE WHEN auth_sign_in_budget.window_start <= ?2 THEN ?1 ELSE auth_sign_in_budget.window_start END,\n admitted = CASE WHEN auth_sign_in_budget.window_start <= ?2 THEN 1 ELSE auth_sign_in_budget.admitted + 1 END\nWHERE auth_sign_in_budget.window_start <= ?2 OR auth_sign_in_budget.admitted < ?3\nRETURNING admitted`,\n sweepBudget: 'DELETE FROM auth_link_budget WHERE window_start <= ?',\n sweepTokens: 'DELETE FROM auth_tokens WHERE expires_at <= ?',\n // Rows for emails that never completed a sign-in leave once nothing references them.\n sweepUnverified:\n 'DELETE FROM auth_users WHERE last_sign_in_at IS NULL AND NOT EXISTS (SELECT 1 FROM auth_tokens t WHERE t.user_id = auth_users.id) AND NOT EXISTS (SELECT 1 FROM auth_link_budget b WHERE b.user_id = auth_users.id)',\n} as const;\n\nexport async function ensureUser(db: D1Like, email: string): Promise<{ email: string; id: string }> {\n const row = await databaseOperation(() =>\n db\n .prepare(SQL.ensureUser)\n .bind(crypto.randomUUID(), email, new Date().toISOString())\n .first<{ email: string; id: string }>(),\n );\n if (row === null) throw new Error('The account row could not be ensured.');\n return row;\n}\n\n/** The deployment-wide admission budget is spent before account creation or delivery. */\nexport async function spendSignInBudget(db: D1Like, nowMs: number): Promise<number | null> {\n return databaseOperation(async () => {\n const spent = await db\n .prepare(SQL.spendSignInBudget)\n .bind(nowMs, nowMs - SIGN_IN_WINDOW_MS, SIGN_INS_PER_WINDOW)\n .first<{ admitted: number }>();\n if (spent !== null) return null;\n const open = await db.prepare(SQL.signInWindow).first<{ window_start: number }>();\n return Math.max(1, Math.ceil(((open?.window_start ?? nowMs) + SIGN_IN_WINDOW_MS - nowMs) / 1000));\n });\n}\n\n/** Spends one send from the hourly allowance; returns the seconds until it reopens when spent. */\nexport async function spendLinkBudget(db: D1Like, userId: string, nowMs: number): Promise<number | null> {\n return databaseOperation(async () => {\n const windowFloor = nowMs - LINK_WINDOW_MS;\n const spent = await db\n .prepare(SQL.spendBudget)\n .bind(userId, nowMs, windowFloor, LINKS_PER_WINDOW)\n .first<{ sent: number }>();\n if (spent !== null) return null;\n const open = await db.prepare(SQL.budgetWindow).bind(userId).first<{ window_start: number }>();\n const reopensAt = (open?.window_start ?? nowMs) + LINK_WINDOW_MS;\n return Math.max(1, Math.ceil((reopensAt - nowMs) / 1000));\n });\n}\n\nexport async function insertToken(db: D1Like, tokenHash: string, userId: string, nowMs: number): Promise<void> {\n await databaseOperation(() =>\n db\n .prepare(SQL.insertToken)\n .bind(tokenHash, userId, nowMs + LINK_TTL_MS)\n .run(),\n );\n}\n\nexport async function deleteToken(db: D1Like, tokenHash: string): Promise<void> {\n await databaseOperation(() => db.prepare(SQL.deleteToken).bind(tokenHash).run());\n}\n\n/** Single-use consume: the unexpired row answers once and is gone after; the account is stamped signed in. */\nexport async function consumeToken(\n db: D1Like,\n tokenHash: string,\n nowMs: number,\n): Promise<{ email: string; id: string; sessionVersion: number } | null> {\n return databaseOperation(() =>\n db\n .prepare(SQL.consumeToken)\n .bind(new Date(nowMs).toISOString(), tokenHash, nowMs)\n .first<{ email: string; id: string; sessionVersion: number }>(),\n );\n}\n\n/** The trigger deletes outstanding links within this same atomic version change. */\nexport function revokeSessions(\n db: D1Like,\n subject: string,\n version: number,\n): Promise<{ session_version: number } | null> {\n return databaseOperation(() =>\n db.prepare(SQL.revokeSessions).bind(subject, version).first<{ session_version: number }>(),\n );\n}\n\nexport async function authReadiness(db: D1Like): Promise<number | null> {\n return databaseOperation(async () => {\n const columns = {\n auth_link_budget: ['user_id', 'window_start', 'sent'],\n auth_schema_meta: ['id', 'applied'],\n auth_sign_in_budget: ['id', 'window_start', 'admitted'],\n auth_tokens: ['token_hash', 'user_id', 'expires_at', 'redeemed_at'],\n auth_users: ['id', 'email', 'created_at', 'last_sign_in_at', 'session_version'],\n } as const;\n for (const [table, expected] of Object.entries(columns)) {\n const actual = await db.prepare(`PRAGMA table_info(${table})`).all<{ name: string }>();\n if (!expected.every((name) => actual.results?.some((column) => column.name === name))) return null;\n }\n const triggers = await db\n .prepare(\n \"SELECT name FROM sqlite_master WHERE type = 'trigger' AND name IN ('auth_token_redeemed', 'auth_sessions_revoked')\",\n )\n .all<{ name: string }>();\n if (triggers.results?.length !== 2) return null;\n const stamp = await db.prepare('SELECT applied FROM auth_schema_meta WHERE id = 1').first<{ applied: number }>();\n await db\n .prepare(`SELECT u.id, u.email, u.created_at, u.last_sign_in_at, u.session_version,\n t.token_hash, t.user_id, t.expires_at, t.redeemed_at,\n b.user_id, b.window_start, b.sent, s.id, s.window_start, s.admitted\n FROM auth_users u LEFT JOIN auth_tokens t ON 0 LEFT JOIN auth_link_budget b ON 0\n LEFT JOIN auth_sign_in_budget s ON 0 LIMIT 1`)\n .first();\n return typeof stamp?.applied === 'number' && Number.isSafeInteger(stamp.applied) && stamp.applied >= 0\n ? stamp.applied\n : null;\n });\n}\n\nexport function readAccount(db: D1Like, userId: string): Promise<AccountRow | null> {\n return databaseOperation(() => db.prepare(SQL.account).bind(userId).first<AccountRow>());\n}\n\n/** Removes the account; FK cascades take its tokens and send budget. Idempotent. */\nexport async function deleteAccount(db: D1Like, userId: string): Promise<void> {\n await databaseOperation(() => db.prepare(SQL.deleteAccount).bind(userId).run());\n}\n\n/** The scheduled sweep: expired links, closed send windows, then never-verified accounts left unreferenced. */\nexport async function sweep(db: D1Like, nowMs: number): Promise<number> {\n const results = await databaseOperation(() =>\n db.batch([\n db.prepare(SQL.sweepTokens).bind(nowMs),\n db.prepare(SQL.sweepBudget).bind(nowMs - LINK_WINDOW_MS),\n db.prepare(SQL.sweepUnverified),\n ]),\n );\n return results.reduce((total, result) => total + (result.meta?.changes ?? 0), 0);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"worker.cjs","names":[],"sources":["../../src/auth/worker.ts"],"sourcesContent":["import { errors, jwtVerify, SignJWT } from 'jose';\nimport { sha256Hex } from '../_crypto';\nimport {\n ACCOUNT_HEADER,\n ARCHIVE_OPERATOR_ROLE,\n type ArchiveErrorCode,\n AUTH_SCHEMA_VERSION,\n archiveAuthHealthSchema,\n archiveErrorResponseSchema,\n isWellFormedText,\n OPERATOR_SESSION_MAX_AGE_SECONDS,\n PUBLISHER_HEADER,\n REQUEST_MARKER_HEADER,\n SESSION_COOKIE_NAME,\n SESSION_MAX_AGE_SECONDS,\n sessionCookieValue,\n uuidSchema,\n} from '../contract';\nimport { serviceRequest } from '../worker/_service';\nimport {\n ApiError,\n badRequest,\n expiredSessionCookie,\n forbidden,\n json,\n noStore,\n queryParams,\n readJsonBody,\n sessionToken,\n toErrorResponse,\n unauthenticated,\n} from '../worker/http';\nimport type { ArchiveExecutionContext } from '../worker/worker';\nimport { confirmationPage, escapeHtml } from './_confirmation';\nimport { type AuthConfig, type AuthWorkerEnv, authConfig, isWellFormedEmail } from './config';\nimport { type AuthDiagnostic, AuthOperationError } from './errors';\nimport {\n authReadiness,\n consumeToken,\n deleteAccount,\n deleteToken,\n ensureUser,\n insertToken,\n LINK_TTL_MS,\n readAccount,\n revokeSessions,\n spendLinkBudget,\n spendSignInBudget,\n sweep,\n} from './store';\n\n/**\n * The reference identity issuer: passwordless email accounts. A POST with an email\n * address ensures the account row and sends a single-use magic link; consuming it\n * signs the browser in by setting the `archive_session` cookie a catalog Worker\n * verifies — an RS256 JWT minted by the active private key, verifiable through\n * `/jwks.json` alongside any configured retired public keys. `OPERATOR_EMAILS` is\n * the one operator-membership truth; operator sessions expire after one hour,\n * bounding authority from previously minted role claims. The account routes export the issuer's\n * own record and erase it together with the catalog profile.\n */\nexport interface AuthWorkerHandler<E extends AuthWorkerEnv = AuthWorkerEnv> {\n fetch(request: Request, env: E, ctx?: ArchiveExecutionContext): Promise<Response>;\n /** The cron sweep: expired links, closed send windows and never-verified accounts. */\n scheduled(controller: unknown, env: E, ctx?: ArchiveExecutionContext): Promise<void>;\n tap(handler: (event: AuthEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;\n}\n\nexport type AuthEvent =\n | {\n readonly type: 'request';\n readonly requestId: string;\n readonly route:\n | '/auth/account'\n | '/auth/jwks.json'\n | '/auth/sign-in'\n | '/auth/verify'\n | '/auth/session'\n | '/auth/sign-out-all'\n | '/auth/health'\n | 'unmatched';\n readonly status: number;\n readonly code: ArchiveErrorCode | 'ok';\n readonly ms: number;\n readonly diagnostic?: AuthDiagnostic;\n }\n | { readonly type: 'scheduled'; readonly status: 'ok'; readonly removed: number }\n | { readonly type: 'scheduled'; readonly status: 'failed'; readonly diagnostic: AuthDiagnostic };\n\nconst ROUTES = {\n account: /^\\/api\\/v1\\/auth\\/account$/,\n health: /^\\/api\\/v1\\/auth\\/health$/,\n jwks: /^\\/api\\/v1\\/auth\\/jwks\\.json$/,\n session: /^\\/api\\/v1\\/auth\\/session$/,\n signIn: /^\\/api\\/v1\\/auth\\/sign-in$/,\n signOutAll: /^\\/api\\/v1\\/auth\\/sign-out-all$/,\n verify: /^\\/api\\/v1\\/auth\\/verify$/,\n} as const;\n\nconst ROUTE_METHODS: Record<keyof typeof ROUTES, readonly string[]> = {\n account: ['GET', 'DELETE'],\n health: ['GET'],\n jwks: ['GET'],\n session: ['GET'],\n signIn: ['POST'],\n signOutAll: ['POST'],\n verify: ['GET', 'POST'],\n};\n\nconst ROUTE_LABELS = {\n account: '/auth/account',\n health: '/auth/health',\n jwks: '/auth/jwks.json',\n session: '/auth/session',\n signIn: '/auth/sign-in',\n signOutAll: '/auth/sign-out-all',\n verify: '/auth/verify',\n} as const;\n\nconst matchRoute = (request: Request): keyof typeof ROUTES | undefined =>\n (Object.keys(ROUTES) as (keyof typeof ROUTES)[]).find((id) => ROUTES[id].test(new URL(request.url).pathname));\n\nconst TOKEN_BYTES = 32;\n// Base64url of TOKEN_BYTES random bytes, unpadded.\nconst LINK_TOKEN = /^[A-Za-z0-9_-]{43}$/;\nconst PUBLISHER_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;\nconst SUBJECT_MAX = 256;\nconst CLOCK_TOLERANCE_SECONDS = 5;\n\nconst randomToken = (): string =>\n btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(TOKEN_BYTES))))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replace(/=+$/, '');\n\nconst unavailable = (): ApiError => new AuthOperationError(503, 'catalog');\n\n/** Exact allowed Origin plus the request marker, like the catalog's mutation guard. */\nfunction requireMutationGuard(request: Request, config: AuthConfig, allowPublicOrigin = false): string {\n const origin = request.headers.get('origin');\n if (origin === null || (!config.allowedOrigins.has(origin) && !(allowPublicOrigin && origin === config.publicOrigin)))\n throw forbidden();\n if (request.headers.get(REQUEST_MARKER_HEADER) !== '1') throw forbidden();\n return origin;\n}\n\nfunction corsHeaders(request: Request, config: AuthConfig): Record<string, string> {\n const origin = request.headers.get('origin');\n if (\n origin === null ||\n (!config.allowedOrigins.has(origin) && !(matchRoute(request) === 'verify' && origin === config.publicOrigin))\n )\n return {};\n return { 'access-control-allow-credentials': 'true', 'access-control-allow-origin': origin, vary: 'Origin' };\n}\n\nfunction preflight(request: Request, config: AuthConfig, methods: readonly string[]): Response {\n const cors = corsHeaders(request, config);\n if (Object.keys(cors).length === 0) throw forbidden();\n return new Response(null, {\n headers: {\n ...cors,\n 'access-control-allow-headers': `Content-Type, ${ACCOUNT_HEADER}, ${PUBLISHER_HEADER}, ${REQUEST_MARKER_HEADER}`,\n 'access-control-allow-methods': [...methods, 'OPTIONS'].join(', '),\n 'access-control-max-age': '600',\n },\n status: 204,\n });\n}\n\nconst secure = (config: AuthConfig): boolean => config.appEnv === 'production';\n\nasync function mintSession(\n config: AuthConfig,\n subject: string,\n operator: boolean,\n sessionVersion: number,\n): Promise<{ maxAge: number; token: string }> {\n const maxAge = operator ? OPERATOR_SESSION_MAX_AGE_SECONDS : SESSION_MAX_AGE_SECONDS;\n const token = await new SignJWT({ sessionVersion, ...(operator ? { roles: [ARCHIVE_OPERATOR_ROLE] } : {}) })\n .setProtectedHeader({ alg: 'RS256', kid: config.signing.kid, typ: 'JWT' })\n .setIssuedAt()\n .setJti(crypto.randomUUID())\n .setIssuer(config.issuer)\n .setAudience(config.audience)\n .setSubject(subject)\n .setExpirationTime(`${maxAge}s`)\n .sign(config.signing.key);\n return { maxAge, token };\n}\n\n/** Verifies the request's own session cookie against every published key; returns the token and subject. */\nasync function requireSession(\n request: Request,\n config: AuthConfig,\n env: AuthWorkerEnv,\n): Promise<{ subject: string; token: string; sessionVersion: number }> {\n const token = sessionToken(request);\n if (token === null) throw unauthenticated();\n try {\n const { payload } = await jwtVerify(token, config.verifier, {\n algorithms: ['RS256'],\n audience: config.audience,\n clockTolerance: CLOCK_TOLERANCE_SECONDS,\n issuer: config.issuer,\n maxTokenAge: SESSION_MAX_AGE_SECONDS,\n requiredClaims: ['sub', 'iss', 'aud', 'iat', 'exp', 'sessionVersion'],\n });\n const subject = payload.sub ?? '';\n if (subject.length === 0 || subject.length > SUBJECT_MAX || !isWellFormedText(subject)) throw unauthenticated();\n const version = payload.sessionVersion;\n if (typeof version !== 'number' || !Number.isSafeInteger(version) || version < 0) throw unauthenticated();\n if (typeof payload.iat !== 'number' || payload.iat > Date.now() / 1000 + CLOCK_TOLERANCE_SECONDS)\n throw unauthenticated();\n if (\n Array.isArray(payload.roles) &&\n payload.roles.includes(ARCHIVE_OPERATOR_ROLE) &&\n (typeof payload.exp !== 'number' ||\n payload.exp - payload.iat > OPERATOR_SESSION_MAX_AGE_SECONDS ||\n Date.now() / 1000 - payload.iat > OPERATOR_SESSION_MAX_AGE_SECONDS + CLOCK_TOLERANCE_SECONDS)\n )\n throw unauthenticated();\n const account = await readAccount(env.AUTH_DB, subject);\n if (account === null || account.session_version !== version) throw unauthenticated();\n return { sessionVersion: version, subject, token };\n } catch (error) {\n if (error instanceof errors.JOSEError) throw unauthenticated();\n throw error;\n }\n}\n\nasync function sendLink(env: AuthWorkerEnv, config: AuthConfig, to: string, link: string): Promise<void> {\n const name = config.appName;\n const minutes = LINK_TTL_MS / 60_000;\n const notice = `The link works once and expires in ${minutes} minutes. If you did not request it, ignore this email.`;\n await env.AUTH_EMAIL.send({\n from: config.senderAddress,\n html: `<p>Open this link to sign in to ${escapeHtml(name)}:</p><p><a href=\"${escapeHtml(link)}\">${escapeHtml(link)}</a></p><p>${notice}</p>`,\n subject: `Your ${name} sign-in link`,\n text: `Open this link to sign in to ${name}:\\n\\n${link}\\n\\n${notice}`,\n to,\n });\n}\n\nasync function handleSignIn(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n requireMutationGuard(request, config);\n const body = await readJsonBody(request);\n if (typeof body !== 'object' || body === null || Array.isArray(body)) throw badRequest();\n const keys = Object.keys(body);\n const requestedEmail: unknown = Reflect.get(body, 'email');\n if (keys.length !== 1 || keys[0] !== 'email' || typeof requestedEmail !== 'string') {\n throw badRequest();\n }\n const email = requestedEmail.trim().toLowerCase();\n if (!isWellFormedEmail(email)) {\n throw badRequest();\n }\n const nowMs = Date.now();\n const admissionRetry = await spendSignInBudget(env.AUTH_DB, nowMs);\n if (admissionRetry !== null) {\n throw new ApiError(429, 'rate-limited', 'Too many sign-in requests. Try again later.', {\n retryAfterSeconds: admissionRetry,\n });\n }\n const user = await ensureUser(env.AUTH_DB, email);\n const retryAfterSeconds = await spendLinkBudget(env.AUTH_DB, user.id, nowMs);\n if (retryAfterSeconds !== null) {\n throw new ApiError(429, 'rate-limited', 'Too many sign-in emails. Try again later.', { retryAfterSeconds });\n }\n const token = randomToken();\n const tokenHash = await sha256Hex(token);\n await insertToken(env.AUTH_DB, tokenHash, user.id, nowMs);\n try {\n await sendLink(env, config, user.email, `${config.publicOrigin}/api/v1/auth/verify?token=${token}`);\n } catch (error) {\n // An unsent link is useless; the spent allowance is deliberately not refunded.\n await deleteToken(env.AUTH_DB, tokenHash).catch(() => undefined);\n throw new AuthOperationError(503, 'email', { cause: error });\n }\n return json(202, { ok: true }, noStore);\n}\n\nasync function handleVerify(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n if (request.method === 'GET') {\n const token = queryParams(new URL(request.url), ['token']).get('token') ?? '';\n if (!LINK_TOKEN.test(token)) throw badRequest('The link is not valid.');\n return confirmationPage({ ...config, nonce: randomToken(), token });\n }\n requireMutationGuard(request, config, true);\n queryParams(new URL(request.url), []);\n const body = await readJsonBody(request);\n if (typeof body !== 'object' || body === null || Array.isArray(body) || Object.keys(body).length !== 1) {\n throw badRequest('The link is not valid.');\n }\n const token: unknown = Reflect.get(body, 'token');\n if (typeof token !== 'string') throw badRequest('The link is not valid.');\n if (!LINK_TOKEN.test(token)) throw badRequest('The link is not valid.');\n const user = await consumeToken(env.AUTH_DB, await sha256Hex(token), Date.now());\n if (user === null) throw new ApiError(410, 'gone', 'That link has expired or was already used.');\n const session = await mintSession(config, user.id, config.operatorEmails.has(user.email), user.sessionVersion);\n return new Response(null, {\n headers: {\n ...noStore,\n 'set-cookie': sessionCookieValue(session.token, { maxAgeSeconds: session.maxAge, secure: secure(config) }),\n },\n status: 204,\n });\n}\n\n/** The issuer's half of a complete export: the account record only, never tokens or budget state. */\nasync function handleExport(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n const { subject } = await requireSession(request, config, env);\n const row = await readAccount(env.AUTH_DB, subject);\n if (row === null) throw unauthenticated();\n return json(\n 200,\n { createdAt: row.created_at, email: row.email, id: row.id, lastSignInAt: row.last_sign_in_at },\n noStore,\n );\n}\n\n/** Re-serializes a failed catalog response into the public envelope; anything unrecognizable is 503. */\nasync function archiveFailure(response: Response): Promise<Response> {\n const text = await response.text().catch(() => '');\n let parsed: unknown = null;\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = null;\n }\n const envelope = archiveErrorResponseSchema.safeParse(parsed);\n if (!envelope.success || response.status < 400 || response.status > 599) throw unavailable();\n const { code, message, requestId } = envelope.data.error;\n const retryAfter = response.headers.get('retry-after');\n return json(\n response.status,\n { error: { code, message, requestId } },\n retryAfter !== null && /^\\d{1,6}$/.test(retryAfter) ? { ...noStore, 'retry-after': retryAfter } : noStore,\n );\n}\n\n/**\n * Full erasure: catalog session revocation and profile erasure first (through the AUTH_ARCHIVE service binding,\n * presenting the caller's own session), then the issuer's account row, and only then\n * the cookie is expired. Any failure keeps the browser session so the same request\n * can be retried; both halves are idempotent, so a retry after a partial success\n * re-runs the catalog no-op and finishes the issuer side. The account precondition\n * pins every attempt to the confirmed issuer subject, even without a publisher.\n */\nasync function handleErase(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n const origin = requireMutationGuard(request, config);\n const { subject, token } = await requireSession(request, config, env);\n const account = request.headers.get(ACCOUNT_HEADER);\n if (account === null || !uuidSchema.safeParse(account).success) throw badRequest();\n if (account !== subject) {\n throw new ApiError(409, 'account-changed', 'The signed-in account changed since this request was prepared.');\n }\n const headers = new Headers({\n cookie: `${SESSION_COOKIE_NAME}=${token}`,\n origin,\n [REQUEST_MARKER_HEADER]: '1',\n });\n const publisher = request.headers.get(PUBLISHER_HEADER);\n if (publisher !== null) {\n if (!PUBLISHER_ID.test(publisher)) throw badRequest();\n headers.set(PUBLISHER_HEADER, publisher);\n }\n let archived: Response;\n try {\n archived = await serviceRequest(\n env.AUTH_ARCHIVE,\n new Request(`${config.publicOrigin}/api/v1/me/account`, { headers, method: 'DELETE', redirect: 'manual' }),\n async (response) => {\n if (response.status === 204) {\n await response.body?.cancel();\n return response;\n }\n return archiveFailure(response);\n },\n );\n } catch (error) {\n throw new AuthOperationError(503, 'catalog', { cause: error });\n }\n if (archived.status !== 204) return archived;\n await deleteAccount(env.AUTH_DB, subject);\n return new Response(null, {\n headers: { ...noStore, 'set-cookie': expiredSessionCookie(secure(config)) },\n status: 204,\n });\n}\n\nasync function handleSignOutAll(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n requireMutationGuard(request, config);\n const { subject, sessionVersion } = await requireSession(request, config, env);\n const account = request.headers.get(ACCOUNT_HEADER);\n if (account === null || !uuidSchema.safeParse(account).success) throw badRequest();\n if (account !== subject)\n throw new ApiError(409, 'account-changed', 'The signed-in account changed since this request was prepared.');\n if ((await revokeSessions(env.AUTH_DB, subject, sessionVersion)) === null) throw unauthenticated();\n return json(200, { ok: true }, { ...noStore, 'set-cookie': expiredSessionCookie(secure(config)) });\n}\n\nasync function handleHealth(env: AuthWorkerEnv): Promise<Response> {\n let reason: 'configuration-unavailable' | 'database-unavailable' | 'schema-mismatch' | null = null;\n let dbSchemaVersion: number | null = null;\n try {\n await authConfig(env);\n } catch {\n reason = 'configuration-unavailable';\n }\n if (reason === null) {\n try {\n dbSchemaVersion = await authReadiness(env.AUTH_DB);\n if (dbSchemaVersion !== AUTH_SCHEMA_VERSION) reason = 'schema-mismatch';\n } catch {\n reason = 'database-unavailable';\n }\n }\n return json(\n reason === null ? 200 : 503,\n archiveAuthHealthSchema.parse({\n dbSchemaVersion,\n reason,\n schemaVersion: AUTH_SCHEMA_VERSION,\n status: reason === null ? 'ok' : 'degraded',\n }),\n noStore,\n );\n}\nasync function route(request: Request, env: AuthWorkerEnv): Promise<Response> {\n const method = request.method.toUpperCase();\n const routeId = matchRoute(request);\n if (routeId === undefined) throw new ApiError(404, 'not-found', 'No such route.');\n if (routeId === 'health' && method === 'GET') return handleHealth(env);\n const config = await authConfig(env);\n const cors = corsHeaders(request, config);\n if (method === 'OPTIONS') return preflight(request, config, ROUTE_METHODS[routeId]);\n if (!ROUTE_METHODS[routeId].includes(method)) {\n throw new ApiError(405, 'method-not-allowed', 'That method is not allowed here.');\n }\n let response: Response;\n switch (routeId) {\n case 'health':\n response = await handleHealth(env);\n break;\n case 'session':\n await requireSession(request, config, env);\n response = json(200, { ok: true }, noStore);\n break;\n case 'signOutAll':\n response = await handleSignOutAll(request, env, config);\n break;\n case 'jwks':\n response = json(200, { keys: config.jwks }, noStore);\n break;\n case 'signIn':\n response = await handleSignIn(request, env, config);\n break;\n case 'verify':\n response = await handleVerify(request, env, config);\n break;\n case 'account':\n response = method === 'GET' ? await handleExport(request, env, config) : await handleErase(request, env, config);\n break;\n }\n return withCors(response, cors);\n}\n\nfunction withCors(response: Response, cors: Record<string, string>): Response {\n if (Object.keys(cors).length === 0) return response;\n const next = new Response(response.body, response);\n for (const [key, value] of Object.entries(cors)) next.headers.set(key, value);\n return next;\n}\n\nconst diagnostic = (error: unknown): AuthDiagnostic =>\n error instanceof AuthOperationError ? error.authDiagnostic : { kind: 'internal' };\n\n/**\n * Builds the reference identity issuer. Route behavior is fixed; every deployment\n * difference is validated configuration — the app name, trusted origins, audience and\n * issuer claims, signing keys, operator roster, the catalog service binding, and\n * where links and redirects land.\n */\nexport function createAuthWorker<E extends AuthWorkerEnv>(): AuthWorkerHandler<E> {\n const tappers = new Set<(event: AuthEvent) => void>();\n const emit = (event: AuthEvent): void => {\n for (const handler of tappers) {\n try {\n handler(event);\n } catch {\n // Observers never change request or scheduled outcomes.\n }\n }\n };\n return {\n async fetch(request: Request, env: E): Promise<Response> {\n const requestId = crypto.randomUUID();\n const started = Date.now();\n let response: Response;\n let failure: unknown;\n try {\n response = await route(request, env);\n } catch (error) {\n failure = error;\n response = toErrorResponse(error, requestId);\n // Failures stay readable by allowed origins; config faults have no trusted origin set.\n const config = await authConfig(env).catch(() => null);\n if (config !== null) {\n if (\n matchRoute(request) === 'verify' &&\n request.method === 'GET' &&\n error instanceof ApiError &&\n error.status === 400\n )\n response = confirmationPage({ ...config, nonce: randomToken(), token: null });\n response = withCors(response, corsHeaders(request, config));\n }\n }\n if (matchRoute(request) === 'verify') {\n response.headers.set('cache-control', 'no-store');\n response.headers.set('referrer-policy', 'no-referrer');\n response.headers.set('x-content-type-options', 'nosniff');\n if (!response.headers.has('content-security-policy')) {\n response.headers.set(\n 'content-security-policy',\n \"default-src 'none'; base-uri 'none'; frame-ancestors 'none'\",\n );\n }\n }\n if (tappers.size > 0) {\n const routeId = matchRoute(request);\n const code =\n response.status < 400\n ? 'ok'\n : routeId === 'health'\n ? 'unavailable'\n : failure instanceof ApiError\n ? failure.code\n : 'internal';\n // Account erasure may forward a catalog error response rather than throw.\n const forwardedCode =\n failure === undefined && response.status >= 400\n ? archiveErrorResponseSchema.safeParse(\n await response\n .clone()\n .json()\n .catch(() => null),\n )\n : null;\n const health =\n routeId === 'health' && response.status === 503\n ? archiveAuthHealthSchema.safeParse(\n await response\n .clone()\n .json()\n .catch(() => null),\n )\n : null;\n emit({\n code: forwardedCode?.success ? forwardedCode.data.error.code : code,\n ms: Math.max(0, Date.now() - started),\n requestId,\n route: routeId === undefined ? 'unmatched' : ROUTE_LABELS[routeId],\n status: response.status,\n type: 'request',\n ...(response.status >= 500\n ? {\n diagnostic: health?.success\n ? {\n kind:\n health.data.reason === 'configuration-unavailable'\n ? 'configuration'\n : health.data.reason === 'schema-mismatch'\n ? 'schema'\n : 'database',\n }\n : failure === undefined\n ? { kind: 'catalog' as const }\n : diagnostic(failure),\n }\n : {}),\n });\n }\n return response;\n },\n\n async scheduled(_controller: unknown, env: E): Promise<void> {\n try {\n await authConfig(env);\n const removed = await sweep(env.AUTH_DB, Date.now());\n if (tappers.size > 0) emit({ removed, status: 'ok', type: 'scheduled' });\n } catch (error) {\n if (tappers.size > 0) emit({ diagnostic: diagnostic(error), status: 'failed', type: 'scheduled' });\n throw error;\n }\n },\n tap(handler, options) {\n if (options?.signal?.aborted) return () => undefined;\n const observer = (event: AuthEvent): void => handler(event);\n const unsubscribe = (): void => {\n tappers.delete(observer);\n options?.signal?.removeEventListener('abort', unsubscribe);\n };\n tappers.add(observer);\n options?.signal?.addEventListener('abort', unsubscribe, { once: true });\n return unsubscribe;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"worker.cjs","names":["AuthOperationError","forbidden","ACCOUNT_HEADER","PUBLISHER_HEADER","REQUEST_MARKER_HEADER","OPERATOR_SESSION_MAX_AGE_SECONDS","SESSION_MAX_AGE_SECONDS","SignJWT","ARCHIVE_OPERATOR_ROLE","sessionToken","unauthenticated","jwtVerify","isWellFormedText","readAccount","errors","LINK_TTL_MS","escapeHtml","readJsonBody","badRequest","isWellFormedEmail","spendSignInBudget","ApiError","ensureUser","spendLinkBudget","sha256Hex","insertToken","deleteToken","json","noStore","queryParams","confirmationPage","consumeToken","sessionCookieValue","archiveErrorResponseSchema","uuidSchema","SESSION_COOKIE_NAME","serviceRequest","deleteAccount","expiredSessionCookie","revokeSessions","authConfig","authReadiness","archiveAuthHealthSchema","toErrorResponse","sweep","require__crypto","require_contract","require__service","require_http","require_errors","require_store","require__confirmation","require_config","jose","ROUTES","ROUTE_METHODS","ROUTE_LABELS","id","request","TOKEN_BYTES","LINK_TOKEN","PUBLISHER_ID","SUBJECT_MAX","CLOCK_TOLERANCE_SECONDS","origin","config","allowPublicOrigin","cors","methods","maxAge","operator","subject","token","payload","version","account","env","error","name","notice","link","body","keys","requestedEmail","email","nowMs","admissionRetry","user","retryAfterSeconds","tokenHash","session","row","text","response","parsed","envelope","retryAfter","headers","publisher","archived","sessionVersion","reason","dbSchemaVersion","method","routeId","next","key","value","tappers","handler","event","requestId","started","failure","code","forwardedCode","health","removed","options"],"sources":["../../src/auth/worker.ts"],"sourcesContent":["import { errors, jwtVerify, SignJWT } from 'jose';\nimport { sha256Hex } from '../_crypto';\nimport {\n ACCOUNT_HEADER,\n ARCHIVE_OPERATOR_ROLE,\n type ArchiveErrorCode,\n AUTH_SCHEMA_VERSION,\n archiveAuthHealthSchema,\n archiveErrorResponseSchema,\n isWellFormedText,\n OPERATOR_SESSION_MAX_AGE_SECONDS,\n PUBLISHER_HEADER,\n REQUEST_MARKER_HEADER,\n SESSION_COOKIE_NAME,\n SESSION_MAX_AGE_SECONDS,\n sessionCookieValue,\n uuidSchema,\n} from '../contract';\nimport { serviceRequest } from '../worker/_service';\nimport {\n ApiError,\n badRequest,\n expiredSessionCookie,\n forbidden,\n json,\n noStore,\n queryParams,\n readJsonBody,\n sessionToken,\n toErrorResponse,\n unauthenticated,\n} from '../worker/http';\nimport type { ArchiveExecutionContext } from '../worker/worker';\nimport { confirmationPage, escapeHtml } from './_confirmation';\nimport { type AuthConfig, type AuthWorkerEnv, authConfig, isWellFormedEmail } from './config';\nimport { type AuthDiagnostic, AuthOperationError } from './errors';\nimport {\n authReadiness,\n consumeToken,\n deleteAccount,\n deleteToken,\n ensureUser,\n insertToken,\n LINK_TTL_MS,\n readAccount,\n revokeSessions,\n spendLinkBudget,\n spendSignInBudget,\n sweep,\n} from './store';\n\n/**\n * The reference identity issuer: passwordless email accounts. A POST with an email\n * address ensures the account row and sends a single-use magic link; consuming it\n * signs the browser in by setting the `archive_session` cookie a catalog Worker\n * verifies — an RS256 JWT minted by the active private key, verifiable through\n * `/jwks.json` alongside any configured retired public keys. `OPERATOR_EMAILS` is\n * the one operator-membership truth; operator sessions expire after one hour,\n * bounding authority from previously minted role claims. The account routes export the issuer's\n * own record and erase it together with the catalog profile.\n */\nexport interface AuthWorkerHandler<E extends AuthWorkerEnv = AuthWorkerEnv> {\n fetch(request: Request, env: E, ctx?: ArchiveExecutionContext): Promise<Response>;\n /** The cron sweep: expired links, closed send windows and never-verified accounts. */\n scheduled(controller: unknown, env: E, ctx?: ArchiveExecutionContext): Promise<void>;\n tap(handler: (event: AuthEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;\n}\n\nexport type AuthEvent =\n | {\n readonly type: 'request';\n readonly requestId: string;\n readonly route:\n | '/auth/account'\n | '/auth/jwks.json'\n | '/auth/sign-in'\n | '/auth/verify'\n | '/auth/session'\n | '/auth/sign-out-all'\n | '/auth/health'\n | 'unmatched';\n readonly status: number;\n readonly code: ArchiveErrorCode | 'ok';\n readonly ms: number;\n readonly diagnostic?: AuthDiagnostic;\n }\n | { readonly type: 'scheduled'; readonly status: 'ok'; readonly removed: number }\n | { readonly type: 'scheduled'; readonly status: 'failed'; readonly diagnostic: AuthDiagnostic };\n\nconst ROUTES = {\n account: /^\\/api\\/v1\\/auth\\/account$/,\n health: /^\\/api\\/v1\\/auth\\/health$/,\n jwks: /^\\/api\\/v1\\/auth\\/jwks\\.json$/,\n session: /^\\/api\\/v1\\/auth\\/session$/,\n signIn: /^\\/api\\/v1\\/auth\\/sign-in$/,\n signOutAll: /^\\/api\\/v1\\/auth\\/sign-out-all$/,\n verify: /^\\/api\\/v1\\/auth\\/verify$/,\n} as const;\n\nconst ROUTE_METHODS: Record<keyof typeof ROUTES, readonly string[]> = {\n account: ['GET', 'DELETE'],\n health: ['GET'],\n jwks: ['GET'],\n session: ['GET'],\n signIn: ['POST'],\n signOutAll: ['POST'],\n verify: ['GET', 'POST'],\n};\n\nconst ROUTE_LABELS = {\n account: '/auth/account',\n health: '/auth/health',\n jwks: '/auth/jwks.json',\n session: '/auth/session',\n signIn: '/auth/sign-in',\n signOutAll: '/auth/sign-out-all',\n verify: '/auth/verify',\n} as const;\n\nconst matchRoute = (request: Request): keyof typeof ROUTES | undefined =>\n (Object.keys(ROUTES) as (keyof typeof ROUTES)[]).find((id) => ROUTES[id].test(new URL(request.url).pathname));\n\nconst TOKEN_BYTES = 32;\n// Base64url of TOKEN_BYTES random bytes, unpadded.\nconst LINK_TOKEN = /^[A-Za-z0-9_-]{43}$/;\nconst PUBLISHER_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;\nconst SUBJECT_MAX = 256;\nconst CLOCK_TOLERANCE_SECONDS = 5;\n\nconst randomToken = (): string =>\n btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(TOKEN_BYTES))))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replace(/=+$/, '');\n\nconst unavailable = (): ApiError => new AuthOperationError(503, 'catalog');\n\n/** Exact allowed Origin plus the request marker, like the catalog's mutation guard. */\nfunction requireMutationGuard(request: Request, config: AuthConfig, allowPublicOrigin = false): string {\n const origin = request.headers.get('origin');\n if (origin === null || (!config.allowedOrigins.has(origin) && !(allowPublicOrigin && origin === config.publicOrigin)))\n throw forbidden();\n if (request.headers.get(REQUEST_MARKER_HEADER) !== '1') throw forbidden();\n return origin;\n}\n\nfunction corsHeaders(request: Request, config: AuthConfig): Record<string, string> {\n const origin = request.headers.get('origin');\n if (\n origin === null ||\n (!config.allowedOrigins.has(origin) && !(matchRoute(request) === 'verify' && origin === config.publicOrigin))\n )\n return {};\n return { 'access-control-allow-credentials': 'true', 'access-control-allow-origin': origin, vary: 'Origin' };\n}\n\nfunction preflight(request: Request, config: AuthConfig, methods: readonly string[]): Response {\n const cors = corsHeaders(request, config);\n if (Object.keys(cors).length === 0) throw forbidden();\n return new Response(null, {\n headers: {\n ...cors,\n 'access-control-allow-headers': `Content-Type, ${ACCOUNT_HEADER}, ${PUBLISHER_HEADER}, ${REQUEST_MARKER_HEADER}`,\n 'access-control-allow-methods': [...methods, 'OPTIONS'].join(', '),\n 'access-control-max-age': '600',\n },\n status: 204,\n });\n}\n\nconst secure = (config: AuthConfig): boolean => config.appEnv === 'production';\n\nasync function mintSession(\n config: AuthConfig,\n subject: string,\n operator: boolean,\n sessionVersion: number,\n): Promise<{ maxAge: number; token: string }> {\n const maxAge = operator ? OPERATOR_SESSION_MAX_AGE_SECONDS : SESSION_MAX_AGE_SECONDS;\n const token = await new SignJWT({ sessionVersion, ...(operator ? { roles: [ARCHIVE_OPERATOR_ROLE] } : {}) })\n .setProtectedHeader({ alg: 'RS256', kid: config.signing.kid, typ: 'JWT' })\n .setIssuedAt()\n .setJti(crypto.randomUUID())\n .setIssuer(config.issuer)\n .setAudience(config.audience)\n .setSubject(subject)\n .setExpirationTime(`${maxAge}s`)\n .sign(config.signing.key);\n return { maxAge, token };\n}\n\n/** Verifies the request's own session cookie against every published key; returns the token and subject. */\nasync function requireSession(\n request: Request,\n config: AuthConfig,\n env: AuthWorkerEnv,\n): Promise<{ subject: string; token: string; sessionVersion: number }> {\n const token = sessionToken(request);\n if (token === null) throw unauthenticated();\n try {\n const { payload } = await jwtVerify(token, config.verifier, {\n algorithms: ['RS256'],\n audience: config.audience,\n clockTolerance: CLOCK_TOLERANCE_SECONDS,\n issuer: config.issuer,\n maxTokenAge: SESSION_MAX_AGE_SECONDS,\n requiredClaims: ['sub', 'iss', 'aud', 'iat', 'exp', 'sessionVersion'],\n });\n const subject = payload.sub ?? '';\n if (subject.length === 0 || subject.length > SUBJECT_MAX || !isWellFormedText(subject)) throw unauthenticated();\n const version = payload.sessionVersion;\n if (typeof version !== 'number' || !Number.isSafeInteger(version) || version < 0) throw unauthenticated();\n if (typeof payload.iat !== 'number' || payload.iat > Date.now() / 1000 + CLOCK_TOLERANCE_SECONDS)\n throw unauthenticated();\n if (\n Array.isArray(payload.roles) &&\n payload.roles.includes(ARCHIVE_OPERATOR_ROLE) &&\n (typeof payload.exp !== 'number' ||\n payload.exp - payload.iat > OPERATOR_SESSION_MAX_AGE_SECONDS ||\n Date.now() / 1000 - payload.iat > OPERATOR_SESSION_MAX_AGE_SECONDS + CLOCK_TOLERANCE_SECONDS)\n )\n throw unauthenticated();\n const account = await readAccount(env.AUTH_DB, subject);\n if (account === null || account.session_version !== version) throw unauthenticated();\n return { sessionVersion: version, subject, token };\n } catch (error) {\n if (error instanceof errors.JOSEError) throw unauthenticated();\n throw error;\n }\n}\n\nasync function sendLink(env: AuthWorkerEnv, config: AuthConfig, to: string, link: string): Promise<void> {\n const name = config.appName;\n const minutes = LINK_TTL_MS / 60_000;\n const notice = `The link works once and expires in ${minutes} minutes. If you did not request it, ignore this email.`;\n await env.AUTH_EMAIL.send({\n from: config.senderAddress,\n html: `<p>Open this link to sign in to ${escapeHtml(name)}:</p><p><a href=\"${escapeHtml(link)}\">${escapeHtml(link)}</a></p><p>${notice}</p>`,\n subject: `Your ${name} sign-in link`,\n text: `Open this link to sign in to ${name}:\\n\\n${link}\\n\\n${notice}`,\n to,\n });\n}\n\nasync function handleSignIn(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n requireMutationGuard(request, config);\n const body = await readJsonBody(request);\n if (typeof body !== 'object' || body === null || Array.isArray(body)) throw badRequest();\n const keys = Object.keys(body);\n const requestedEmail: unknown = Reflect.get(body, 'email');\n if (keys.length !== 1 || keys[0] !== 'email' || typeof requestedEmail !== 'string') {\n throw badRequest();\n }\n const email = requestedEmail.trim().toLowerCase();\n if (!isWellFormedEmail(email)) {\n throw badRequest();\n }\n const nowMs = Date.now();\n const admissionRetry = await spendSignInBudget(env.AUTH_DB, nowMs);\n if (admissionRetry !== null) {\n throw new ApiError(429, 'rate-limited', 'Too many sign-in requests. Try again later.', {\n retryAfterSeconds: admissionRetry,\n });\n }\n const user = await ensureUser(env.AUTH_DB, email);\n const retryAfterSeconds = await spendLinkBudget(env.AUTH_DB, user.id, nowMs);\n if (retryAfterSeconds !== null) {\n throw new ApiError(429, 'rate-limited', 'Too many sign-in emails. Try again later.', { retryAfterSeconds });\n }\n const token = randomToken();\n const tokenHash = await sha256Hex(token);\n await insertToken(env.AUTH_DB, tokenHash, user.id, nowMs);\n try {\n await sendLink(env, config, user.email, `${config.publicOrigin}/api/v1/auth/verify?token=${token}`);\n } catch (error) {\n // An unsent link is useless; the spent allowance is deliberately not refunded.\n await deleteToken(env.AUTH_DB, tokenHash).catch(() => undefined);\n throw new AuthOperationError(503, 'email', { cause: error });\n }\n return json(202, { ok: true }, noStore);\n}\n\nasync function handleVerify(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n if (request.method === 'GET') {\n const token = queryParams(new URL(request.url), ['token']).get('token') ?? '';\n if (!LINK_TOKEN.test(token)) throw badRequest('The link is not valid.');\n return confirmationPage({ ...config, nonce: randomToken(), token });\n }\n requireMutationGuard(request, config, true);\n queryParams(new URL(request.url), []);\n const body = await readJsonBody(request);\n if (typeof body !== 'object' || body === null || Array.isArray(body) || Object.keys(body).length !== 1) {\n throw badRequest('The link is not valid.');\n }\n const token: unknown = Reflect.get(body, 'token');\n if (typeof token !== 'string') throw badRequest('The link is not valid.');\n if (!LINK_TOKEN.test(token)) throw badRequest('The link is not valid.');\n const user = await consumeToken(env.AUTH_DB, await sha256Hex(token), Date.now());\n if (user === null) throw new ApiError(410, 'gone', 'That link has expired or was already used.');\n const session = await mintSession(config, user.id, config.operatorEmails.has(user.email), user.sessionVersion);\n return new Response(null, {\n headers: {\n ...noStore,\n 'set-cookie': sessionCookieValue(session.token, { maxAgeSeconds: session.maxAge, secure: secure(config) }),\n },\n status: 204,\n });\n}\n\n/** The issuer's half of a complete export: the account record only, never tokens or budget state. */\nasync function handleExport(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n const { subject } = await requireSession(request, config, env);\n const row = await readAccount(env.AUTH_DB, subject);\n if (row === null) throw unauthenticated();\n return json(\n 200,\n { createdAt: row.created_at, email: row.email, id: row.id, lastSignInAt: row.last_sign_in_at },\n noStore,\n );\n}\n\n/** Re-serializes a failed catalog response into the public envelope; anything unrecognizable is 503. */\nasync function archiveFailure(response: Response): Promise<Response> {\n const text = await response.text().catch(() => '');\n let parsed: unknown = null;\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = null;\n }\n const envelope = archiveErrorResponseSchema.safeParse(parsed);\n if (!envelope.success || response.status < 400 || response.status > 599) throw unavailable();\n const { code, message, requestId } = envelope.data.error;\n const retryAfter = response.headers.get('retry-after');\n return json(\n response.status,\n { error: { code, message, requestId } },\n retryAfter !== null && /^\\d{1,6}$/.test(retryAfter) ? { ...noStore, 'retry-after': retryAfter } : noStore,\n );\n}\n\n/**\n * Full erasure: catalog session revocation and profile erasure first (through the AUTH_ARCHIVE service binding,\n * presenting the caller's own session), then the issuer's account row, and only then\n * the cookie is expired. Any failure keeps the browser session so the same request\n * can be retried; both halves are idempotent, so a retry after a partial success\n * re-runs the catalog no-op and finishes the issuer side. The account precondition\n * pins every attempt to the confirmed issuer subject, even without a publisher.\n */\nasync function handleErase(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n const origin = requireMutationGuard(request, config);\n const { subject, token } = await requireSession(request, config, env);\n const account = request.headers.get(ACCOUNT_HEADER);\n if (account === null || !uuidSchema.safeParse(account).success) throw badRequest();\n if (account !== subject) {\n throw new ApiError(409, 'account-changed', 'The signed-in account changed since this request was prepared.');\n }\n const headers = new Headers({\n cookie: `${SESSION_COOKIE_NAME}=${token}`,\n origin,\n [REQUEST_MARKER_HEADER]: '1',\n });\n const publisher = request.headers.get(PUBLISHER_HEADER);\n if (publisher !== null) {\n if (!PUBLISHER_ID.test(publisher)) throw badRequest();\n headers.set(PUBLISHER_HEADER, publisher);\n }\n let archived: Response;\n try {\n archived = await serviceRequest(\n env.AUTH_ARCHIVE,\n new Request(`${config.publicOrigin}/api/v1/me/account`, { headers, method: 'DELETE', redirect: 'manual' }),\n async (response) => {\n if (response.status === 204) {\n await response.body?.cancel();\n return response;\n }\n return archiveFailure(response);\n },\n );\n } catch (error) {\n throw new AuthOperationError(503, 'catalog', { cause: error });\n }\n if (archived.status !== 204) return archived;\n await deleteAccount(env.AUTH_DB, subject);\n return new Response(null, {\n headers: { ...noStore, 'set-cookie': expiredSessionCookie(secure(config)) },\n status: 204,\n });\n}\n\nasync function handleSignOutAll(request: Request, env: AuthWorkerEnv, config: AuthConfig): Promise<Response> {\n requireMutationGuard(request, config);\n const { subject, sessionVersion } = await requireSession(request, config, env);\n const account = request.headers.get(ACCOUNT_HEADER);\n if (account === null || !uuidSchema.safeParse(account).success) throw badRequest();\n if (account !== subject)\n throw new ApiError(409, 'account-changed', 'The signed-in account changed since this request was prepared.');\n if ((await revokeSessions(env.AUTH_DB, subject, sessionVersion)) === null) throw unauthenticated();\n return json(200, { ok: true }, { ...noStore, 'set-cookie': expiredSessionCookie(secure(config)) });\n}\n\nasync function handleHealth(env: AuthWorkerEnv): Promise<Response> {\n let reason: 'configuration-unavailable' | 'database-unavailable' | 'schema-mismatch' | null = null;\n let dbSchemaVersion: number | null = null;\n try {\n await authConfig(env);\n } catch {\n reason = 'configuration-unavailable';\n }\n if (reason === null) {\n try {\n dbSchemaVersion = await authReadiness(env.AUTH_DB);\n if (dbSchemaVersion !== AUTH_SCHEMA_VERSION) reason = 'schema-mismatch';\n } catch {\n reason = 'database-unavailable';\n }\n }\n return json(\n reason === null ? 200 : 503,\n archiveAuthHealthSchema.parse({\n dbSchemaVersion,\n reason,\n schemaVersion: AUTH_SCHEMA_VERSION,\n status: reason === null ? 'ok' : 'degraded',\n }),\n noStore,\n );\n}\nasync function route(request: Request, env: AuthWorkerEnv): Promise<Response> {\n const method = request.method.toUpperCase();\n const routeId = matchRoute(request);\n if (routeId === undefined) throw new ApiError(404, 'not-found', 'No such route.');\n if (routeId === 'health' && method === 'GET') return handleHealth(env);\n const config = await authConfig(env);\n const cors = corsHeaders(request, config);\n if (method === 'OPTIONS') return preflight(request, config, ROUTE_METHODS[routeId]);\n if (!ROUTE_METHODS[routeId].includes(method)) {\n throw new ApiError(405, 'method-not-allowed', 'That method is not allowed here.');\n }\n let response: Response;\n switch (routeId) {\n case 'health':\n response = await handleHealth(env);\n break;\n case 'session':\n await requireSession(request, config, env);\n response = json(200, { ok: true }, noStore);\n break;\n case 'signOutAll':\n response = await handleSignOutAll(request, env, config);\n break;\n case 'jwks':\n response = json(200, { keys: config.jwks }, noStore);\n break;\n case 'signIn':\n response = await handleSignIn(request, env, config);\n break;\n case 'verify':\n response = await handleVerify(request, env, config);\n break;\n case 'account':\n response = method === 'GET' ? await handleExport(request, env, config) : await handleErase(request, env, config);\n break;\n }\n return withCors(response, cors);\n}\n\nfunction withCors(response: Response, cors: Record<string, string>): Response {\n if (Object.keys(cors).length === 0) return response;\n const next = new Response(response.body, response);\n for (const [key, value] of Object.entries(cors)) next.headers.set(key, value);\n return next;\n}\n\nconst diagnostic = (error: unknown): AuthDiagnostic =>\n error instanceof AuthOperationError ? error.authDiagnostic : { kind: 'internal' };\n\n/**\n * Builds the reference identity issuer. Route behavior is fixed; every deployment\n * difference is validated configuration — the app name, trusted origins, audience and\n * issuer claims, signing keys, operator roster, the catalog service binding, and\n * where links and redirects land.\n */\nexport function createAuthWorker<E extends AuthWorkerEnv>(): AuthWorkerHandler<E> {\n const tappers = new Set<(event: AuthEvent) => void>();\n const emit = (event: AuthEvent): void => {\n for (const handler of tappers) {\n try {\n handler(event);\n } catch {\n // Observers never change request or scheduled outcomes.\n }\n }\n };\n return {\n async fetch(request: Request, env: E): Promise<Response> {\n const requestId = crypto.randomUUID();\n const started = Date.now();\n let response: Response;\n let failure: unknown;\n try {\n response = await route(request, env);\n } catch (error) {\n failure = error;\n response = toErrorResponse(error, requestId);\n // Failures stay readable by allowed origins; config faults have no trusted origin set.\n const config = await authConfig(env).catch(() => null);\n if (config !== null) {\n if (\n matchRoute(request) === 'verify' &&\n request.method === 'GET' &&\n error instanceof ApiError &&\n error.status === 400\n )\n response = confirmationPage({ ...config, nonce: randomToken(), token: null });\n response = withCors(response, corsHeaders(request, config));\n }\n }\n if (matchRoute(request) === 'verify') {\n response.headers.set('cache-control', 'no-store');\n response.headers.set('referrer-policy', 'no-referrer');\n response.headers.set('x-content-type-options', 'nosniff');\n if (!response.headers.has('content-security-policy')) {\n response.headers.set(\n 'content-security-policy',\n \"default-src 'none'; base-uri 'none'; frame-ancestors 'none'\",\n );\n }\n }\n if (tappers.size > 0) {\n const routeId = matchRoute(request);\n const code =\n response.status < 400\n ? 'ok'\n : routeId === 'health'\n ? 'unavailable'\n : failure instanceof ApiError\n ? failure.code\n : 'internal';\n // Account erasure may forward a catalog error response rather than throw.\n const forwardedCode =\n failure === undefined && response.status >= 400\n ? archiveErrorResponseSchema.safeParse(\n await response\n .clone()\n .json()\n .catch(() => null),\n )\n : null;\n const health =\n routeId === 'health' && response.status === 503\n ? archiveAuthHealthSchema.safeParse(\n await response\n .clone()\n .json()\n .catch(() => null),\n )\n : null;\n emit({\n code: forwardedCode?.success ? forwardedCode.data.error.code : code,\n ms: Math.max(0, Date.now() - started),\n requestId,\n route: routeId === undefined ? 'unmatched' : ROUTE_LABELS[routeId],\n status: response.status,\n type: 'request',\n ...(response.status >= 500\n ? {\n diagnostic: health?.success\n ? {\n kind:\n health.data.reason === 'configuration-unavailable'\n ? 'configuration'\n : health.data.reason === 'schema-mismatch'\n ? 'schema'\n : 'database',\n }\n : failure === undefined\n ? { kind: 'catalog' as const }\n : diagnostic(failure),\n }\n : {}),\n });\n }\n return response;\n },\n\n async scheduled(_controller: unknown, env: E): Promise<void> {\n try {\n await authConfig(env);\n const removed = await sweep(env.AUTH_DB, Date.now());\n if (tappers.size > 0) emit({ removed, status: 'ok', type: 'scheduled' });\n } catch (error) {\n if (tappers.size > 0) emit({ diagnostic: diagnostic(error), status: 'failed', type: 'scheduled' });\n throw error;\n }\n },\n tap(handler, options) {\n if (options?.signal?.aborted) return () => undefined;\n const observer = (event: AuthEvent): void => handler(event);\n const unsubscribe = (): void => {\n tappers.delete(observer);\n options?.signal?.removeEventListener('abort', unsubscribe);\n };\n tappers.add(observer);\n options?.signal?.addEventListener('abort', unsubscribe, { once: true });\n return unsubscribe;\n },\n };\n}\n"],"mappings":"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"}