@rebasepro/server 0.25.1 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{GCSStorageController-CRd9bFrc.js → GCSStorageController-40FyFeSx.js} +6 -2
- package/dist/GCSStorageController-40FyFeSx.js.map +1 -0
- package/dist/{S3StorageController-D00EFa5P.js → S3StorageController-Bl5shp94.js} +6 -2
- package/dist/S3StorageController-Bl5shp94.js.map +1 -0
- package/dist/access-BFAUvsSs.js +1897 -0
- package/dist/access-BFAUvsSs.js.map +1 -0
- package/dist/api/branch-routes.d.ts +25 -0
- package/dist/api/live-schema-routes.d.ts +10 -0
- package/dist/api/rest/api-generator.d.ts +9 -1
- package/dist/api/rest/idempotency.d.ts +15 -1
- package/dist/api/root-error-handler.d.ts +6 -5
- package/dist/api/types.d.ts +12 -0
- package/dist/{ast-schema-editor-mCeSC03x.js → ast-schema-editor-madTa9Hh.js} +2 -2
- package/dist/{ast-schema-editor-mCeSC03x.js.map → ast-schema-editor-madTa9Hh.js.map} +1 -1
- package/dist/auth/access.d.ts +10 -0
- package/dist/auth/adapter-middleware.d.ts +2 -1
- package/dist/auth/adapter-service-key.d.ts +24 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +21 -1
- package/dist/auth/api-keys/api-key-store.d.ts +8 -1
- package/dist/auth/api-keys/key-grant.d.ts +18 -1
- package/dist/auth/expired-token-sweep.d.ts +5 -0
- package/dist/auth/impersonation.d.ts +3 -1
- package/dist/auth/jwt.d.ts +8 -2
- package/dist/auth/oauth-signin-policy.d.ts +21 -35
- package/dist/auth/rls-scope.d.ts +17 -7
- package/dist/auth/rls-simulator-routes.d.ts +51 -0
- package/dist/auth/simulation-read-only.d.ts +28 -0
- package/dist/auth/simulation.d.ts +141 -0
- package/dist/auth/sql-rate-limit-store.d.ts +15 -1
- package/dist/{auth-Dkvo2tTJ.js → auth-izI7AEzC.js} +370 -1880
- package/dist/auth-izI7AEzC.js.map +1 -0
- package/dist/auth_adapter-B7RUMk6e.js +89 -0
- package/dist/auth_adapter-B7RUMk6e.js.map +1 -0
- package/dist/backend/create-backend.d.ts +6 -0
- package/dist/backend/options.d.ts +9 -4
- package/dist/{backend-DTAOsLQc.js → backend-CaG_Jyso.js} +9 -2
- package/dist/backend-CaG_Jyso.js.map +1 -0
- package/dist/{backup-B9m3cb3X.js → backup-BVbHoKo8.js} +6 -6
- package/dist/{backup-B9m3cb3X.js.map → backup-BVbHoKo8.js.map} +1 -1
- package/dist/boot/boot.d.ts +2 -15
- package/dist/boot/ddl-bootstrap.d.ts +34 -0
- package/dist/boot/env.d.ts +6 -0
- package/dist/boot/fetch-bundle.d.ts +5 -1
- package/dist/boot/provision-only.d.ts +23 -0
- package/dist/boot/provision.d.ts +53 -2
- package/dist/boot/role.d.ts +2 -0
- package/dist/boot/schema-change-recorder.d.ts +45 -0
- package/dist/boot/security-headers.d.ts +22 -0
- package/dist/branch-routes-B2vvEx53.js +84 -0
- package/dist/branch-routes-B2vvEx53.js.map +1 -0
- package/dist/{contract-routes-CsFXW7j4.js → contract-routes-CU2A3JnF.js} +2 -2
- package/dist/{contract-routes-CsFXW7j4.js.map → contract-routes-CU2A3JnF.js.map} +1 -1
- package/dist/cron/cron-store.d.ts +8 -1
- package/dist/{cron-loader-Dj6lA4HQ.js → cron-loader-mMhqbVsl.js} +2 -2
- package/dist/{cron-loader-Dj6lA4HQ.js.map → cron-loader-mMhqbVsl.js.map} +1 -1
- package/dist/{cron-routes-CwbLZguO.js → cron-routes-Cs33j_Kr.js} +3 -3
- package/dist/{cron-routes-CwbLZguO.js.map → cron-routes-Cs33j_Kr.js.map} +1 -1
- package/dist/{cron-scheduler-B04ACoUE.js → cron-scheduler-Be7CNghL.js} +2 -2
- package/dist/{cron-scheduler-B04ACoUE.js.map → cron-scheduler-Be7CNghL.js.map} +1 -1
- package/dist/{cron-store-DkyfuLOH.js → cron-store-DLn11M8H.js} +10 -9
- package/dist/cron-store-DLn11M8H.js.map +1 -0
- package/dist/{strings.es-BeZWAodn.js → data_driver-D0mg1Y_x.js} +2 -90
- package/dist/data_driver-D0mg1Y_x.js.map +1 -0
- package/dist/{ddl-bootstrap-Ck20GSIX.js → ddl-bootstrap-CjRrn12l.js} +91 -2
- package/dist/ddl-bootstrap-CjRrn12l.js.map +1 -0
- package/dist/deploy/pod-contract.d.ts +1 -1
- package/dist/email/dev-sink.d.ts +6 -0
- package/dist/email/index.d.ts +1 -1
- package/dist/{soft-delete-params-BmlVwecC.js → field-access-DYsvjI_P.js} +27 -57
- package/dist/field-access-DYsvjI_P.js.map +1 -0
- package/dist/{filter-dialect-O1HK6Nvr.js → filter-dialect-TrKFWLqT.js} +3 -3
- package/dist/{filter-dialect-O1HK6Nvr.js.map → filter-dialect-TrKFWLqT.js.map} +1 -1
- package/dist/{function-loader-KKDqTcIr.js → function-loader-B8YzXEsQ.js} +21 -14
- package/dist/function-loader-B8YzXEsQ.js.map +1 -0
- package/dist/functions/hono-like.d.ts +12 -0
- package/dist/functions/index.js +19 -1
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/request-timeout.d.ts +3 -3
- package/dist/history/history-routes.d.ts +6 -0
- package/dist/history/retention-sweep.d.ts +5 -0
- package/dist/{history-recorder-B2rkuhuy.js → history-recorder-Bsau7h4X.js} +6 -6
- package/dist/history-recorder-Bsau7h4X.js.map +1 -0
- package/dist/{history-store-DPofGVDp.js → history-store-BQz5YCkW.js} +26 -2
- package/dist/history-store-BQz5YCkW.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.es.js +2449 -543
- package/dist/index.es.js.map +1 -1
- package/dist/init/process-safety.d.ts +4 -3
- package/dist/init/shutdown.d.ts +8 -0
- package/dist/init.d.ts +6 -1
- package/dist/jobs/job-store.d.ts +8 -1
- package/dist/{jobs-h1WRq3cp.js → jobs-Co6Ni5HP.js} +6 -6
- package/dist/jobs-Co6Ni5HP.js.map +1 -0
- package/dist/{jwt-D3oFauZI.js → jwt-rDS0884_.js} +4 -3
- package/dist/{jwt-D3oFauZI.js.map → jwt-rDS0884_.js.map} +1 -1
- package/dist/{keys-COzQ2aE8.js → keys-BXcdRNhJ.js} +2 -2
- package/dist/{keys-COzQ2aE8.js.map → keys-BXcdRNhJ.js.map} +1 -1
- package/dist/mcp/consent-page.d.ts +38 -1
- package/dist/mcp/mcp-routes.d.ts +9 -0
- package/dist/mcp/mcp-tools.d.ts +7 -1
- package/dist/mcp/oauth-store.d.ts +12 -5
- package/dist/metrics/history-recorder.d.ts +4 -1
- package/dist/metrics/history-store.d.ts +8 -0
- package/dist/{offline-BrqCRgih-BtDB1ecy.js → offline-AvuEcg7u-DFHL98OT.js} +127 -40
- package/dist/offline-AvuEcg7u-DFHL98OT.js.map +1 -0
- package/dist/{offline-store-BhYXViha-BW5j1We_.js → offline-store-BhYXViha-YTmjBpo2.js} +5 -5
- package/dist/{offline-store-BhYXViha-BW5j1We_.js.map → offline-store-BhYXViha-YTmjBpo2.js.map} +1 -1
- package/dist/{openapi-generator-BjHr6upl.js → openapi-generator-CyxEtYEX.js} +61 -22
- package/dist/openapi-generator-CyxEtYEX.js.map +1 -0
- package/dist/{proxy-CH8Jsovg.js → proxy-BYir8tWK.js} +4 -6
- package/dist/proxy-BYir8tWK.js.map +1 -0
- package/dist/{query-parser-otFZqJsC.js → query-parser-CDXWf2hH.js} +6 -5
- package/dist/{query-parser-otFZqJsC.js.map → query-parser-CDXWf2hH.js.map} +1 -1
- package/dist/{request-timeout-59crS-9u.js → request-timeout-DVjo-c_8.js} +4 -4
- package/dist/request-timeout-DVjo-c_8.js.map +1 -0
- package/dist/rls-simulator-routes-bEY7-5-u.js +190 -0
- package/dist/rls-simulator-routes-bEY7-5-u.js.map +1 -0
- package/dist/schema-edit/deployed-schema-guard.d.ts +47 -0
- package/dist/{schema-editor-routes-Bsut0bCC.js → schema-editor-routes-Dc3DU_jm.js} +3 -3
- package/dist/{schema-editor-routes-Bsut0bCC.js.map → schema-editor-routes-Dc3DU_jm.js.map} +1 -1
- package/dist/soft-delete-params-BRONCOA2.js +59 -0
- package/dist/soft-delete-params-BRONCOA2.js.map +1 -0
- package/dist/{sql-export-routes-DmB_uZ4l.js → sql-export-routes-QycX8W8S.js} +38 -4
- package/dist/sql-export-routes-QycX8W8S.js.map +1 -0
- package/dist/{src-D5Y3N-S4.js → src-Bh9LRPb9.js} +21 -2
- package/dist/{src-D5Y3N-S4.js.map → src-Bh9LRPb9.js.map} +1 -1
- package/dist/storage/GCSStorageController.d.ts +2 -0
- package/dist/storage/LocalStorageController.d.ts +8 -3
- package/dist/storage/S3StorageController.d.ts +2 -0
- package/dist/storage/maintenance-keys.d.ts +17 -0
- package/dist/storage/maintenance-routes.d.ts +28 -5
- package/dist/storage/maintenance.d.ts +136 -4
- package/dist/storage/orphan-scans.d.ts +120 -0
- package/dist/storage/types.d.ts +11 -0
- package/dist/strings.es-B7QLnHh2.js +96 -0
- package/dist/strings.es-B7QLnHh2.js.map +1 -0
- package/dist/upload-validation-Bgo626uY.js.map +1 -1
- package/package.json +5 -5
- package/dist/GCSStorageController-CRd9bFrc.js.map +0 -1
- package/dist/S3StorageController-D00EFa5P.js.map +0 -1
- package/dist/auth-Dkvo2tTJ.js.map +0 -1
- package/dist/auth_adapter-CIItCTc8.js +0 -21
- package/dist/auth_adapter-CIItCTc8.js.map +0 -1
- package/dist/backend-DTAOsLQc.js.map +0 -1
- package/dist/cron-store-DkyfuLOH.js.map +0 -1
- package/dist/ddl-bootstrap-Ck20GSIX.js.map +0 -1
- package/dist/function-loader-KKDqTcIr.js.map +0 -1
- package/dist/history-recorder-B2rkuhuy.js.map +0 -1
- package/dist/history-store-DPofGVDp.js.map +0 -1
- package/dist/jobs-h1WRq3cp.js.map +0 -1
- package/dist/offline-BrqCRgih-BtDB1ecy.js.map +0 -1
- package/dist/openapi-generator-BjHr6upl.js.map +0 -1
- package/dist/proxy-CH8Jsovg.js.map +0 -1
- package/dist/request-timeout-59crS-9u.js.map +0 -1
- package/dist/soft-delete-params-BmlVwecC.js.map +0 -1
- package/dist/sql-export-routes-DmB_uZ4l.js.map +0 -1
- package/dist/strings.es-BeZWAodn.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-izI7AEzC.js","names":[],"sources":["../../common/src/util/email.ts","../../common/src/util/policy/sqlToPolicy.ts","../../common/src/util/policy/securityRuleToConditions.ts","../../common/src/util/policy/evaluatePolicy.ts","../../common/src/util/permissions.ts","../src/auth/resolve-rate-limit-store.ts","../src/auth/sql-rate-limit-store.ts","../src/email/html.ts","../src/email/templates.ts","../src/email/link-base.ts","../src/auth/admin-user-ops.ts","../src/auth/adapter-middleware.ts","../src/auth/password.ts","../src/auth/auth-hooks.ts","../src/email/smtp-email-service.ts","../src/email/dev-sink.ts","../src/auth/rate-limit-store.ts","../src/auth/rate-limiter.ts","../src/auth/captcha.ts","../src/auth/mfa.ts","../src/auth/mfa-crypto.ts","../src/auth/cookie-utils.ts","../src/auth/mfa-routes.ts","../src/auth/mfa-gate.ts","../src/auth/registration-policy.ts","../src/auth/address-ownership.ts","../src/auth/deliverable-address.ts","../src/auth/email-change-routes.ts","../src/auth/session-routes.ts","../src/auth/passwordless-signup.ts","../src/auth/magic-link-routes.ts","../src/auth/otp-routes.ts","../src/auth/oauth-signin-policy.ts","../src/auth/routes.ts","../src/auth/capabilities.ts","../src/auth/reset-password-admin.ts","../src/auth/admin-roles-route.ts","../src/auth/user-picking.ts","../src/auth/admin-users-route.ts","../src/auth/builtin-auth-adapter.ts","../src/auth/jwks-routes.ts","../src/auth/api-keys/legacy-permissions.ts","../src/auth/api-keys/api-key-store.ts","../src/auth/api-keys/key-grant.ts","../src/auth/api-keys/api-key-routes.ts","../src/auth/oauth-code-flow.ts","../src/auth/google-oauth.ts","../src/auth/linkedin-oauth.ts","../src/auth/github-oauth.ts","../src/auth/oidc-id-token.ts","../src/auth/microsoft-oauth.ts","../src/auth/apple-oauth.ts","../src/auth/facebook-oauth.ts","../src/auth/twitter-oauth.ts","../src/auth/discord-oauth.ts","../src/auth/gitlab-oauth.ts","../src/auth/bitbucket-oauth.ts","../src/auth/slack-oauth.ts","../src/auth/spotify-oauth.ts","../src/auth/custom-auth-adapter.ts","../src/auth/index.ts"],"sourcesContent":["/**\n * Email normalization — one implementation, because the database enforces it.\n *\n * `ensureAuthTablesExist` puts a `UNIQUE INDEX ON users (lower(email))` on the\n * auth table. That index decides what \"the same address\" means, and it does not\n * trim: to Postgres, `' foo@bar.com'` and `'foo@bar.com'` are two addresses and\n * both may exist. So every write that reaches the column has to agree with\n * every read, exactly, or the two disagree in the one direction that matters —\n * a row that exists and cannot be found.\n *\n * That is not hypothetical. The lookup path trimmed and the admin create paths\n * did not, so a user created through `POST /api/data/users` or\n * `POST /api/auth/admin/users` with a stray space was stored untrimmed,\n * survived the unique index alongside the real address, and was unreachable by\n * login forever after. The HTTP auth routes were unaffected only because Zod's\n * `.email()` happens to reject surrounding whitespace — a guard on a different\n * layer, for a different reason, that the admin paths do not sit behind.\n *\n * It lives in `common` because `server`, `server-postgres` and `server-mongo`\n * all write this column and must agree exactly, and `common` is the only\n * package all three already depend on.\n */\n\n/**\n * Canonical form of an email address: trimmed, lower-cased.\n *\n * Non-strings pass through untouched, so this is safe to apply to a value out\n * of a partial update payload whose type is not known yet.\n */\nexport function normalizeEmail<T>(email: T): T | string {\n return typeof email === \"string\" ? email.trim().toLowerCase() : email;\n}\n","import { ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, LiteralPolicyOperand, PolicyExpression, policy, rewriteLegacyRlsFunctions } from \"@rebasepro/types\";\nimport { toSnakeCase } from \"@rebasepro/utils\";\n\n/**\n * A tiny, regex-based SQL \"parser\" for security rules.\n *\n * This is NOT a full SQL parser. It is designed to handle the subset of SQL\n * commonly used in `USING` and `WITH CHECK` clauses, enough to drive the\n * optimistic client-side UI decision.\n *\n * It handles:\n * - `field = 'literal'`\n * - `field != 'literal'`\n * - `field = current_setting('app.uid')` (or the legacy `app.user_id`)\n * - `A AND B`, `A OR B` — only where the keyword is at the top level\n * - `true`\n * - `IN (...)` (as optimistic true)\n *\n * For anything it doesn't understand, it returns a `raw` expression, which\n * the evaluator treats as \"unknown\" (and usually optimistic true).\n *\n * **This output also round-trips back into DDL** via `policyToPostgres` (the\n * schema/policy generators), so decomposing a clause the parser only partly\n * understands is not a cosmetic mistake — it emits invalid SQL. When in doubt,\n * prefer `raw`: it is reproduced verbatim.\n */\n/** True when `keyword` starts at `i` as a standalone word. */\nfunction isKeywordAt(upper: string, i: number, keyword: string): boolean {\n if (!upper.startsWith(keyword, i)) return false;\n const before = i === 0 ? \" \" : upper[i - 1];\n const after = upper[i + keyword.length] ?? \" \";\n return /[\\s()]/.test(before) && /[\\s()]/.test(after);\n}\n\n/**\n * Split `sql` on a boolean keyword, but only where it sits at paren depth 0 and\n * outside a string literal. Returns null when it never does, so the caller\n * leaves the clause alone.\n *\n * This used to be `sql.split(/ AND /i)`, which tore subqueries in half: the\n * `AND` inside\n * `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id = rebase.uid())`\n * split the expression, and re-emitting the halves produced\n * `(EXISTS (...) AND m.user_id = rebase.uid())`\n * where `m` is no longer in scope — SQL that Postgres rejects outright with\n * \"missing FROM-clause entry for table\". Returning null instead keeps such a\n * clause as a `raw` expression, which round-trips verbatim.\n */\nfunction splitTopLevel(sql: string, keyword: \"AND\" | \"OR\"): string[] | null {\n const upper = sql.toUpperCase();\n const parts: string[] = [];\n let depth = 0;\n let inString = false;\n let start = 0;\n\n for (let i = 0; i < sql.length; i++) {\n const ch = sql[i];\n if (inString) {\n if (ch === \"'\") {\n if (sql[i + 1] === \"'\") i++; // '' escapes a quote inside a literal\n else inString = false;\n }\n continue;\n }\n if (ch === \"'\") { inString = true; continue; }\n if (ch === \"(\") { depth++; continue; }\n if (ch === \")\") { depth--; continue; }\n if (depth === 0 && isKeywordAt(upper, i, keyword)) {\n parts.push(sql.slice(start, i));\n i += keyword.length - 1;\n start = i + 1;\n }\n }\n\n if (parts.length === 0) return null;\n parts.push(sql.slice(start));\n const trimmedParts = parts.map(p => p.trim()).filter(p => p.length > 0);\n return trimmedParts.length > 1 ? trimmedParts : null;\n}\n\n/** `SELECT` as the first word of `sql`. */\nconst STARTS_WITH_SELECT = /^select(?=[\\s(])/i;\n\n/**\n * Drop redundant wrapping parens (`(a AND b)` → `a AND b`), never `(a) AND (b)`\n * — and never the parens of a scalar subquery: `(SELECT x)` without them is not\n * an expression at all, and re-emitted bare it is a syntax error inside\n * `USING (…)`.\n */\nfunction stripOuterParens(sql: string): string {\n let s = sql.trim();\n while (wrapsWhole(s) && !STARTS_WITH_SELECT.test(s.slice(1).trim())) {\n s = s.slice(1, -1).trim();\n }\n return s;\n}\n\n/** Whether `s` is one parenthesised group: `(a AND b)`, not `(a) AND (b)`. */\nfunction wrapsWhole(s: string): boolean {\n if (!s.startsWith(\"(\") || !s.endsWith(\")\")) return false;\n let depth = 0;\n let inString = false;\n for (let i = 0; i < s.length; i++) {\n const ch = s[i];\n if (inString) {\n if (ch === \"'\") {\n if (s[i + 1] === \"'\") i++;\n else inString = false;\n }\n continue;\n }\n if (ch === \"'\") { inString = true; continue; }\n if (ch === \"(\") depth++;\n else if (ch === \")\") {\n depth--;\n if (depth === 0 && i < s.length - 1) return false;\n }\n }\n return true;\n}\n\n/** `x` when `s` is exactly `(SELECT x)`, otherwise null. */\nfunction scalarSubqueryBody(s: string): string | null {\n if (!wrapsWhole(s)) return null;\n const inner = s.slice(1, -1).trim();\n return STARTS_WITH_SELECT.test(inner) ? inner.slice(\"select\".length).trim() : null;\n}\n\n/**\n * The names in a role array's body — `'admin', 'editor'`, each with or without\n * Postgres's `::text` — or `null` when an element is anything else. A quote\n * or a comma inside a name is not split into roles that were never there: the\n * clause stays raw and round-trips verbatim.\n */\nfunction roleArrayElements(list: string): string[] | null {\n const roles: string[] = [];\n for (const element of list.split(\",\")) {\n const match = /^'([^',]*)'(?:::text)?$/.exec(element.trim());\n if (!match) return null;\n roles.push(match[1]);\n }\n return roles;\n}\n\n/** Whether any part of `expr` fell through to `raw`. */\nfunction containsRaw(expr: PolicyExpression): boolean {\n switch (expr.kind) {\n case \"raw\":\n return true;\n case \"and\":\n case \"or\":\n return expr.operands.some(containsRaw);\n case \"not\":\n return containsRaw(expr.operand);\n case \"existsIn\":\n return containsRaw(expr.where);\n default:\n return false;\n }\n}\n\nexport function sqlToPolicy(sql: string): PolicyExpression {\n // Normalised before anything else looks at it, so every pattern below only\n // has to know the current spelling. A database migrated by a pre-1.0 release\n // still holds `auth.uid()` in its policy bodies until the next push or boot\n // recompiles them — and until then the admin UI reads those bodies back\n // through here. Without this they parse as opaque `raw`, and the framework's\n // own policies get badged as hand-written drift.\n //\n // Normalising rather than accepting both spellings throughout is deliberate:\n // it also means a legacy policy that falls through to `raw` is stored in the\n // new spelling, so editing and saving one in the Studio migrates it.\n const trimmed = stripOuterParens(rewriteLegacyRlsFunctions(sql).trim());\n\n // `(SELECT x)` — how the compiler spells a part of a policy that does not\n // read the row, so Postgres evaluates it once per statement. Read back as\n // `x` when `x` is understood completely: without a FROM a scalar subquery\n // is one row holding x's value, so the two mean the same. Anything less —\n // a FROM, a LIMIT, an alias, a clause the parser does not know — stays\n // `raw`, parens included, and is reproduced verbatim.\n const subqueryBody = scalarSubqueryBody(trimmed);\n if (subqueryBody !== null) {\n const inner = sqlToPolicy(subqueryBody);\n return containsRaw(inner) ? policy.raw(trimmed) : inner;\n }\n\n if (trimmed.toLowerCase() === \"true\") return policy.true();\n if (trimmed.toLowerCase() === \"false\") return policy.false();\n\n // Handle roles overlap (&&) and containment (@>), as the compiler writes\n // them and as Postgres stores them:\n // string_to_array(rebase.roles(), ',') && ARRAY['admin', 'editor']\n // string_to_array(rebase.roles(), ','::text) && ARRAY['admin'::text, 'editor'::text]\n // The second is what `pg_policies` holds, so what `schema introspect`\n // writes as a rule's `using`. Read as raw, a junction could not inherit\n // the rule, and its derived edge-write policies were left with none.\n const rolesMatch = trimmed.match(/^string_to_array\\s*\\(\\s*rebase\\.roles\\(\\)\\s*,\\s*','(?:::text)?\\s*\\)\\s*(&&|@>)\\s*ARRAY\\s*\\[(.+)\\]$/i);\n const roles = rolesMatch ? roleArrayElements(rolesMatch[2]) : null;\n if (rolesMatch && roles) {\n return rolesMatch[1] === \"&&\" ? policy.rolesOverlap(roles) : policy.rolesContain(roles);\n }\n\n // OR binds looser than AND, so it splits first.\n const orParts = splitTopLevel(trimmed, \"OR\");\n if (orParts) return policy.or(...orParts.map(sqlToPolicy));\n\n const andParts = splitTopLevel(trimmed, \"AND\");\n if (andParts) return policy.and(...andParts.map(sqlToPolicy));\n\n // Handle = and !=\n const match = trimmed.match(/^(.+?)\\s*(!?=)\\s*(.+)$/);\n if (match) {\n const [, leftStr, op, rightStr] = match;\n const left = parseOperand(leftStr.trim());\n const right = parseOperand(rightStr.trim());\n if (left && right) {\n return policy.compare(left, op === \"=\" ? \"eq\" : \"neq\", right);\n }\n }\n\n // Fallback to raw — the NORMALISED text, not the input. Storing the input\n // verbatim would mean a legacy policy read out of a database, edited in the\n // Studio and saved, writes `auth.uid()` back into the project's config: a\n // call to a function 1.0 no longer creates.\n return policy.raw(trimmed);\n}\n\n/**\n * Literals from other BaaS platforms that people compare `rebase.uid()` against\n * out of habit. Mirrors the driver's `FOREIGN_CONVENTION_ROLES` guard on\n * `pgRoles`, one surface over: the same muscle memory inside a `using:` string\n * is the more dangerous spelling, because it inverts a rule instead of\n * emptying a table.\n */\n/**\n * A `Map`, not an object literal.\n *\n * As `Record<string, string>` this was indexed with a literal taken straight\n * out of a policy, so every key on `Object.prototype` answered: a rule\n * comparing `rebase.uid()` to `\"valueOf\"`, `\"toString\"`, `\"constructor\"` or\n * `\"hasOwnProperty\"` found a truthy \"platform\" and reported an anonymous-grant\n * risk that does not exist — with the matched function interpolated into the\n * explanation as the platform's name. A security warning that fires on\n * innocent input is worse than none: it is what teaches people to skip the\n * warnings that are real.\n *\n * Same shape as the prototype-pollution class swept out of `setIn`, `getIn`,\n * `mergeDeep` and `unflattenObject` — a data-derived key reaching a plain\n * object. Found by a property test, on the input `\"valueOf\"`.\n */\nconst FOREIGN_CONVENTION_UIDS = new Map<string, string>([\n [\"anon\", \"Supabase\"],\n [\"authenticated\", \"Supabase\"],\n [\"service_role\", \"Supabase\"]\n]);\n\n/**\n * The same foreign literals, as a pattern for SQL that could not be parsed\n * back into structure.\n */\nconst FOREIGN_UID_LITERAL_SQL = new RegExp(\n String.raw`rebase\\.uid\\(\\)\\s*=\\s*'(${[...FOREIGN_CONVENTION_UIDS.keys()].join(\"|\")})'`,\n \"i\"\n);\n\n/** A clause that reads as a lockdown but admits anonymous callers. */\nexport interface AnonymousGrantRisk {\n /** Which spelling was found. */\n pattern: \"foreign-uid-literal\" | \"uid-not-null\";\n /** The offending fragment — the literal, or the SQL that is a tautology. */\n detail: string;\n /** Why it admits anonymous callers, and what to write instead. */\n explanation: string;\n}\n\n/**\n * `rebase.uid() IS NOT NULL` in raw SQL, the clause that is always true.\n *\n * Both schema spellings, because this runs over policy bodies read back from a\n * database, and one migrated by a pre-1.0 release still holds `auth.uid()`.\n * A security check that stops recognising a dangerous clause because the\n * framework renamed a function is a check that silently turns off.\n */\nconst UID_NOT_NULL = /\\b(?:rebase|auth)\\.uid\\(\\)\\s+IS\\s+NOT\\s+NULL/i;\n\n/**\n * Find clauses that read as \"signed-in users only\" but admit anonymous callers.\n *\n * Both spellings come from the same place — Supabase, where its own `auth.uid()`\n * really is NULL for an anonymous request. Rebase substitutes\n * {@link ANONYMOUS_USER_ID} instead (a blank id would read back as NULL, which\n * is how the trusted *server* context is recognised), so:\n *\n * - `rebase.uid() IS NOT NULL` is a tautology on the user path, and\n * - `rebase.uid() != 'anon'` excludes one spelling of anonymous and admits the\n * other. This one is not hypothetical and was not only a foreign habit:\n * rebase's own request path reported `'anon'` while everything that compiled\n * or checked a policy used `'anonymous'`, so whichever literal an author\n * picked, half the anonymous callers walked through. See\n * {@link ANONYMOUS_USER_IDS}.\n *\n * Either one turns a lockdown into a full grant, and neither looks wrong. No\n * real user id is ever one of these literals, and a user-context request is\n * never NULL, so a match is always a mistake rather than a deliberate check.\n *\n * Structured expressions are checked too, not just parsed SQL: `policy.compare`\n * can spell the same mistake.\n */\nexport function findAnonymousGrants(expr: PolicyExpression): AnonymousGrantRisk[] {\n const found: AnonymousGrantRisk[] = [];\n\n const visit = (e: PolicyExpression): void => {\n switch (e.kind) {\n case \"and\":\n case \"or\":\n e.operands.forEach(visit);\n return;\n case \"not\":\n visit(e.operand);\n return;\n case \"existsIn\":\n visit(e.where);\n return;\n case \"raw\": {\n if (UID_NOT_NULL.test(e.sql)) {\n found.push({\n pattern: \"uid-not-null\",\n detail: e.sql,\n explanation: \"`rebase.uid() IS NOT NULL` is true for every request that came from a client, \" +\n `including anonymous ones — they carry '${ANONYMOUS_USER_ID}', not NULL. ` +\n \"Use `condition: policy.authenticated()` to mean \\\"signed in\\\".\"\n });\n }\n // The foreign literals have to be looked for here too, not only\n // in `compare`. `sqlToPolicy` falls back to `raw` for anything\n // it cannot structure — an `EXISTS (...)` subquery always does —\n // so a policy read back from the database arrives as one opaque\n // string. Checking only the tautology meant a genuine\n // `rebase.uid() = 'anon'` inside an `existsIn` was structurally\n // undetectable once round-tripped, and the caller read the empty\n // result as \"no risks found\".\n const foreign = FOREIGN_UID_LITERAL_SQL.exec(e.sql);\n if (foreign) {\n const literal = foreign[1];\n found.push({\n pattern: \"foreign-uid-literal\",\n detail: literal,\n explanation: `'${literal}' is a ${FOREIGN_CONVENTION_UIDS.get(literal)} convention. Rebase ` +\n `reports an anonymous request as '${ANONYMOUS_USER_ID}', so comparing against ` +\n `'${literal}' passes for every caller. Use \\`condition: policy.authenticated()\\` to ` +\n \"mean \\\"signed in\\\".\"\n });\n }\n return;\n }\n case \"compare\": {\n const literal = [e.left, e.right].find(o => o.kind === \"literal\") as LiteralPolicyOperand | undefined;\n const comparesUid = e.left.kind === \"authUid\" || e.right.kind === \"authUid\";\n if (!comparesUid || typeof literal?.value !== \"string\") return;\n const platform = FOREIGN_CONVENTION_UIDS.get(literal.value);\n if (!platform) return;\n found.push({\n pattern: \"foreign-uid-literal\",\n detail: literal.value,\n explanation: `'${literal.value}' is a ${platform} convention. Rebase reports an anonymous ` +\n `request as '${ANONYMOUS_USER_ID}', so comparing against '${literal.value}' passes for ` +\n \"every caller. Use `condition: policy.authenticated()` to mean \\\"signed in\\\" — it \" +\n `compiles to NOT IN (${ANONYMOUS_USER_IDS.map(v => `'${v}'`).join(\", \")}), covering ` +\n \"every spelling rebase has reported rather than whichever one you remember.\"\n });\n return;\n }\n default:\n return;\n }\n };\n\n visit(expr);\n return found;\n}\n\nfunction parseOperand(str: string) {\n // current_setting('app.uid') or rebase.uid(). `app.user_id` is the\n // pre-rename spelling and stays parseable: policies are data, so a\n // database provisioned before the rename still holds rules written\n // against it, and round-tripping one must not silently drop the operand.\n //\n // ANCHORED, and that is the whole point. These tests used to be\n // unanchored — `.test(str)` rather than `^…$` — so any operand text that\n // merely *contained* a uid call was replaced wholesale by the call itself.\n // Everything else in the expression was discarded with it, including a\n // leading `NOT (`:\n //\n // NOT (rebase.uid() = rebase.uid()) parsed as rebase.uid() = rebase.uid()\n //\n // A deny became an unconditional grant. The realistic spelling is a\n // hand-written defensive rule with a uid call on both sides —\n // COALESCE(rebase.uid(), '') = COALESCE(owner_id, rebase.uid())\n // — which collapsed to the same tautology. This is not confined to the\n // admin UI: `securityRuleToConditions` feeds a rule's raw `using:` string\n // through here, and the Postgres DDL generators compile the result, so the\n // tautology was written into the database as the policy body.\n //\n // An operand this cannot identify exactly must return null, which drops the\n // whole clause to `raw` and reproduces it verbatim. That is the rule the\n // rest of this file already follows: when in doubt, prefer `raw`.\n if (/^current_setting\\s*\\(\\s*'app\\.(uid|user_id)'\\s*\\)$/i.test(str) || /^rebase\\.uid\\(\\)$/i.test(str)) {\n return policy.authUid();\n }\n // The same call as an initPlan — the compiler's spelling, `(SELECT\n // rebase.uid())`, and Postgres's re-rendering of it, `( SELECT rebase.uid()\n // AS uid)`. Anchored like the line above, for the same reason.\n if (/^\\(\\s*select\\s+rebase\\.uid\\(\\)(?:\\s+as\\s+\"?uid\"?)?\\s*\\)$/i.test(str)) {\n return policy.authUid();\n }\n\n // Literal string: 'value', with `''` decoded back to a single quote.\n //\n // `quoteLiteral` doubles every quote on the way out, and this did not undo\n // it, so a literal containing an apostrophe grew on every trip: O'Brien →\n // O''Brien → O''''Brien, doubling each time a policy was read back and\n // recompiled. Past the first trip the emitted policy compares against a\n // string no row holds.\n const literal = parseSingleQuoted(str);\n if (literal !== null) {\n return policy.literal(literal);\n }\n\n // Unquoted literals, which must be recognised BEFORE the bare-word branch\n // below or they are read as column names.\n //\n // `quoteLiteral` emits booleans, numbers and null unquoted, so `a = false`\n // came back as a comparison against a *field* called `false`, and `a = 42`\n // against a field called `42`. The recompiled SQL is identical either way,\n // which is why this survived a round-trip check on the SQL — but the\n // expression is now wrong, and the expression is what the admin UI\n // evaluates. Against a row with no `a`, Postgres denies (`NULL = false` is\n // not true) while the JS evaluator compared two missing columns, found them\n // equal, and allowed. That is precisely the client/database drift the\n // shared PolicyExpression model exists to make impossible.\n //\n // Unambiguous in both directions: a SQL identifier cannot begin with a\n // digit, and bare `true`/`false`/`null` are always the literals — a column\n // so named would have to be double-quoted to be referenced at all.\n if (/^-?\\d+$/.test(str)) return policy.literal(Number(str));\n if (/^-?\\d*\\.\\d+$/.test(str)) return policy.literal(Number(str));\n if (/^true$/i.test(str)) return policy.literal(true);\n if (/^false$/i.test(str)) return policy.literal(false);\n if (/^null$/i.test(str)) return policy.literal(null);\n\n // Bare field name — but only one that survives the snake-casing the\n // compiler will apply to it. `toSnakeCase(\"_\")` is the empty string, and a\n // field that compiles to an empty column reference emits `= 'x'`, which is\n // a syntax error at CREATE POLICY time. Such a name is left to `raw`, where\n // it round-trips verbatim instead. `toSnakeCase` itself is not touched:\n // column names derived by it are already in shipped databases.\n if (/^\\w+$/.test(str) && toSnakeCase(str) !== \"\") {\n return policy.field(str);\n }\n\n return null;\n}\n\n/**\n * Decode a single-quoted SQL literal, or null when `str` is not exactly one.\n *\n * Rejecting is as important as decoding: `'a' = 'b'` is two literals and an\n * operator, not one literal whose body contains a quote, and a regex anchored\n * on the outer quotes would happily read it as the latter. Every interior quote\n * must therefore be part of a `''` pair.\n */\nfunction parseSingleQuoted(str: string): string | null {\n if (str.length < 2 || !str.startsWith(\"'\") || !str.endsWith(\"'\")) return null;\n const body = str.slice(1, -1);\n let out = \"\";\n for (let i = 0; i < body.length; i++) {\n if (body[i] !== \"'\") {\n out += body[i];\n continue;\n }\n if (body[i + 1] === \"'\") {\n out += \"'\";\n i++;\n continue;\n }\n return null; // a bare quote — `str` is not a single literal\n }\n return out;\n}\n","import { PolicyExpression, SecurityRule, policy } from \"@rebasepro/types\";\nimport { sqlToPolicy } from \"./sqlToPolicy\";\n\n/**\n * The normalized `USING` / `WITH CHECK` conditions for a single security rule,\n * expressed in the engine-agnostic {@link PolicyExpression} model.\n *\n * A `null` clause means \"this rule contributes no condition for that clause\";\n * consumers apply the default (Postgres denies with `false`).\n */\nexport interface RuleConditions {\n usingExpr: PolicyExpression | null;\n withCheckExpr: PolicyExpression | null;\n}\n\n/**\n * Desugars a {@link SecurityRule} — its `access`/`ownerField`/`roles` shortcuts,\n * structured `condition`/`check`, and raw `using`/`withCheck` — into a single\n * normalized {@link PolicyExpression} pair.\n *\n * **This is the linchpin against drift:** both the Postgres DDL generators and\n * the client-side evaluator consume this one function, so there is exactly one\n * definition of what a rule means. In particular, application `roles` are folded\n * into the expression here (AND'd with the base condition, matching how Postgres\n * generates the clause) rather than being handled separately by each consumer.\n */\nexport function securityRuleToConditions(rule: SecurityRule): RuleConditions {\n return {\n usingExpr: withRoles(baseUsing(rule), rule),\n withCheckExpr: withRoles(baseWithCheck(rule), rule)\n };\n}\n\nfunction baseUsing(rule: SecurityRule): PolicyExpression | null {\n if (rule.condition) return rule.condition;\n if (rule.using != null) return sqlToPolicy(rule.using);\n if (rule.access === \"public\") return policy.true();\n if (rule.ownerField) return policy.compare(policy.field(rule.ownerField), \"eq\", policy.authUid());\n return null;\n}\n\nfunction baseWithCheck(rule: SecurityRule): PolicyExpression | null {\n if (rule.check) return rule.check;\n if (rule.withCheck != null) return sqlToPolicy(rule.withCheck);\n // No explicit WITH CHECK → fall back to the USING condition, matching\n // PostgreSQL's own default behavior.\n return baseUsing(rule);\n}\n\n/**\n * AND the base condition with an application-role check, or produce a roles-only\n * condition when there is no base. Mirrors the Postgres generator so that a\n * role-scoped restrictive rule denies exactly the same set of users on both\n * sides.\n */\nfunction withRoles(base: PolicyExpression | null, rule: SecurityRule): PolicyExpression | null {\n if (!rule.roles || rule.roles.length === 0) return base;\n const rolesExpr = policy.rolesOverlap(rule.roles);\n if (rule.mode === \"restrictive\") {\n // Restrictive rule: applies ONLY if user has the roles.\n // If user DOES NOT have the roles, they are NOT restricted (passes).\n // If user HAS the roles, they must pass the base condition.\n // Logical equivalent: NOT(roles) OR base\n return base ? policy.or(policy.not(rolesExpr), base) : policy.not(rolesExpr);\n }\n return base ? policy.and(base, rolesExpr) : rolesExpr;\n}\n","import { ANONYMOUS_USER_ID, isAnonymousUid, Entity, PolicyCompareOperator, PolicyExpression, PolicyOperand } from \"@rebasepro/types\";\n\n/**\n * Result of evaluating a policy client-side. `\"unknown\"` means the expression\n * could not be decided without more information — either a raw-SQL escape-hatch\n * node (which the client deliberately never guesses) or a row-column reference\n * with no entity in hand (e.g. list-level gating). Callers decide how to resolve\n * `\"unknown\"`: fail-closed for an enforcement decision, optimistic for pure\n * visibility gating.\n */\nexport type TriState = boolean | \"unknown\";\n\n/**\n * Context for {@link evaluatePolicy}: the acting user (or none) and the row\n * being evaluated (or none, for collection-level gating).\n */\nexport interface PolicyEvalContext {\n /**\n * The current user's id, or null/undefined when no user is signed in.\n *\n * Null here means *anonymous visitor*, not \"server context\" — a client is\n * never the server context. `authUid` operands therefore resolve to\n * {@link ANONYMOUS_USER_ID} rather than `null`, matching the `rebase.uid()`\n * the database would see for the same request.\n */\n uid?: string | null;\n /** The current user's application roles. */\n roles?: string[];\n /**\n * Whether this session is a GUEST — anonymous sign-in rather than an\n * account. Optional, and absent means \"not a guest\", so a caller that does\n * not know keeps the behaviour it had.\n */\n isAnonymous?: boolean;\n /** The row being evaluated, or null when no specific row is available. */\n entity: Entity | null;\n}\n\n/**\n * Evaluates a {@link PolicyExpression} against a user + row, using three-valued\n * (Kleene) logic so that `\"unknown\"` sub-results propagate soundly.\n *\n * This is the JavaScript twin of {@link policyToPostgres}: both derive from the\n * same expression, so the admin UI matches database enforcement by construction\n * for every non-raw rule.\n */\nexport function evaluatePolicy(expr: PolicyExpression, ctx: PolicyEvalContext): TriState {\n switch (expr.kind) {\n case \"true\":\n return true;\n case \"false\":\n return false;\n case \"and\":\n return kleeneAnd(expr.operands.map(o => evaluatePolicy(o, ctx)));\n case \"or\":\n return kleeneOr(expr.operands.map(o => evaluatePolicy(o, ctx)));\n case \"not\":\n return kleeneNot(evaluatePolicy(expr.operand, ctx));\n case \"compare\":\n return evaluateCompare(expr.op, expr.left, expr.right, ctx);\n case \"rolesOverlap\": {\n const userRoles = ctx.roles ?? [];\n return expr.roles.some(r => r === \"public\" || userRoles.includes(r));\n }\n case \"rolesContain\": {\n const userRoles = ctx.roles ?? [];\n return expr.roles.every(r => r === \"public\" || userRoles.includes(r));\n }\n case \"authenticated\":\n // Every anonymous spelling, matching what this node compiles to in\n // Postgres — the two evaluators disagreeing about who is signed in\n // is the client optimistically rendering a row the database will\n // refuse, or hiding one it would have allowed.\n return ctx.uid != null && !isAnonymousUid(ctx.uid);\n case \"registered\":\n // The same two halves the Postgres compilation has. A client that\n // disagreed with the database here would optimistically render a\n // row the database refuses, or hide one it would have allowed.\n return ctx.uid != null && !isAnonymousUid(ctx.uid) && ctx.isAnonymous !== true;\n case \"serverContext\":\n // A client is never the server context. Postgres decides this by\n // `rebase.uid() IS NULL`, which a client request can never produce:\n // the driver substitutes ANONYMOUS_USER_ID for a missing id.\n return false;\n case \"existsIn\":\n // A membership subquery cannot be run client-side — server-authoritative.\n return \"unknown\";\n case \"raw\":\n // Arbitrary SQL cannot be evaluated client-side — never guess.\n return \"unknown\";\n }\n}\n\n// ── Three-valued logic ───────────────────────────────────────────────\n\nfunction kleeneAnd(values: TriState[]): TriState {\n if (values.some(v => v === false)) return false;\n if (values.some(v => v === \"unknown\")) return \"unknown\";\n return true;\n}\n\nfunction kleeneOr(values: TriState[]): TriState {\n if (values.some(v => v === true)) return true;\n if (values.some(v => v === \"unknown\")) return \"unknown\";\n return false;\n}\n\nfunction kleeneNot(value: TriState): TriState {\n if (value === \"unknown\") return \"unknown\";\n return !value;\n}\n\n// ── Comparison ───────────────────────────────────────────────────────\n\ntype ResolvedOperand = { known: false } | { known: true; value: unknown };\n\nfunction resolveOperand(operand: PolicyOperand, ctx: PolicyEvalContext): ResolvedOperand {\n switch (operand.kind) {\n case \"literal\":\n return { known: true, value: operand.value };\n case \"authUid\":\n // The sentinel, not null: `rebase.uid()` is never NULL for a request\n // that came from a client, so comparing against null here would\n // disagree with the database on exactly the rules that test for it\n // (e.g. `rebase.uid() <> 'anonymous'`).\n return { known: true, value: ctx.uid ?? ANONYMOUS_USER_ID };\n case \"authRoles\":\n return { known: true, value: ctx.roles ?? [] };\n case \"field\":\n // Can't resolve a row column without the row.\n if (!ctx.entity) return { known: false };\n return { known: true, value: ctx.entity.values[operand.name] };\n case \"outerField\":\n // Only meaningful inside an `existsIn` subquery (server-authoritative).\n return { known: false };\n case \"authClaim\":\n // Server-authoritative, like `existsIn` and `raw`, and for a reason\n // worth stating: a claim is TEXT, and Postgres compares it to the\n // column after casting it to the column's type. Reproducing that\n // here means reproducing uuid case-folding, numeric widening and\n // the `NULLIF`, in JavaScript, from a `PolicyEvalContext` that does\n // not know the column's type. A second implementation of a cast\n // that is subtly wrong is worse than no answer: it would render a\n // row the database refuses, or hide one it would have allowed.\n return { known: false };\n }\n}\n\nfunction evaluateCompare(\n op: PolicyCompareOperator,\n left: PolicyOperand,\n right: PolicyOperand,\n ctx: PolicyEvalContext\n): TriState {\n const l = resolveOperand(left, ctx);\n const r = resolveOperand(right, ctx);\n if (!l.known || !r.known) return \"unknown\";\n\n const a = l.value;\n const b = r.value;\n\n if (a === null || b === null) {\n // SQL answers NULL for *every* comparison against NULL, and a policy\n // that answers NULL does not grant the row. So the only question here\n // is which JavaScript answer reproduces that outcome.\n //\n // This used to answer `false` for `eq` and `true` for `neq`, which is\n // JavaScript's two-valued reading of a three-valued question.\n //\n // `neq` was a grant the database does not give:\n // `owner_id != rebase.uid()` on a row whose `owner_id` is NULL read as\n // *permitted* in the admin panel and was refused by Postgres — on every\n // row where the column is null, which for a nullable column is usually\n // most of them.\n //\n // `false` for `eq` looked safe, because false denies and NULL denies.\n // It is not, because it does not survive negation: `not(a = NULL)`\n // became `true` while `NOT NULL` stays NULL, so the same grant reappears\n // one operator up. A local answer that is only right in a positive\n // position is not right — it just moves.\n //\n // \"unknown\" is what SQL actually says, it composes correctly through\n // Kleene negation, and enforcement callers already resolve it\n // fail-closed. Both were found by the exhaustive Postgres differential\n // in `policy-agreement-exhaustive.test.ts`, the second only after the\n // first was fixed.\n return \"unknown\";\n }\n\n if (op === \"eq\") return a === b;\n if (op === \"neq\") return a !== b;\n\n if (typeof a === \"string\" && typeof b === \"string\") {\n if (op === \"lt\") return a < b;\n if (op === \"lte\") return a <= b;\n if (op === \"gt\") return a > b;\n if (op === \"gte\") return a >= b;\n }\n\n if (typeof a === \"number\" && typeof b === \"number\") {\n if (op === \"lt\") return a < b;\n if (op === \"lte\") return a <= b;\n if (op === \"gt\") return a > b;\n if (op === \"gte\") return a >= b;\n }\n\n if (typeof a === \"bigint\" && typeof b === \"bigint\") {\n if (op === \"lt\") return a < b;\n if (op === \"lte\") return a <= b;\n if (op === \"gt\") return a > b;\n if (op === \"gte\") return a >= b;\n }\n\n return \"unknown\";\n}\n","import { AuthState, Entity, CollectionConfig, SecurityOperation, SecurityRule, User } from \"@rebasepro/types\";\nimport { securityRuleToConditions } from \"./policy/securityRuleToConditions\";\nimport { evaluatePolicy, PolicyEvalContext, TriState } from \"./policy/evaluatePolicy\";\n\n/**\n * Minimal auth context for permission checking.\n * Only requires the user object — avoids forcing callers to construct\n * a full AuthController just to check permissions.\n *\n * An alias, not a second definition: {@link AuthState} in `@rebasepro/types` is\n * the same shape and is what `dynamicProps` and the JSON-Logic condition context\n * now take, so declaring it twice would be the `WhereFilterOp` mistake again —\n * two copies that agree only by luck.\n */\nexport type AuthContext<USER extends User = User> = AuthState<USER>;\n\n/**\n * How to resolve a policy result that cannot be decided client-side (a raw-SQL\n * escape-hatch rule, or a row-column reference with no row in hand).\n *\n * - `\"allow\"` (default): optimistic — used for admin-UI gating, where Postgres\n * remains the authoritative gate and hiding a working action is worse than\n * showing one the server may reject.\n * - `\"deny\"`: fail-closed — used by real enforcement callers (e.g. a driver\n * applying policies in-process), so an undecidable rule never silently allows.\n */\nexport type UnknownResolution = \"allow\" | \"deny\";\n\n/**\n * Which half of a rule to evaluate.\n *\n * Postgres evaluates `USING` against the row as it is *now* and `WITH CHECK`\n * against the row as it *will be*, both inside the transaction. A driver\n * enforcing an update in-process has two different rows in hand and therefore\n * needs to ask the two questions separately — asking one question about one row\n * either checks the new values against the old row's ownership or the reverse.\n *\n * - `\"both\"` (default): what a single-row decision means (`USING ∧ WITH CHECK`).\n * - `\"using\"`: the read/target clause only — ask it about the stored row.\n * - `\"withCheck\"`: the write clause only — ask it about the row being written.\n *\n * Rule *selection* is unaffected: the target operation still decides which rules\n * apply, so `\"using\"` on an `update` evaluates the update rules' USING clause,\n * not the delete rules'.\n */\nexport type PolicyClauses = \"both\" | \"using\" | \"withCheck\";\n\nexport interface CheckOperationOptions {\n onUnknown?: UnknownResolution;\n clauses?: PolicyClauses;\n}\n\n/** Combine clause results with AND under three-valued (Kleene) logic. */\nfunction kleeneAnd(values: TriState[]): TriState {\n if (values.some(v => v === false)) return false;\n if (values.some(v => v === \"unknown\")) return \"unknown\";\n return true;\n}\n\n/** The operations a rule covers, mirroring the Postgres generator's resolution. */\nfunction ruleOperations(rule: SecurityRule): readonly SecurityOperation[] {\n return rule.operations && rule.operations.length > 0\n ? rule.operations\n : [rule.operation ?? \"all\"];\n}\n\nfunction ruleApplies(rule: SecurityRule, targetOperation: SecurityOperation): boolean {\n const ops = ruleOperations(rule);\n return ops.includes(targetOperation) || ops.includes(\"all\");\n}\n\n/**\n * Evaluate a single rule for one operation, returning a tri-state.\n *\n * A `null` clause (the rule contributes no condition for a required clause)\n * denies — matching Postgres, which emits `USING (false)` / `WITH CHECK (false)`\n * in that case. USING applies to SELECT/UPDATE/DELETE; WITH CHECK to\n * INSERT/UPDATE; both must pass for UPDATE.\n */\nfunction evaluateRuleForOperation(\n rule: SecurityRule,\n ctx: PolicyEvalContext,\n targetOperation: SecurityOperation,\n clauses: PolicyClauses = \"both\"\n): TriState {\n const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);\n const clause = (expr: typeof usingExpr): TriState => expr === null ? false : evaluatePolicy(expr, ctx);\n\n const needsUsing = targetOperation !== \"insert\" && clauses !== \"withCheck\";\n const needsWithCheck = (targetOperation === \"insert\" || targetOperation === \"update\") && clauses !== \"using\";\n\n const results: TriState[] = [];\n if (needsUsing) results.push(clause(usingExpr));\n if (needsWithCheck) results.push(clause(withCheckExpr));\n return kleeneAnd(results);\n}\n\nfunction resolveTriState(value: TriState, onUnknown: UnknownResolution): boolean {\n if (value === \"unknown\") return onUnknown === \"allow\";\n return value;\n}\n\n/**\n * Decide whether an operation is permitted for a user on a (possibly null) row,\n * by evaluating the collection's security rules with the shared policy model —\n * the same model compiled to Postgres RLS DDL, so the decision matches database\n * enforcement for every non-raw rule.\n *\n * Engine-independent by design. `securityRules` are a declaration about the\n * data, not about Postgres: the engine decides *who* enforces them (Postgres\n * compiles them to RLS DDL, a document driver applies them in-process), never\n * *whether* they hold. Gating this function on the engine's `supportsRLS`\n * capability is what made every `{ onUnknown: \"deny\" }` call site in the Mongo\n * driver return `true` before it evaluated anything — and it did so only for\n * collections that spelled their engine out, so declaring `engine: \"mongodb\"`\n * was what switched authorization off.\n *\n * @param options.onUnknown how to treat rules that cannot be decided\n * client-side (raw SQL, or row predicates with no row). Defaults to `\"allow\"`\n * for optimistic UI gating; enforcement callers should pass `\"deny\"`.\n * @param options.clauses which half of each rule to evaluate. See\n * {@link PolicyClauses}; defaults to `\"both\"`.\n */\nexport function checkOperation<M extends Record<string, unknown>, USER extends User>(\n collection: CollectionConfig<M>,\n authContext: AuthContext<USER>,\n entity: Entity<M> | null,\n targetOperation: SecurityOperation,\n options?: CheckOperationOptions\n): boolean {\n const onUnknown = options?.onUnknown ?? \"allow\";\n const clauses = options?.clauses ?? \"both\";\n const securityRules = collection.securityRules;\n if (!securityRules || securityRules.length === 0) {\n return true;\n }\n\n const applicableRules = securityRules.filter((r: SecurityRule) => ruleApplies(r, targetOperation));\n if (applicableRules.length === 0) return false;\n\n const ctx: PolicyEvalContext = {\n uid: authContext.user?.uid,\n roles: authContext.user?.roles ?? [],\n // What `policy.registered()` reads. Left out, every guest was an\n // account here: a document driver let a guest read, write and delete\n // the rows `registered()` withholds, whatever user it was handed.\n isAnonymous: authContext.user?.isAnonymous === true,\n entity\n };\n\n let grantedByPermissive = false;\n let deniedByRestrictive = false;\n let hasPermissive = false;\n\n for (const rule of applicableRules) {\n const mode = rule.mode || \"permissive\";\n const passed = resolveTriState(evaluateRuleForOperation(rule, ctx, targetOperation, clauses), onUnknown);\n\n if (mode === \"restrictive\") {\n if (!passed) {\n deniedByRestrictive = true;\n break;\n }\n } else {\n hasPermissive = true;\n if (passed) grantedByPermissive = true;\n }\n }\n\n if (deniedByRestrictive) return false;\n return hasPermissive ? grantedByPermissive : false;\n}\n\nexport function canReadCollection<M extends Record<string, unknown>, USER extends User>\n (\n collection: CollectionConfig<M>,\n authContext: AuthContext<USER>\n ): boolean {\n return checkOperation(collection, authContext, null, \"select\");\n}\n\nexport function canEditEntity<M extends Record<string, unknown>, USER extends User>\n (\n collection: CollectionConfig<M>,\n authContext: AuthContext<USER>,\n path: string,\n entity: Entity<M> | null\n ): boolean {\n return checkOperation(collection, authContext, entity, \"update\");\n}\n\nexport function canCreateEntity<M extends Record<string, unknown>, USER extends User>\n (\n collection: CollectionConfig<M>,\n authContext: AuthContext<USER>,\n path: string,\n entity: Entity<M> | null\n ): boolean {\n return checkOperation(collection, authContext, entity, \"insert\");\n}\n\nexport function canDeleteEntity<M extends Record<string, unknown>, USER extends User>\n (\n collection: CollectionConfig<M>,\n authContext: AuthContext<USER>,\n path: string,\n entity: Entity<M> | null\n ): boolean {\n return checkOperation(collection, authContext, entity, \"delete\");\n}\n","/**\n * Choosing where rate-limit counts live, from configuration and environment.\n *\n * Pure, and separate from both stores, because the decision is the risk. Getting\n * it wrong in either direction is silent: a deployment that wanted shared counts\n * and got per-process ones enforces N times its limit, and a single-container\n * deployment that got shared ones pays a database round trip on every request\n * for a count nobody is sharing with.\n *\n * ## Why memory is still the default\n *\n * The same reason `role.ts` gives for having no channel-bus refusal: **a process\n * cannot read its own replica count.** A single container scaled to three needs\n * a shared store exactly as much as a split deployment does, and neither is\n * visible from inside one process. So this does not guess. It stays on the\n * default that costs nothing, and the deployment that knows it has peers says\n * so — with one environment variable, which is the same sentence whether it is\n * written in a compose file, a Helm value, or a platform-pinned pod spec.\n */\n\n/** Where a deployment keeps its rate-limit counts. */\nexport type RateLimitStoreKind = \"memory\" | \"sql\";\n\n/** The environment this module reads. Narrowed so it can be called with a literal. */\nexport interface RateLimitStoreEnv {\n /** `memory` (default) or `sql`. */\n REBASE_RATE_LIMIT_STORE?: string;\n}\n\n/**\n * A refusal to boot, phrased as the variable to change.\n *\n * Its own class so `bootFromBundle` can present it as a configuration problem\n * rather than a crash, matching `RoleConfigurationError`.\n */\nexport class RateLimitStoreConfigurationError extends Error {\n constructor(message: string, readonly hint?: string) {\n super(message);\n this.name = \"RateLimitStoreConfigurationError\";\n }\n}\n\n/**\n * Which store this process should build.\n *\n * A misspelled value **refuses to boot** rather than falling back to memory.\n * Every other outcome here is silent — the whole failure mode of this setting is\n * that nothing looks wrong — so the one moment it can be caught is the moment it\n * is read. `REBASE_RATE_LIMIT_STORE=postgres` is the obvious thing to type and\n * would otherwise be a per-replica limit wearing the shape of a shared one.\n *\n * A blank or whitespace-only value is *unset*, not invalid:\n * `REBASE_RATE_LIMIT_STORE=${SOMETHING}` with `SOMETHING` undefined is the\n * ordinary way to write a compose file, and it is how the platform neutralises a\n * tenant's own variable.\n */\nexport function resolveRateLimitStoreKind(env: RateLimitStoreEnv): RateLimitStoreKind {\n const raw = (env.REBASE_RATE_LIMIT_STORE ?? \"\").trim().toLowerCase();\n if (raw === \"\") return \"memory\";\n if (raw === \"memory\" || raw === \"sql\") return raw;\n\n throw new RateLimitStoreConfigurationError(\n `REBASE_RATE_LIMIT_STORE=\"${raw}\" is not a rate-limit store.`,\n 'Use \"sql\" to share counts across processes, or \"memory\" (the default) to keep them ' +\n \"in this process. A deployment running more than one replica or more than one role \" +\n 'wants \"sql\" — otherwise each process enforces the whole limit by itself.'\n );\n}\n","/**\n * A rate-limit store every process in a deployment shares.\n *\n * {@link MemoryRateLimitStore} counts in one process's memory, so N replicas\n * enforce N times the limit between them. That is fine for the single container\n * almost every deployment runs, and wrong the moment a deployment splits — an\n * `api` pod and a `functions` pod each grant a caller the full allowance, and\n * nothing anywhere reports it. A limit that silently lets through 3× the traffic\n * is worse than no limit, because it is believed.\n *\n * So this keeps the counts in Postgres, on the same seam the cron store already\n * uses: `DataDriver.admin.executeSql`. No new dependency, no new service, and it\n * degrades on a driver with no SQL (Mongo) rather than failing to boot.\n *\n * ## Why two buckets rather than one row per hit\n *\n * The memory store keeps a timestamp per hit, which is exact and costs a row per\n * request here. This keeps **one row per key per window** and approximates the\n * same sliding behaviour by weighting the previous window by how much of it is\n * still in view:\n *\n * estimate = current + previous × (1 − elapsed ÷ window)\n *\n * At the moment a window rolls over the previous bucket counts fully and decays\n * to nothing by the end, so the boundary burst a fixed window allows — spend the\n * whole allowance in the last second, spend it again in the first second of the\n * next — is bounded rather than free. This is the standard approximation and its\n * error is at most one window's distribution skew, which for a floor against\n * runaway clients is not a number worth a row per request.\n *\n * ## Why a denied hit is given back\n *\n * `INSERT … ON CONFLICT DO UPDATE … RETURNING` is atomic per row, which is what\n * the interface demands: two concurrent hits cannot both read `limit - 1` and\n * both be allowed. But it means the count is incremented *before* the decision,\n * so a request that turns out to be over the limit has already been charged for.\n *\n * The memory store does not charge denied hits, and a store whose semantics\n * differ from the default is a store nobody can safely switch to — the limit\n * would tighten for reasons an operator would attribute to their traffic. So a\n * denial issues a compensating decrement. That is a second statement, on the\n * path that is by definition the rare one; the allowed path stays at one.\n */\nimport type { DataDriver } from \"@rebasepro/types\";\nimport { isSQLAdmin } from \"@rebasepro/types\";\nimport { revokeInternalTableSql, firstSqlRow } from \"@rebasepro/common\";\nimport { logger } from \"../utils/logger.js\";\nimport { createDdlBootstrapper, provisioningExec } from \"../boot/ddl-bootstrap.js\";\nimport type { RateLimitDecision, RateLimitStore } from \"./rate-limit-store.js\";\n\nconst SCHEMA = \"rebase\";\nconst TABLE_NAME = \"rate_limit_hits\";\nconst TABLE = `${SCHEMA}.${TABLE_NAME}`;\n\n/** How often expired buckets are deleted. */\nconst DEFAULT_SWEEP_MS = 5 * 60 * 1000;\n\nexport interface SqlRateLimitStoreOptions {\n /** How often to delete expired buckets. Defaults to 5 minutes. */\n sweepMs?: number;\n /**\n * Default `true`. `false` is a backend that changes nothing in its\n * database: the table is used if it is there, never created, and counts\n * stay per-process without it.\n */\n provision?: boolean;\n /**\n * Clock, for tests. Driving the window directly beats sleeping through half\n * of one — a real sleep overruns on a loaded machine and takes the hits it\n * expected to still be in the window with it.\n */\n now?: () => number;\n}\n\n/**\n * Build the store, or `undefined` if this driver cannot back it.\n *\n * Returning `undefined` rather than throwing matches `createCronStore`: a\n * deployment on a driver with no SQL admin should degrade to per-process counts\n * with a warning, not refuse to serve.\n */\n/** The shared store, plus the one step a provisioning run takes ahead of time. */\nexport interface SqlRateLimitStore extends RateLimitStore {\n /**\n * Create the table now rather than on the first counted request — what a\n * provisioning run calls. Resolves to whether it is usable.\n */\n ensureTable(): Promise<boolean>;\n}\n\nexport function createSqlRateLimitStore(\n driver: DataDriver,\n options: SqlRateLimitStoreOptions = {}\n): SqlRateLimitStore | undefined {\n const admin = driver.admin;\n if (!isSQLAdmin(admin)) {\n logger.warn(\n \"⚠️ [rate-limit] The configured driver has no SQL admin, so rate-limit counts \" +\n \"cannot be shared between processes. Falling back to this process's memory — \" +\n \"each replica will enforce the limit independently.\"\n );\n return undefined;\n }\n\n const exec = provisioningExec(\n (sqlText, execOptions) =>\n admin.executeSql(sqlText, execOptions?.params ? { params: execOptions.params } : undefined),\n options.provision\n );\n\n const ddl = createDdlBootstrapper(exec, \"rate-limit-store\");\n const now = options.now ?? Date.now;\n const sweepMs = options.sweepMs ?? DEFAULT_SWEEP_MS;\n\n let sweepTimer: ReturnType<typeof setInterval> | undefined;\n let ready: Promise<boolean> | undefined;\n\n /**\n * Create the table once, and remember whether it worked.\n *\n * Lazily rather than at boot because the store is constructed while routes\n * are being mounted, and an `await` there would put a database round trip on\n * the boot path of every deployment — including the ones that never take a\n * request. The first request pays for it.\n *\n * The result is cached even on failure: a store that cannot create its table\n * must not retry the DDL on every request, which would turn one broken\n * permission into a per-request query storm.\n *\n * Readiness is decided by reading the table, never by the DDL not throwing —\n * `ensureObject` routes through `step`, which logs a failure and returns\n * normally. Keying on the absence of an exception would report a table that\n * was never created as ready, and every later request would fall into the\n * per-request error path instead of the one-time fallback.\n */\n async function ensureReady(): Promise<boolean> {\n if (!ready) {\n ready = (async () => {\n try {\n await ddl.ensureObject(\"Creating schema rebase\", \"CREATE SCHEMA IF NOT EXISTS rebase\");\n await ddl.ensureObject(`Creating ${TABLE}`, `\n CREATE TABLE IF NOT EXISTS ${TABLE} (\n key TEXT NOT NULL,\n bucket BIGINT NOT NULL,\n hits INTEGER NOT NULL DEFAULT 0,\n expires_at TIMESTAMPTZ NOT NULL,\n PRIMARY KEY (key, bucket)\n )\n `);\n // The sweep's only access path. Without it a deployment with\n // many distinct keys sweeps by sequential scan, which is the\n // kind of cost that shows up as unexplained load long after\n // anyone connects it to a rate limiter.\n await ddl.ensureObject(\"Creating idx_rate_limit_hits_expiry\", `\n CREATE INDEX IF NOT EXISTS idx_rate_limit_hits_expiry\n ON ${TABLE}(expires_at)\n `);\n\n // Keyed on what exists, not on whether this instance created\n // it — the loser of a boot race must still revoke, or an\n // end-user role keeps write access to the table that decides\n // its own limits.\n const readable = await ddl.isReadable(TABLE);\n if (readable) {\n await ddl.step(\"Revoking end-user access to rate_limit_hits\", () =>\n exec(revokeInternalTableSql(SCHEMA, TABLE_NAME)));\n } else if (options.provision === false) {\n logger.error(\n `[rate-limit] ${TABLE} does not exist, and this backend changes nothing in its ` +\n \"database, so counts stay per-process. Every replica will enforce the limit \" +\n \"independently until a provisioning run creates the table.\"\n );\n } else {\n logger.error(\n \"[rate-limit] The shared rate-limit table could not be created, so counts \" +\n \"stay per-process. Every replica will enforce the limit independently.\"\n );\n }\n return readable;\n } catch (err) {\n logger.error(\n \"[rate-limit] Could not prepare the shared rate-limit table. Falling back \" +\n \"to per-process counts for the life of this process.\",\n { error: err instanceof Error ? err.message : String(err) }\n );\n return false;\n }\n })();\n }\n return ready;\n }\n\n function startSweeping(): void {\n if (sweepTimer) return;\n sweepTimer = setInterval(() => {\n void exec(`DELETE FROM ${TABLE} WHERE expires_at < now()`).catch((err: unknown) => {\n // A failed sweep is not a failed request. Log and let the next\n // one try: the rows are harmless until they are many.\n logger.debug(\"[rate-limit] Bucket sweep failed\", {\n error: err instanceof Error ? err.message : String(err)\n });\n });\n }, sweepMs);\n // Never hold a process open for a garbage collection.\n sweepTimer.unref?.();\n }\n\n return {\n ensureTable: ensureReady,\n\n async hit(key: string, windowMs: number, limit: number): Promise<RateLimitDecision> {\n if (!(await ensureReady())) {\n // No table, so no shared count. Allowing is the right failure\n // direction for a *floor*: refusing every request because the\n // limiter's bookkeeping is broken turns a missing safety net\n // into an outage.\n return { allowed: true, remaining: limit, retryAfterMs: 0 };\n }\n startSweeping();\n\n const at = now();\n const bucket = Math.floor(at / windowMs);\n const elapsed = at - bucket * windowMs;\n // How much of the previous window is still in view. 1 at the instant\n // of rollover, 0 by the end of this one.\n const carry = 1 - elapsed / windowMs;\n const expiresAt = new Date((bucket + 2) * windowMs).toISOString();\n\n let current: number;\n let previous: number;\n try {\n const rows = await exec(\n `WITH bumped AS (\n INSERT INTO ${TABLE} AS r (key, bucket, hits, expires_at)\n VALUES ($1, $2, 1, $3)\n ON CONFLICT (key, bucket)\n DO UPDATE SET hits = r.hits + 1, expires_at = EXCLUDED.expires_at\n RETURNING hits\n )\n SELECT\n (SELECT hits FROM bumped) AS current,\n COALESCE((SELECT hits FROM ${TABLE} WHERE key = $1 AND bucket = $4), 0) AS previous`,\n { params: [key, bucket, expiresAt, bucket - 1] }\n );\n const row = firstRow(rows);\n current = Number(row?.current ?? 1);\n previous = Number(row?.previous ?? 0);\n } catch (err) {\n logger.debug(\"[rate-limit] Shared count unavailable for this request\", {\n error: err instanceof Error ? err.message : String(err)\n });\n return { allowed: true, remaining: limit, retryAfterMs: 0 };\n }\n\n const estimate = current + previous * carry;\n\n if (estimate > limit) {\n // Give back the hit this request was charged, so the store agrees\n // with MemoryRateLimitStore about what a denial costs.\n void exec(\n `UPDATE ${TABLE} SET hits = GREATEST(hits - 1, 0) WHERE key = $1 AND bucket = $2`,\n { params: [key, bucket] }\n ).catch(() => { /* the bucket expires on its own; never fail a request on this */ });\n\n return {\n allowed: false,\n remaining: 0,\n // When the oldest thing still counted against this caller\n // leaves the window. With a bucketed window that is the end\n // of the current bucket.\n retryAfterMs: Math.max(0, (bucket + 1) * windowMs - at)\n };\n }\n\n return {\n allowed: true,\n remaining: Math.max(0, Math.floor(limit - estimate)),\n retryAfterMs: 0\n };\n },\n\n dispose(): void {\n if (sweepTimer) clearInterval(sweepTimer);\n sweepTimer = undefined;\n }\n };\n}\n\n/**\n * The first row of whatever shape this driver returns.\n *\n * `SqlExec` declares an array of rows, but drivers in this repository have also\n * returned a `{ rows }` envelope through the same escape hatch. Reading one\n * shape only is how a store silently counts nothing and reports every caller as\n * being on their first request — a limiter that never limits, with no error\n * anywhere. Tolerating both costs one line.\n */\nconst firstRow = firstSqlRow<Record<string, unknown>>;\n","/**\n * HTML escaping for email bodies.\n *\n * Every default template interpolates values the server does not control — a\n * `displayName` chosen at registration, an `appName` from config, a link base a\n * host set — into markup that is then mailed, signed by the sending domain, to\n * an address the same request chose. Unescaped, that turns `POST /auth/register`\n * into a way to deliver arbitrary HTML (a heading, an anchor) from a domain\n * whose SPF and DKIM check out.\n *\n * The fix is deliberately not five escaping calls at five interpolation sites:\n * a sixth template would simply forget. Templates are built with the {@link html}\n * tag, which escapes *every* interpolated value by default, and markup that is\n * meant to pass through verbatim — the static style strings, a nested fragment —\n * must say so with {@link raw}. Forgetting `raw` produces visibly escaped text\n * in a test; forgetting to escape is invisible until someone exploits it.\n */\n\n/**\n * Markup that has already been vetted and must be interpolated verbatim.\n *\n * Only ever construct this from a string literal in this package's own source\n * (or from the {@link html} tag, which produces one). Wrapping user input in\n * `raw()` defeats the entire mechanism.\n */\nexport class RawHtml {\n constructor(readonly value: string) {}\n\n toString(): string {\n return this.value;\n }\n}\n\n/**\n * Mark a static, author-controlled string as safe to interpolate unescaped.\n */\nexport function raw(markup: string): RawHtml {\n return new RawHtml(markup);\n}\n\n/**\n * Escape the five characters that can change the meaning of HTML text or of a\n * quoted attribute value.\n *\n * `&` goes first: escaping it after the others would double-escape the entities\n * they just produced. `'` is included because it is a legal attribute delimiter,\n * and `\"` because it is the one this file uses.\n */\nexport function escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n/**\n * Render one interpolated value: pass {@link RawHtml} through, drop nullish,\n * escape everything else.\n */\nfunction interpolate(value: unknown): string {\n if (value instanceof RawHtml) return value.value;\n if (value === null || value === undefined) return \"\";\n return escapeHtml(String(value));\n}\n\n/**\n * Tagged template for email markup. Every `${...}` is escaped unless it is\n * {@link RawHtml}.\n *\n * Returns `RawHtml` so fragments nest without being escaped a second time:\n *\n * ```ts\n * const button = url ? html`<a href=\"${url}\">Open</a>` : raw(\"\");\n * const body = html`<div>${button}</div>`;\n * ```\n */\nexport function html(strings: TemplateStringsArray, ...values: unknown[]): RawHtml {\n let out = strings[0];\n for (let i = 0; i < values.length; i++) {\n out += interpolate(values[i]) + strings[i + 1];\n }\n return new RawHtml(out);\n}\n","/**\n * Default email templates for authentication emails.\n *\n * Every HTML body is built with the `html` tag from `./html`, which escapes each\n * interpolated value unless it is explicitly marked `raw`. `displayName` reaches\n * these templates straight from the registration body, and the resulting mail is\n * sent — signed by the sending domain — to an address the same anonymous request\n * chose, so an unescaped interpolation here is a phishing primitive, not a\n * rendering glitch. Escaping lives in the tag rather than at the call sites so a\n * sixth template inherits it instead of having to remember it.\n */\n\nimport { html, raw, RawHtml } from \"./html\";\n\ninterface TemplateUser {\n email: string;\n displayName?: string | null;\n}\n\n/**\n * Get a greeting name for the user.\n *\n * Returns the raw value — `displayName` is whatever the account was registered\n * with, including markup. Escaping is the `html` tag's job, so that a template\n * that interpolates the greeting into text (the plain-text bodies below) and one\n * that interpolates it into markup can share this.\n */\nfunction getGreeting(user: TemplateUser): string {\n return user.displayName || user.email.split(\"@\")[0];\n}\n\n/**\n * Common email styles.\n *\n * `raw` because these are static, author-written CSS — the one category of\n * interpolation the `html` tag lets through unescaped. (Escaping them would\n * survive an HTML parser but not every mail client's CSS parser: the\n * `'Segoe UI'` quotes would arrive as entities.)\n */\nconst styles = {\n container: raw(`\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n max-width: 600px;\n margin: 0 auto;\n padding: 40px 20px;\n background-color: #f9fafb;\n `),\n card: raw(`\n background-color: #ffffff;\n border-radius: 12px;\n padding: 40px;\n box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);\n `),\n heading: raw(`\n color: #26292e;\n font-size: 24px;\n font-weight: 600;\n margin: 0 0 20px 0;\n `),\n paragraph: raw(`\n color: #50545b;\n font-size: 16px;\n line-height: 1.6;\n margin: 0 0 20px 0;\n `),\n button: raw(`\n display: inline-block;\n background-color: #3b82f6;\n color: #ffffff;\n font-size: 16px;\n font-weight: 600;\n text-decoration: none;\n padding: 14px 28px;\n border-radius: 8px;\n margin: 20px 0;\n `),\n footer: raw(`\n color: #9da2aa;\n font-size: 14px;\n margin-top: 30px;\n padding-top: 20px;\n border-top: 1px solid #e5e8eb;\n `),\n warning: raw(`\n color: #6e737b;\n font-size: 14px;\n background-color: #fef3c7;\n padding: 12px 16px;\n border-radius: 6px;\n margin-top: 20px;\n `),\n /**\n * The one-time code itself. Monospace so the digits line up, spaced so a\n * `1` and a `7` are not read as one glyph, and large enough to be copied by\n * eye from a phone onto another device — which is the entire flow.\n */\n code: raw(`\n display: inline-block;\n font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n font-size: 34px;\n font-weight: 700;\n letter-spacing: 10px;\n color: #26292e;\n background-color: #f3f5f6;\n border-radius: 10px;\n padding: 18px 20px 18px 30px;\n `)\n};\n\n/**\n * The app name every template falls back to when `email.appName` is unset.\n */\nconst DEFAULT_APP_NAME = \"Rebase\";\n\n/**\n * The Rebase mark, hosted. Mail clients do not render SVG (Gmail strips it\n * outright), so this is the PNG rather than `logo.svg`, and it is an absolute\n * URL rather than a data URI because Gmail blocks those in `<img src>` too.\n */\nconst REBASE_LOGO_URL = \"https://rebase.pro/img/logo_small.png\";\n\n/**\n * Whether a configured logo can actually be fetched by a mail client.\n *\n * A relative path, a `data:` URI or a `file:` URL all produce a broken image in\n * the recipient's inbox rather than an error anyone would see, so an unusable\n * value renders no logo at all instead. This is deliberately not a boot-time\n * assert like `assertEmailLinkBases`: a dead link makes the mail useless, a\n * missing logo only makes it plain.\n */\nfunction isMailableImageUrl(url: string | undefined): url is string {\n if (!url) return false;\n try {\n const { protocol } = new URL(url);\n return protocol === \"https:\" || protocol === \"http:\";\n } catch {\n return false;\n }\n}\n\n/**\n * Whether this config still carries the stock app name — i.e. nobody has\n * branded this install.\n *\n * Testing against the *value* rather than against `undefined` is deliberate and\n * load-bearing: `APP_NAME` is declared `z.string().default(\"Rebase\")` in the\n * cloud, in the demo app and in the eject template, so `appName` arrives here as\n * the literal string \"Rebase\" even when the operator set nothing. Keying the\n * fallback on `appName === undefined` would mean the logo never appeared in any\n * Rebase-run product.\n */\nfunction isUnbranded(appName: string | undefined): boolean {\n return !appName || appName === DEFAULT_APP_NAME;\n}\n\n/**\n * Resolve the branding a template should render from the email config.\n *\n * The fallback is asymmetric on purpose. `appName` falls back to \"Rebase\"\n * because an unconfigured app has no better name to show. The *logo* is stricter:\n * an app that named itself \"Acme\" but set no `logoUrl` gets no logo at all,\n * because the alternative is mailing Acme's users a Rebase mark from Acme's\n * domain. Only an install that has not renamed itself is treated as Rebase's own.\n */\nexport function resolveEmailBranding(\n config?: { appName?: string; logoUrl?: string }\n): { appName: string; logoUrl?: string } {\n const logoUrl = config?.logoUrl ?? (isUnbranded(config?.appName) ? REBASE_LOGO_URL : undefined);\n return {\n appName: config?.appName || DEFAULT_APP_NAME,\n logoUrl: isMailableImageUrl(logoUrl) ? logoUrl : undefined\n };\n}\n\n/**\n * The logo block that sits above the card in every default template.\n *\n * `width`/`height` are attributes as well as CSS because Outlook's Word renderer\n * ignores the style block. `alt` carries the app name, for screen readers and\n * for the very common case of a client that blocks remote images on a first\n * email from an unknown sender.\n *\n * The type styling on the `<img>` is what that blocked case falls back to: alt\n * text inherits it, and at the body's 16px \"Rebase\" overflows a 48px box and\n * renders as \"Rebas\". 12px plus `height: auto` — CSS the clients that block\n * images do read, while Outlook still sizes from the attributes — lets the name\n * through intact.\n */\nfunction renderHeader(appName: string, logoUrl?: string): RawHtml {\n if (!logoUrl) return raw(\"\");\n return html`\n <div style=\"text-align: center; margin-bottom: 24px;\">\n <img src=\"${logoUrl}\" width=\"48\" height=\"48\" alt=\"${appName}\"\n style=\"display: inline-block; width: 48px; height: auto; border: 0; outline: none; text-decoration: none; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-size: 12px; color: #6e737b;\">\n </div>\n`;\n}\n\n/**\n * Default password reset email template\n */\nexport function getPasswordResetTemplate(\n resetUrl: string,\n user: TemplateUser,\n appName = DEFAULT_APP_NAME,\n logoUrl?: string\n): { subject: string; html: string; text: string } {\n const greeting = getGreeting(user);\n\n const subject = `Reset your ${appName} password`;\n\n const body = html`\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${subject}</title>\n</head>\n<body style=\"margin: 0; padding: 0; background-color: #f9fafb;\">\n <div style=\"${styles.container}\">${renderHeader(appName, logoUrl)}\n <div style=\"${styles.card}\">\n <h1 style=\"${styles.heading}\">Reset Your Password</h1>\n \n <p style=\"${styles.paragraph}\">\n Hi ${greeting},\n </p>\n \n <p style=\"${styles.paragraph}\">\n We received a request to reset your password for your ${appName} account. \n Click the button below to create a new password:\n </p>\n \n <div style=\"text-align: center;\">\n <a href=\"${resetUrl}\" style=\"${styles.button}\">Reset Password</a>\n </div>\n \n <p style=\"${styles.paragraph}\">\n Or copy and paste this link into your browser:\n </p>\n <p style=\"color: #3b82f6; word-break: break-all; font-size: 14px;\">\n ${resetUrl}\n </p>\n \n <div style=\"${styles.warning}\">\n ⏰ This link will expire in 1 hour for security reasons.\n </div>\n \n <div style=\"${styles.footer}\">\n <p style=\"margin: 0;\">\n If you didn't request a password reset, you can safely ignore this email. \n Your password will remain unchanged.\n </p>\n </div>\n </div>\n </div>\n</body>\n</html>\n `;\n\n const text = `\nReset Your Password\n\nHi ${greeting},\n\nWe received a request to reset your password for your ${appName} account.\n\nClick this link to create a new password:\n${resetUrl}\n\nThis link will expire in 1 hour for security reasons.\n\nIf you didn't request a password reset, you can safely ignore this email.\nYour password will remain unchanged.\n `.trim();\n\n return { subject,\nhtml: body.toString().trim(),\ntext };\n}\n\n/**\n * Default email verification template\n */\nexport function getEmailVerificationTemplate(\n verifyUrl: string,\n user: TemplateUser,\n appName = DEFAULT_APP_NAME,\n logoUrl?: string\n): { subject: string; html: string; text: string } {\n const greeting = getGreeting(user);\n\n const subject = `Verify your ${appName} email address`;\n\n const body = html`\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${subject}</title>\n</head>\n<body style=\"margin: 0; padding: 0; background-color: #f9fafb;\">\n <div style=\"${styles.container}\">${renderHeader(appName, logoUrl)}\n <div style=\"${styles.card}\">\n <h1 style=\"${styles.heading}\">Verify Your Email</h1>\n \n <p style=\"${styles.paragraph}\">\n Hi ${greeting},\n </p>\n \n <p style=\"${styles.paragraph}\">\n Thanks for signing up for ${appName}! Please verify your email address \n by clicking the button below:\n </p>\n \n <div style=\"text-align: center;\">\n <a href=\"${verifyUrl}\" style=\"${styles.button}\">Verify Email Address</a>\n </div>\n \n <p style=\"${styles.paragraph}\">\n Or copy and paste this link into your browser:\n </p>\n <p style=\"color: #3b82f6; word-break: break-all; font-size: 14px;\">\n ${verifyUrl}\n </p>\n \n <div style=\"${styles.footer}\">\n <p style=\"margin: 0;\">\n If you didn't create an account with ${appName}, you can safely ignore this email.\n </p>\n </div>\n </div>\n </div>\n</body>\n</html>\n `;\n\n const text = `\nVerify Your Email\n\nHi ${greeting},\n\nThanks for signing up for ${appName}! Please verify your email address by clicking this link:\n${verifyUrl}\n\nIf you didn't create an account with ${appName}, you can safely ignore this email.\n `.trim();\n\n return { subject,\nhtml: body.toString().trim(),\ntext };\n}\n\n/**\n * How long an invitation's set-password link stays valid.\n *\n * One number for both halves: `finalizeAdminUserCreation` mints the token for\n * this long and the template below tells the invitee this long. They were two\n * literals, and the email said \"1 hour\" for a token that lived 24 — so an\n * invitee who opened it the next morning believed the link was dead.\n */\nexport const USER_INVITATION_LINK_TTL_MS = 24 * 60 * 60 * 1000;\n\nconst USER_INVITATION_LINK_TTL_HOURS = USER_INVITATION_LINK_TTL_MS / (60 * 60 * 1000);\n\n/**\n * Default user invitation email template\n * Sent when an admin creates a new user account\n */\nexport function getUserInvitationTemplate(\n setPasswordUrl: string,\n user: TemplateUser,\n appName = DEFAULT_APP_NAME,\n logoUrl?: string\n): { subject: string; html: string; text: string } {\n const greeting = getGreeting(user);\n\n const subject = `You've been invited to ${appName}`;\n\n const body = html`\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${subject}</title>\n</head>\n<body style=\"margin: 0; padding: 0; background-color: #f9fafb;\">\n <div style=\"${styles.container}\">${renderHeader(appName, logoUrl)}\n <div style=\"${styles.card}\">\n <h1 style=\"${styles.heading}\">Welcome to ${appName}!</h1>\n \n <p style=\"${styles.paragraph}\">\n Hi ${greeting},\n </p>\n \n <p style=\"${styles.paragraph}\">\n An account has been created for you on ${appName}. \n Click the button below to set your password and get started:\n </p>\n \n <div style=\"text-align: center;\">\n <a href=\"${setPasswordUrl}\" style=\"${styles.button}\">Set Your Password</a>\n </div>\n \n <p style=\"${styles.paragraph}\">\n Or copy and paste this link into your browser:\n </p>\n <p style=\"color: #3b82f6; word-break: break-all; font-size: 14px;\">\n ${setPasswordUrl}\n </p>\n \n <div style=\"${styles.warning}\">\n ⏰ This link will expire in ${USER_INVITATION_LINK_TTL_HOURS} hours for security reasons.\n </div>\n \n <div style=\"${styles.footer}\">\n <p style=\"margin: 0;\">\n If you weren't expecting this invitation, you can safely ignore this email.\n </p>\n </div>\n </div>\n </div>\n</body>\n</html>\n `;\n\n const text = `\nWelcome to ${appName}!\n\nHi ${greeting},\n\nAn account has been created for you on ${appName}.\n\nClick this link to set your password and get started:\n${setPasswordUrl}\n\nThis link will expire in ${USER_INVITATION_LINK_TTL_HOURS} hours for security reasons.\n\nIf you weren't expecting this invitation, you can safely ignore this email.\n `.trim();\n\n return { subject,\nhtml: body.toString().trim(),\ntext };\n}\n\n/**\n * Default welcome email template\n * Sent automatically when a new user registers\n */\nexport function getWelcomeEmailTemplate(\n user: TemplateUser,\n appName = DEFAULT_APP_NAME,\n loginUrl?: string,\n logoUrl?: string\n): { subject: string; html: string; text: string } {\n const greeting = getGreeting(user);\n const url = loginUrl || \"\";\n\n const subject = `Welcome to ${appName}`;\n\n const body = html`\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${subject}</title>\n</head>\n<body style=\"margin: 0; padding: 0; background-color: #f9fafb;\">\n <div style=\"${styles.container}\">${renderHeader(appName, logoUrl)}\n <div style=\"${styles.card}\">\n <h1 style=\"${styles.heading}\">Welcome to ${appName}</h1>\n\n <p style=\"${styles.paragraph}\">\n Hi ${greeting},\n </p>\n\n <p style=\"${styles.paragraph}\">\n Your ${appName} account is ready.\n </p>\n\n ${url ? html`\n <div style=\"text-align: center;\">\n <a href=\"${url}\" style=\"${styles.button}\">Open ${appName}</a>\n </div>\n ` : raw(\"\")}\n\n <p style=\"${styles.paragraph}\">\n If you have any questions, just reply to this email.\n </p>\n\n <div style=\"${styles.footer}\">\n <p style=\"margin: 0;\">\n You received this because an account was created with this email address at ${appName}.\n </p>\n </div>\n </div>\n </div>\n</body>\n</html>\n `;\n\n const text = `\nWelcome to ${appName}\n\nHi ${greeting},\n\nYour ${appName} account is ready.\n\n${url ? `Open ${appName}: ${url}` : \"\"}\n\nIf you have any questions, just reply to this email.\n `.trim();\n\n return { subject,\nhtml: body.toString().trim(),\ntext };\n}\n\n/**\n * Default one-time sign-in code email.\n *\n * The code is the entire content, so it is set large and repeated in the\n * plain-text part: somebody is about to copy it by eye onto another device,\n * which is the whole reason this flow exists rather than a link.\n */\nexport function getEmailOtpTemplate(\n code: string,\n user: TemplateUser,\n appName = DEFAULT_APP_NAME,\n logoUrl?: string\n): { subject: string; html: string; text: string } {\n const greeting = getGreeting(user);\n\n // The code goes in the subject line too: a phone shows the subject on the\n // lock screen, and that is often the only place it needs to be read.\n const subject = `${code} is your ${appName} sign-in code`;\n\n const body = html`\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${subject}</title>\n</head>\n<body style=\"margin: 0; padding: 0; background-color: #f9fafb;\">\n <div style=\"${styles.container}\">${renderHeader(appName, logoUrl)}\n <div style=\"${styles.card}\">\n <h1 style=\"${styles.heading}\">Your sign-in code</h1>\n\n <p style=\"${styles.paragraph}\">\n Hi ${greeting},\n </p>\n\n <p style=\"${styles.paragraph}\">\n Enter this code to finish signing in to ${appName}:\n </p>\n\n <div style=\"text-align: center; margin: 32px 0;\">\n <span style=\"${styles.code}\">${code}</span>\n </div>\n\n <div style=\"${styles.warning}\">\n ⏰ This code expires in 10 minutes and can only be used once.\n </div>\n\n <div style=\"${styles.footer}\">\n <p style=\"margin: 0;\">\n If you didn't try to sign in, you can safely ignore this email.\n We will never ask you for this code.\n </p>\n </div>\n </div>\n </div>\n</body>\n</html>\n `;\n\n const text = `\nYour ${appName} sign-in code\n\nHi ${greeting},\n\nEnter this code to finish signing in:\n\n ${code}\n\nThis code expires in 10 minutes and can only be used once.\n\nIf you didn't try to sign in, you can safely ignore this email. We will never\nask you for this code.\n `.trim();\n\n return { subject, html: body.toString().trim(), text };\n}\n\n/**\n * Default magic link email template\n */\nexport function getMagicLinkTemplate(\n magicLinkUrl: string,\n user: TemplateUser,\n appName = DEFAULT_APP_NAME,\n logoUrl?: string\n): { subject: string; html: string; text: string } {\n const greeting = getGreeting(user);\n\n const subject = `Sign in to ${appName}`;\n\n const body = html`\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${subject}</title>\n</head>\n<body style=\"margin: 0; padding: 0; background-color: #f9fafb;\">\n <div style=\"${styles.container}\">${renderHeader(appName, logoUrl)}\n <div style=\"${styles.card}\">\n <h1 style=\"${styles.heading}\">Sign In to ${appName}</h1>\n \n <p style=\"${styles.paragraph}\">\n Hi ${greeting},\n </p>\n \n <p style=\"${styles.paragraph}\">\n We received a request to sign in to your ${appName} account. \n Click the button below to log in:\n </p>\n \n <div style=\"text-align: center;\">\n <a href=\"${magicLinkUrl}\" style=\"${styles.button}\">Sign In</a>\n </div>\n \n <p style=\"${styles.paragraph}\">\n Or copy and paste this link into your browser:\n </p>\n <p style=\"color: #3b82f6; word-break: break-all; font-size: 14px;\">\n ${magicLinkUrl}\n </p>\n \n <div style=\"${styles.warning}\">\n ⏰ This link will expire in 15 minutes for security reasons and can only be used once.\n </div>\n \n <div style=\"${styles.footer}\">\n <p style=\"margin: 0;\">\n If you didn't request this sign-in link, you can safely ignore this email. \n No action is needed.\n </p>\n </div>\n </div>\n </div>\n</body>\n</html>\n `;\n\n const text = `\nSign In to ${appName}\n\nHi ${greeting},\n\nWe received a request to sign in to your ${appName} account.\n\nClick this link to log in:\n${magicLinkUrl}\n\nThis link will expire in 15 minutes for security reasons and can only be used once.\n\nIf you didn't request this sign-in link, you can safely ignore this email.\nNo action is needed.\n `.trim();\n\n return { subject,\nhtml: body.toString().trim(),\ntext };\n}\n\n/**\n * Default address-change confirmation, mailed to the NEW address.\n *\n * Names the account being moved — its current address — because the link\n * makes the address the account's: whoever follows it hands their inbox to\n * that account, and must be able to tell whether it is theirs.\n */\nexport function getEmailChangeTemplate(\n confirmUrl: string,\n user: TemplateUser,\n newEmail: string,\n appName = DEFAULT_APP_NAME,\n logoUrl?: string\n): { subject: string; html: string; text: string } {\n const greeting = getGreeting(user);\n\n const subject = `Confirm your new ${appName} email address`;\n\n const body = html`\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${subject}</title>\n</head>\n<body style=\"margin: 0; padding: 0; background-color: #f9fafb;\">\n <div style=\"${styles.container}\">${renderHeader(appName, logoUrl)}\n <div style=\"${styles.card}\">\n <h1 style=\"${styles.heading}\">Confirm Your New Email</h1>\n\n <p style=\"${styles.paragraph}\">\n Hi ${greeting},\n </p>\n\n <p style=\"${styles.paragraph}\">\n The ${appName} account ${user.email} asked to use ${newEmail} from now on.\n Confirm it is yours by clicking the button below:\n </p>\n\n <div style=\"text-align: center;\">\n <a href=\"${confirmUrl}\" style=\"${styles.button}\">Confirm New Email</a>\n </div>\n\n <p style=\"${styles.paragraph}\">\n Or copy and paste this link into your browser:\n </p>\n <p style=\"color: #3b82f6; word-break: break-all; font-size: 14px;\">\n ${confirmUrl}\n </p>\n\n <div style=\"${styles.warning}\">\n ⏰ This link expires in 24 hours and can only be used once.\n </div>\n\n <div style=\"${styles.footer}\">\n <p style=\"margin: 0;\">\n If you did not ask for this, ignore this email: nothing changes until the link is followed.\n </p>\n </div>\n </div>\n </div>\n</body>\n</html>\n `;\n\n const text = `\nConfirm Your New Email\n\nHi ${greeting},\n\nThe ${appName} account ${user.email} asked to use ${newEmail} from now on. Confirm it is yours by following this link:\n${confirmUrl}\n\nThis link expires in 24 hours and can only be used once.\n\nIf you did not ask for this, ignore this email: nothing changes until the link is followed.\n `.trim();\n\n return { subject,\nhtml: body.toString().trim(),\ntext };\n}\n\n/**\n * Default notice to the OLD address that a change was asked for. It carries\n * no link: the old inbox can do nothing about the change except know of it,\n * and a link here would be a second way to make it.\n */\nexport function getEmailChangeNoticeTemplate(\n user: TemplateUser,\n newEmail: string,\n appName = DEFAULT_APP_NAME,\n logoUrl?: string\n): { subject: string; html: string; text: string } {\n const greeting = getGreeting(user);\n\n const subject = `Your ${appName} email address is being changed`;\n\n const body = html`\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${subject}</title>\n</head>\n<body style=\"margin: 0; padding: 0; background-color: #f9fafb;\">\n <div style=\"${styles.container}\">${renderHeader(appName, logoUrl)}\n <div style=\"${styles.card}\">\n <h1 style=\"${styles.heading}\">Email Change Requested</h1>\n\n <p style=\"${styles.paragraph}\">\n Hi ${greeting},\n </p>\n\n <p style=\"${styles.paragraph}\">\n Someone signed in to your ${appName} account asked to change its email address\n from ${user.email} to ${newEmail}. The change happens only when the link we sent\n to ${newEmail} is followed.\n </p>\n\n <div style=\"${styles.footer}\">\n <p style=\"margin: 0;\">\n If this was you, there is nothing to do here. If it was not, sign in, change your\n password and sign out of every other session.\n </p>\n </div>\n </div>\n </div>\n</body>\n</html>\n `;\n\n const text = `\nEmail Change Requested\n\nHi ${greeting},\n\nSomeone signed in to your ${appName} account asked to change its email address from ${user.email} to ${newEmail}. The change happens only when the link we sent to ${newEmail} is followed.\n\nIf this was you, there is nothing to do here. If it was not, sign in, change your password and sign out of every other session.\n `.trim();\n\n return { subject,\nhtml: body.toString().trim(),\ntext };\n}\n","import type { EmailConfig } from \"./types\";\n\n/**\n * Resolution — and boot-time validation — of the base URL every emailed link is\n * built from.\n *\n * Each link kind used to read one config field and fall back to `\"\"`, which\n * produces `href=\"/verify-email?token=…\"`. A mail client has no base document to\n * resolve that against, so the link is inert: the route still answers\n * `{ success: true }`, the token is still minted and stored, and nothing in the\n * logs says anything is wrong. `verifyEmailUrl` in particular is set by no boot\n * path, so that was the *default* behaviour of email verification.\n *\n * Two changes: the fallback chains live here rather than being re-spelled at\n * each call site (only the magic-link route had one), and\n * {@link assertEmailLinkBases} refuses at boot when any link kind has no\n * absolute base. A configuration error that only ever shows up as \"the link in\n * the email does nothing\" is worth a failed start.\n */\n\n/** Which link a base URL is being resolved for. */\nexport type EmailLinkKind = \"resetPassword\" | \"verifyEmail\" | \"magicLink\";\n\n/**\n * The config fields consulted for each link kind, most specific first.\n *\n * `resetPasswordUrl` is the general fallback because it is the only one any boot\n * path sets, and in every real deployment all three point at the same frontend.\n */\nconst LINK_BASE_FIELDS: Record<EmailLinkKind, (keyof EmailConfig)[]> = {\n resetPassword: [\"resetPasswordUrl\"],\n verifyEmail: [\"verifyEmailUrl\", \"resetPasswordUrl\"],\n magicLink: [\"magicLinkUrl\", \"resetPasswordUrl\"]\n};\n\n/**\n * True when `value` is an absolute `http(s)` URL — the only kind a mail client\n * can follow. A protocol-relative or path-only base is treated as missing.\n */\nfunction isAbsoluteHttpUrl(value: string): boolean {\n try {\n const url = new URL(value);\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n } catch {\n return false;\n }\n}\n\n/** Drop a trailing slash so callers can append `/verify-email?token=…`. */\nfunction normalizeBase(value: string): string {\n return value.replace(/\\/+$/, \"\");\n}\n\n/**\n * The absolute base URL for a link kind, or `\"\"` when the config has none.\n *\n * Callers append their path to the result. `\"\"` is only reachable on a config\n * that never went through {@link assertEmailLinkBases} — a hand-constructed\n * `SMTPEmailService` in a test, say — and is kept rather than thrown so that a\n * misconfiguration cannot turn a password-reset request into a 500 that\n * distinguishes existing accounts from missing ones.\n */\nexport function resolveEmailLinkBase(\n config: EmailConfig | undefined,\n kind: EmailLinkKind\n): string {\n if (!config) return \"\";\n for (const field of LINK_BASE_FIELDS[kind]) {\n const value = config[field];\n if (typeof value === \"string\" && isAbsoluteHttpUrl(value)) {\n return normalizeBase(value);\n }\n }\n return \"\";\n}\n\n/**\n * Throw when an email configuration cannot produce a followable link of every\n * kind it sends.\n *\n * Called from `createEmailService`, i.e. from every boot path that wires email\n * up (the managed runtime, both driver bootstrappers, and any app that passes\n * `auth.email`). A base that is set but relative is reported separately from one\n * that is missing, because the two have different fixes.\n */\nexport function assertEmailLinkBases(config: EmailConfig): void {\n const relative = ([\"resetPasswordUrl\", \"verifyEmailUrl\", \"magicLinkUrl\"] as const)\n .filter(field => typeof config[field] === \"string\" && config[field] !== \"\" && !isAbsoluteHttpUrl(config[field]!));\n\n if (relative.length > 0) {\n throw new Error(\n `Email is configured with a relative link base: ${relative.map(f => `email.${f}`).join(\", \")}. ` +\n \"Emailed links must be absolute (e.g. \\\"https://app.example.com\\\") — a mail client has no \" +\n \"base document to resolve a relative href against, so the link is dead.\"\n );\n }\n\n // Every kind, not any: the fallbacks run one way. Verification and magic\n // links fall back to `resetPasswordUrl`, and nothing falls back to them, so\n // a config with only `verifyEmailUrl` built good verification links and a\n // dead link in every password-reset and magic-link email.\n const unresolved = (Object.keys(LINK_BASE_FIELDS) as EmailLinkKind[])\n .filter(kind => !resolveEmailLinkBase(config, kind));\n\n if (unresolved.length === Object.keys(LINK_BASE_FIELDS).length) {\n throw new Error(\n \"Email is configured but no base URL for emailed links is set. Password-reset, verification \" +\n \"and magic-link emails would carry relative hrefs, which are dead links in every mail client. \" +\n \"Set FRONTEND_URL (or `auth.email.resetPasswordUrl`) to an absolute URL such as \" +\n \"\\\"https://app.example.com\\\".\"\n );\n }\n\n if (unresolved.length > 0) {\n throw new Error(\n `Email is configured without a base URL for ${unresolved.map(kind => LINK_KIND_LABELS[kind]).join(\" and \")} ` +\n \"emails, so their links would be relative hrefs — dead in every mail client. Those links fall back \" +\n \"to `auth.email.resetPasswordUrl` only: set FRONTEND_URL (or `auth.email.resetPasswordUrl`) to an \" +\n \"absolute URL such as \\\"https://app.example.com\\\".\"\n );\n }\n}\n\n/** How each link kind is named in a boot error. */\nconst LINK_KIND_LABELS: Record<EmailLinkKind, string> = {\n resetPassword: \"password-reset\",\n verifyEmail: \"verification\",\n magicLink: \"magic-link\"\n};\n","/**\n * Admin User Operations\n *\n * Shared utilities and orchestration for admin-initiated user management\n * (user creation via REST API, password reset via admin panel), and the rules\n * user administration holds to whichever door it arrives by — the admin user\n * routes, or the auth collection through the data API.\n *\n * Hook resolution order:\n * 1. Collection-level hook (`auth.onCreateUser` on the collection) — closest to the data\n * 2. Backend-level hook (`AuthHooks.onAdminCreateUser`) — global override\n * 3. Built-in default — framework fallback\n */\n\nimport { randomBytes, createHash, randomInt } from \"node:crypto\";\nimport { normalizeEmail } from \"@rebasepro/common\";\nimport type { AuthRepository } from \"./interfaces\";\nimport type { EmailService, EmailConfig } from \"../email\";\nimport type { ResolvedAuthHooks } from \"./auth-hooks\";\nimport type {\n AuthAdapter,\n AuthCollectionConfig,\n AuthCollectionContext,\n UserCreationFinalizeResult,\n UserCreationOptions,\n UserCreationPrepareResult\n} from \"@rebasepro/types\";\nimport { ApiError } from \"../api/errors\";\nimport { readRoleNames } from \"./access\";\nimport { getUserInvitationTemplate, resolveEmailBranding, USER_INVITATION_LINK_TTL_MS } from \"../email/templates\";\nimport { resolveEmailLinkBase } from \"../email/link-base\";\nimport { logger } from \"../utils/logger\";\n\n// ─── Shared Crypto Utilities ────────────────────────────────────────────────\n\n/**\n * Generate a cryptographically secure random password that meets strength requirements.\n *\n * 16 characters, guaranteed at least one uppercase, one lowercase, one digit.\n * Ambiguous characters (0, O, 1, l, I) are excluded.\n */\nexport function generateSecurePassword(): string {\n const upper = \"ABCDEFGHJKLMNPQRSTUVWXYZ\";\n const lower = \"abcdefghjkmnpqrstuvwxyz\";\n const digits = \"23456789\";\n const all = upper + lower + digits;\n\n const pick = (chars: string) => chars[randomInt(chars.length)];\n const parts = [pick(upper), pick(lower), pick(digits)];\n\n for (let i = parts.length; i < 16; i++) {\n parts.push(pick(all));\n }\n\n // Shuffle\n for (let i = parts.length - 1; i > 0; i--) {\n const j = randomInt(i + 1);\n [parts[i], parts[j]] = [parts[j], parts[i]];\n }\n return parts.join(\"\");\n}\n\n/**\n * Generate a cryptographically secure random token (80 hex characters).\n */\nexport function generateSecureToken(): string {\n return randomBytes(40).toString(\"hex\");\n}\n\n/**\n * Hash a token for database storage using SHA-256.\n */\nexport function hashToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n}\n\n// ─── Admin User Creation ────────────────────────────────────────────────────\n\n/**\n * Context needed by admin user creation / password reset operations.\n */\nexport interface AdminUserContext {\n authRepo: AuthRepository;\n emailService?: EmailService;\n emailConfig?: EmailConfig;\n resolvedHooks: ResolvedAuthHooks;\n /** The parsed auth config from the collection (if `auth` is an object, not just `true`). */\n collectionAuthConfig?: AuthCollectionConfig;\n}\n\n/**\n * Result of preparing user values for admin-initiated creation.\n */\nexport interface AdminUserPrepareResult {\n /** Values ready for `driver.save()`. */\n values: Record<string, unknown>;\n /** The cleartext password (for returning to admin or sending via email). */\n clearPassword?: string;\n /** Whether the hook already handled the invitation email. */\n hookHandledEmail: boolean;\n /** Whether an invitation was sent (only relevant when hookHandledEmail is true). */\n invitationSent: boolean;\n}\n\n/**\n * Build the `AuthCollectionContext` facade from server internals.\n *\n * This is the simplified context exposed to collection-level auth hooks,\n * keeping them decoupled from internal interfaces like `AuthRepository`.\n */\nfunction buildCollectionContext(ctx: AdminUserContext): AuthCollectionContext {\n const isEmailConfigured = !!(ctx.emailService && ctx.emailService.isConfigured());\n\n return {\n hashPassword: (password: string) => ctx.resolvedHooks.hashPassword(password),\n sendEmail: isEmailConfigured\n ? (options) => ctx.emailService!.send(options)\n : undefined,\n emailConfigured: isEmailConfigured,\n appName: ctx.emailConfig?.appName || \"Rebase\",\n resetPasswordUrl: ctx.emailConfig?.resetPasswordUrl || \"\"\n };\n}\n\n/**\n * The body field with which an administrator waives the password policy for\n * one create — an import of accounts whose passwords another system chose.\n * An instruction to the door, never data: it reaches neither the row nor a\n * create hook.\n */\nexport const SKIP_PASSWORD_POLICY_FIELD = \"skipPasswordPolicy\";\n\n/**\n * Hold a create body's chosen `password` to the password policy, as\n * registration and every other door that sets a password are, and return the\n * body without {@link SKIP_PASSWORD_POLICY_FIELD}.\n *\n * The flag waives the policy only when the door says the caller may waive it\n * (`options.mayWaivePasswordPolicy`, an administrator); from anyone else it\n * waives nothing, and a refusal says so.\n *\n * @throws ApiError 400 `WEAK_PASSWORD`\n */\nexport function enforceCreationPasswordPolicy(\n body: Record<string, unknown>,\n resolvedHooks: Pick<ResolvedAuthHooks, \"validatePasswordStrength\">,\n options?: UserCreationOptions\n): Record<string, unknown> {\n const { [SKIP_PASSWORD_POLICY_FIELD]: skipRequested, ...rest } = body;\n const waived = skipRequested === true && options?.mayWaivePasswordPolicy === true;\n const password = rest.password;\n if (!waived && typeof password === \"string\" && password !== \"\") {\n const validation = resolvedHooks.validatePasswordStrength(password);\n if (!validation.valid) {\n const refusedWaiver = skipRequested === true\n ? `. \\`${SKIP_PASSWORD_POLICY_FIELD}\\` is honoured only for administrators and the service key`\n : \"\";\n throw ApiError.badRequest(`Password too weak: ${validation.errors.join(\". \")}${refusedWaiver}`, \"WEAK_PASSWORD\");\n }\n }\n return rest;\n}\n\n/**\n * Prepare user values for an admin-initiated user creation.\n *\n * Resolution order:\n * 1. Collection-level `auth.onCreateUser` — closest to the data\n * 2. Backend-level `AuthHooks.onAdminCreateUser` — global override\n * 3. Built-in default — generate password → hash → normalize email\n *\n * Before any of them, a chosen password is held to the password policy\n * ({@link enforceCreationPasswordPolicy}).\n *\n * The caller is responsible for persisting (via `driver.save()`).\n */\nexport async function prepareAdminUserValues(\n rawBody: Record<string, unknown>,\n ctx: AdminUserContext,\n options?: UserCreationOptions\n): Promise<AdminUserPrepareResult> {\n const { resolvedHooks, collectionAuthConfig } = ctx;\n // Roles written through the auth collection obey the rule `POST /users` does.\n if (rawBody.roles !== undefined && rawBody.roles !== null) readRoleNames(rawBody.roles);\n const body = enforceCreationPasswordPolicy(rawBody, resolvedHooks, options);\n\n // 1. Collection-level hook (closest to the data)\n if (collectionAuthConfig?.onCreateUser) {\n const collectionCtx = buildCollectionContext(ctx);\n const hookResult = await collectionAuthConfig.onCreateUser(body, collectionCtx);\n return {\n values: hookResult.values,\n clearPassword: hookResult.temporaryPassword,\n hookHandledEmail: true,\n invitationSent: hookResult.invitationSent ?? false\n };\n }\n\n // 2. Backend-level hook (global override)\n if (resolvedHooks.onAdminCreateUser) {\n const hookResult = await resolvedHooks.onAdminCreateUser(body, {\n authRepo: ctx.authRepo,\n emailService: ctx.emailService,\n emailConfig: ctx.emailConfig,\n hashPassword: (password: string) => resolvedHooks.hashPassword(password)\n });\n return {\n values: hookResult.values,\n clearPassword: hookResult.temporaryPassword,\n hookHandledEmail: true,\n invitationSent: hookResult.invitationSent ?? false\n };\n }\n\n // 3. Built-in default\n const password = body.password as string | undefined;\n const clearPassword = password || generateSecurePassword();\n const passwordHash = await resolvedHooks.hashPassword(clearPassword);\n\n const values = { ...body };\n values.passwordHash = passwordHash;\n if (values.email) {\n values.email = normalizeEmail(values.email as string);\n }\n values.emailVerified = true;\n delete values.password;\n\n return {\n values,\n clearPassword: password ? undefined : clearPassword,\n hookHandledEmail: false,\n invitationSent: false\n };\n}\n\n/**\n * Handle post-creation work for admin-created users.\n *\n * Sends an invitation email (password-reset link) if email is configured\n * and no explicit password was provided. Falls back to returning the\n * temporary password if email fails or is not configured.\n */\nexport async function finalizeAdminUserCreation(\n entity: { id: string; values: Record<string, unknown> },\n clearPassword: string | undefined,\n ctx: AdminUserContext\n): Promise<{\n temporaryPassword?: string;\n invitationSent: boolean;\n emailDeliveryFailed?: boolean;\n}> {\n // If an explicit password was provided (clearPassword is undefined), nothing to do\n if (!clearPassword) {\n return { invitationSent: false };\n }\n\n const isEmailConfigured = !!(ctx.emailService && ctx.emailService.isConfigured());\n\n if (isEmailConfigured) {\n try {\n const token = generateSecureToken();\n const tokenHash = hashToken(token);\n // The TTL the invitation email states, from the same constant.\n const expiresAt = new Date(Date.now() + USER_INVITATION_LINK_TTL_MS);\n\n await ctx.authRepo.createPasswordResetToken(entity.id, tokenHash, expiresAt);\n\n // The resolver every other emailed link uses: it drops a trailing\n // slash, which the raw config value kept — `…example.com/` linked\n // to `//reset-password`, a path a router does not match.\n const baseUrl = resolveEmailLinkBase(ctx.emailConfig, \"resetPassword\");\n const setPasswordUrl = `${baseUrl}/reset-password?token=${token}`;\n\n // The invitation template, not the password-reset one.\n //\n // This is the *creation* path: the recipient has never had an\n // account, let alone a password to reset. It sent\n // \"Reset your <App> password\" anyway, because\n // `templates.userInvitation` — declared in `EmailConfig`, typed as\n // `UserInvitationTemplateFunction`, and backed by a written default\n // that `email/index.ts` exports — was read by nothing at all. The\n // one flow named for it reached past it to its neighbour.\n //\n // `getUserInvitationTemplate` says what actually happened: \"An\n // account has been created for you … set your password and get\n // started\". `reset-password-admin.ts` keeps the reset template,\n // because there the account really does already exist.\n const { appName, logoUrl } = resolveEmailBranding(ctx.emailConfig);\n const templateFn = ctx.emailConfig?.templates?.userInvitation;\n const emailContent = templateFn\n ? templateFn(setPasswordUrl, { email: entity.values.email as string,\ndisplayName: entity.values.displayName as string })\n : getUserInvitationTemplate(setPasswordUrl, { email: entity.values.email as string,\ndisplayName: entity.values.displayName as string }, appName, logoUrl);\n\n await ctx.emailService!.send({\n to: entity.values.email as string,\n subject: emailContent.subject,\n html: emailContent.html,\n text: emailContent.text\n });\n return { invitationSent: true };\n } catch (emailError: unknown) {\n logger.error(\"Failed to send reset email\", { error: emailError instanceof Error ? emailError.message : emailError });\n // Fall back to returning the temporary password\n return { temporaryPassword: clearPassword,\ninvitationSent: false,\nemailDeliveryFailed: true };\n }\n }\n\n // No email service — return the temporary password\n return { temporaryPassword: clearPassword,\ninvitationSent: false };\n}\n\n/**\n * The step after a new user's row is written: who delivers the credentials,\n * and what the create response says about it.\n *\n * A create hook that ran owns delivery. What it reported is the answer: the\n * invitation it says it sent, and the temporary password it chose, which the\n * admin has to see because it is the one the user will be told. The framework\n * sends nothing of its own. Without a hook, `finalize` runs: the built-in\n * invitation, or the generated password when email is not configured or fails.\n *\n * Both doors that create a user call this, the auth collection's REST route\n * and `POST /admin/users`. The second used to call `finalizeAdminUserCreation`\n * directly and read neither `hookHandledEmail` nor `invitationSent`, so it\n * handled a hook's password as if the framework had generated it. With email\n * configured it sent its own invitation, a second one when the hook had sent\n * its own, and left the password out of the response. The decision lives here\n * so that the two doors cannot disagree about it again.\n *\n * The result has exactly the fields a create response carries, with absent\n * ones left out.\n */\nexport async function completeUserCreation(\n prepared: UserCreationPrepareResult,\n finalize?: (clearPassword: string | undefined) => Promise<UserCreationFinalizeResult>\n): Promise<UserCreationFinalizeResult> {\n const result: UserCreationFinalizeResult = prepared.hookHandledEmail\n ? { temporaryPassword: prepared.clearPassword,\ninvitationSent: prepared.invitationSent }\n : finalize\n ? await finalize(prepared.clearPassword)\n : { invitationSent: false };\n\n return {\n invitationSent: result.invitationSent,\n ...(result.temporaryPassword ? { temporaryPassword: result.temporaryPassword } : {}),\n ...(result.emailDeliveryFailed ? { emailDeliveryFailed: true } : {})\n };\n}\n\n// ─── User Administration Rules ──────────────────────────────────────────────\n//\n// What `PUT` and `DELETE /admin/users/:uid` refuse, as functions, so the other\n// door into the same rows — the auth collection through the data API — refuses\n// it too. That door was the admin panel's own Users view, and it deleted the\n// last administrator, skipped `beforeUserDelete`, and stored emails as typed.\n\n/**\n * Refuse a change that leaves no administrator: `leaving` are users losing the\n * role (demoted, or deleted), and the project must keep at least one.\n *\n * Judged over the whole set, so a bulk write that takes every admin at once is\n * refused even though each of its rows alone would leave one behind.\n */\nasync function assertAdministratorsRemain(\n authRepo: AuthRepository,\n leaving: readonly string[],\n message: string\n): Promise<void> {\n const admins: string[] = [];\n for (const uid of new Set(leaving)) {\n if ((await authRepo.getUserRoleIds(uid)).includes(\"admin\")) admins.push(uid);\n }\n if (admins.length === 0) return;\n const { total } = await authRepo.listUsersPaginated({ roleId: \"admin\", limit: 1 });\n if (total <= admins.length) {\n throw ApiError.forbidden(message, \"LAST_ADMIN\");\n }\n}\n\n/** Refuse deleting these users when that would leave the project with no administrator. */\nexport async function assertUserDeletionsAllowed(\n authRepo: AuthRepository,\n uids: readonly string[]\n): Promise<void> {\n await assertAdministratorsRemain(\n authRepo,\n uids,\n uids.length > 1 ? \"Cannot delete every administrator\" : \"Cannot delete the last administrator\"\n );\n}\n\n/** Refuse setting these users' roles when that would leave the project with no administrator. */\nexport async function assertRoleChangesAllowed(\n authRepo: AuthRepository,\n changes: ReadonlyArray<{ uid: string; roles: readonly unknown[] }>\n): Promise<void> {\n await assertAdministratorsRemain(\n authRepo,\n changes.filter(change => !change.roles.includes(\"admin\")).map(change => change.uid),\n changes.length > 1 ? \"Cannot demote every administrator\" : \"Cannot demote the last administrator\"\n );\n}\n\n/**\n * A user's new email, in the form sign-in looks it up by — or a 409 when\n * another account holds it.\n *\n * Sign-in, password reset and magic links all find the account by\n * `normalizeEmail(input)`. An address stored as typed is one those lookups\n * never match: its owner is locked out, and registering the address again hits\n * the case-insensitive unique index.\n */\nexport async function normalizeUserEmailChange(\n authRepo: AuthRepository,\n uid: string,\n email: string\n): Promise<string> {\n const normalized = normalizeEmail(email);\n // The same 409 `POST /users` and registration give. Both engines also map\n // the unique index behind it, which covers the race; the check is what\n // answers for a custom repository that does not — unchecked, a plain\n // collision was a 500.\n const holder = await authRepo.getUserByEmail(normalized);\n if (holder && holder.id !== uid) {\n throw ApiError.conflict(\"Email already registered\", \"EMAIL_EXISTS\");\n }\n return normalized;\n}\n\n/**\n * Run `afterUserDelete` without letting it fail the deletion it follows: the\n * user is already gone, so a failing listener is logged, not answered.\n */\nexport function runAfterUserDelete(resolvedHooks: ResolvedAuthHooks, uid: string): void {\n if (!resolvedHooks.afterUserDelete) return;\n resolvedHooks.afterUserDelete(uid).catch(err => {\n logger.error(\"[AuthHooks] afterUserDelete error\", {\n error: err instanceof Error ? err.message : err\n });\n });\n}\n\n/**\n * The auth adapter's side of user administration through the auth collection\n * (`AuthAdapter.prepareUserUpdates` and the deletion pair): the admin user\n * routes' rules and hooks, for the data API's writes to the same rows.\n */\nexport function authCollectionUserAdmin(ctx: {\n authRepo: AuthRepository;\n resolvedHooks: ResolvedAuthHooks;\n}): Required<Pick<AuthAdapter, \"prepareUserUpdates\" | \"prepareUserDeletions\" | \"finalizeUserDeletions\">> {\n const { authRepo, resolvedHooks } = ctx;\n return {\n async prepareUserUpdates(updates) {\n for (const { values } of updates) {\n if (values.roles !== undefined && values.roles !== null) readRoleNames(values.roles);\n }\n await assertRoleChangesAllowed(\n authRepo,\n updates.flatMap(({ uid, values }) => Array.isArray(values.roles) ? [{ uid, roles: values.roles }] : [])\n );\n const prepared: Record<string, unknown>[] = [];\n for (const { uid, values } of updates) {\n prepared.push(typeof values.email === \"string\"\n ? { ...values, email: await normalizeUserEmailChange(authRepo, uid, values.email) }\n : values);\n }\n return prepared;\n },\n\n async prepareUserDeletions(uids) {\n await assertUserDeletionsAllowed(authRepo, uids);\n // Throws to prevent the deletion — before any row goes.\n if (resolvedHooks.beforeUserDelete) {\n for (const uid of uids) await resolvedHooks.beforeUserDelete(uid);\n }\n },\n\n async finalizeUserDeletions(uids) {\n for (const uid of uids) {\n // The sessions end with the account on every engine, not only\n // where the refresh tokens cascade with the row.\n await authRepo.deleteAllRefreshTokensForUser(uid);\n runAfterUserDelete(resolvedHooks, uid);\n }\n }\n };\n}\n","/**\n * Adapter Auth Middleware\n *\n * Creates a Hono middleware that delegates authentication to an `AuthAdapter`\n * instead of hardcoded JWT verification. This is used when the user passes\n * an `AuthAdapter` to `initializeRebaseBackend()`.\n *\n * The middleware:\n * 1. Checks for API key tokens (`rk_` prefix) first — these are Rebase-level\n * 2. Falls back to `adapter.verifyRequest(request)` to resolve the user\n * 3. Scopes the DataDriver via `withAuth()` for RLS\n * 4. Rejects a presented-but-unverifiable token with 401, whatever\n * `requireAuth` says\n * 5. Honours or refuses the RLS simulator's `x-rebase-simulate-*` headers\n * (`applySimulation`) and `x-rebase-impersonate` (`applyImpersonation`)\n * 6. Enforces auth (401) when `requireAuth` is true and no user is found\n *\n * The behavior is identical to `createAuthMiddleware()` — only the\n * token verification strategy is pluggable.\n */\n\nimport type { MiddlewareHandler, Context } from \"hono\";\nimport type { DataDriver, AuthAdapter } from \"@rebasepro/types\";\nimport { ANONYMOUS_USER_ID } from \"@rebasepro/types\";\nimport type { HonoEnv } from \"../api/types\";\nimport type { ApiKeyStore } from \"./api-keys/api-key-store\";\nimport { scopeDataDriver, SERVICE_IDENTITY } from \"./rls-scope\";\nimport { validateApiKey } from \"./api-keys/api-key-middleware\";\nimport { applyImpersonation } from \"./impersonation\";\nimport { applySimulation } from \"./simulation\";\nimport { extractBearerToken } from \"./bearer-token\";\nimport { logger } from \"../utils/logger\";\nimport { ApiError } from \"../api/errors\";\nimport { refuse } from \"./middleware\";\n\nexport interface AdapterAuthMiddlewareOptions {\n /** The auth adapter to delegate verification to. */\n adapter: AuthAdapter;\n /** The DataDriver to scope via withAuth() for RLS. */\n driver: DataDriver;\n /**\n * Optional per-request driver resolver for multi-data-source backends.\n * Returns the unscoped delegate to use for this request (e.g. Postgres vs\n * Mongo). When omitted, `driver` is used for every request.\n */\n resolveDriver?: (c: Context<HonoEnv>) => DataDriver;\n /**\n * If true, return 401 when no valid user is resolved.\n * Defaults to `true` (secure by default).\n */\n requireAuth?: boolean;\n /** Optional API key store — when provided, `rk_` bearer tokens are accepted. */\n apiKeyStore?: ApiKeyStore;\n}\n\n/**\n * What a request is refused with when `adapter.verifyRequest` throws: the\n * adapter's own answer when it gave one, otherwise a 401 — fail closed either\n * way. The built-in adapter throws the 401 that says why a session is refused\n * (`SESSION_REVOKED`, `ACCOUNT_DISABLED`) and a 503 when it cannot read the\n * caller's account (`DATABASE_UNAVAILABLE` when the database is down). Every\n * door that asks an adapter answers with this, so none of them turns an outage\n * into a bad session, which sends the client to refresh one that is fine.\n */\nexport function adapterRefusal(error: unknown): ApiError {\n return error instanceof ApiError && error.statusCode >= 400 ? error : ApiError.unauthenticated(\"Unauthorized\");\n}\n\n/**\n * Create a Hono middleware that uses an `AuthAdapter` for request verification.\n */\nexport function createAdapterAuthMiddleware(options: AdapterAuthMiddlewareOptions): MiddlewareHandler<HonoEnv> {\n const { adapter, driver: baseDriver, resolveDriver, requireAuth: enforceAuth = true, apiKeyStore } = options;\n\n return async (c, next) => {\n // Pick the per-request delegate (multi-data-source) before scoping.\n const driver = resolveDriver ? resolveDriver(c) : baseDriver;\n // Whether the caller presented a bearer token at all. A token that is\n // present and does not verify is a 401 below, exactly as in\n // `createAuthMiddleware` — see the comment there.\n const presentedToken = extractBearerToken(c.req.header(\"authorization\"));\n // ── API Key check (Rebase-level, independent of auth adapter) ────\n if (apiKeyStore) {\n const token = presentedToken ?? \"\";\n if (token.startsWith(\"rk_\")) {\n const result = await validateApiKey(c, token, { store: apiKeyStore,\ndriver });\n if (result !== true) return result;\n const refusedSimulation = await applySimulation(c, { credential: \"api-key\", driver });\n if (refusedSimulation) return refusedSimulation;\n const refused = await applyImpersonation(c, { credential: \"api-key\", driver });\n if (refused) return refused;\n return next();\n }\n }\n\n let authenticatedUser = null;\n\n try {\n authenticatedUser = await adapter.verifyRequest(c.req.raw);\n } catch (error) {\n // adapter.verifyRequest() threw — reject the request (fail closed).\n return refuse(c, adapterRefusal(error));\n }\n\n if (authenticatedUser) {\n // Authenticated — set user context and scope driver\n c.set(\"user\", {\n uid: authenticatedUser.uid,\n email: authenticatedUser.email,\n roles: authenticatedUser.roles,\n isAnonymous: authenticatedUser.isAnonymous === true\n });\n try {\n c.set(\"driver\", await scopeDataDriver(driver, {\n uid: authenticatedUser.uid,\n roles: authenticatedUser.roles,\n // A guest is a signed-in caller with no account behind\n // them. Absent from an adapter that has no such concept,\n // and absent reads as \"not a guest\".\n isAnonymous: authenticatedUser.isAnonymous === true,\n // The token's custom claims, as `createAuthMiddleware`\n // passes them: `rebase.jwt()` and claim-form tenancy read\n // them. The identity keys are written over them when the\n // driver builds the database identity.\n ...(authenticatedUser.claims ? { claims: authenticatedUser.claims } : {})\n }));\n } catch (error) {\n logger.error(\"[AUTH-ADAPTER] RLS scoping failed for authenticated user\", { error: error });\n return refuse(c, ApiError.internal(\"Internal authentication error\"));\n }\n } else {\n // Token present but invalid — always reject, regardless of\n // `requireAuth`. Presenting a malformed or expired token should\n // never grant access, and it must not be *downgraded* to an\n // anonymous request either: the SDK's refresh flow is driven by a\n // 401, so a router mounted with `requireAuth: false` — the custom\n // functions router is the only one — would answer 200 with public\n // content to a user whose access token had merely expired, and the\n // refresh would never fire.\n //\n // This is the rule `createAuthMiddleware` already applies. It lived\n // in only one of the two implementations, and this is the path that\n // runs whenever `config.auth` is a config object.\n //\n // A request with no Authorization header stays anonymous: adapters\n // may authenticate by cookie, and \"no credential\" is not \"a bad\n // credential\".\n if (presentedToken !== undefined) {\n return refuse(c, ApiError.unauthenticated(\"Invalid or expired token\"));\n }\n\n // Not authenticated — scope as anon for RLS evaluation\n try {\n c.set(\"driver\", await scopeDataDriver(driver, { uid: ANONYMOUS_USER_ID,\nroles: [\"anon\"] }));\n } catch (error) {\n logger.error(\"[AUTH-ADAPTER] Failed to create anon-scoped driver\", { error: error });\n return refuse(c, ApiError.internal(\"Server configuration error\"));\n }\n }\n\n // After the caller's own scoping, so a refusal is decided on who they\n // are, and a grant replaces their identity rather than adding to it.\n // The built-in adapter verifies the service key as `SERVICE_IDENTITY`,\n // which is how that credential is recognised here.\n const credential = !authenticatedUser\n ? \"none\"\n : authenticatedUser.uid === SERVICE_IDENTITY.uid ? \"service-key\" : \"session\";\n const resolveUser = adapter.resolveUser ? adapter.resolveUser.bind(adapter) : undefined;\n // Simulated access first: it refuses a request that also impersonates.\n const refusedSimulation = await applySimulation(c, { credential, driver, resolveUser, apiKeyStore });\n if (refusedSimulation) return refusedSimulation;\n const refused = await applyImpersonation(c, { credential, driver, resolveUser });\n if (refused) return refused;\n\n // Enforce auth if required. Word for word the JWT middleware's message,\n // for the reason set out there: a bare \"Unauthorized\" from a collection\n // whose author wrote `access: \"public\"` reads as broken RLS, and the two\n // paths must not explain the same gate differently.\n // A simulated anonymous caller has no `user` but is let past: see\n // `auth/simulation.ts`.\n if (enforceAuth && !c.get(\"user\") && !c.get(\"simulation\")) {\n return refuse(c, ApiError.unauthenticated(\n \"Unauthorized: Authentication required. This is the API-level gate, not a row \" +\n \"rule — a collection granting public reads still needs a caller. Set \" +\n \"AUTH_REQUIRE=false (or `auth.requireAuth: false`) to let RLS alone decide.\"\n ));\n }\n\n return next();\n };\n}\n","import { scrypt, randomBytes, timingSafeEqual, type ScryptOptions } from \"crypto\";\nimport { promisify } from \"util\";\n\n// `promisify` resolves to the 4-argument overload of `scrypt`, which has no\n// options parameter — so passing one is a type error, and that is exactly why\n// the parameters below sat unused. Named here so the options overload is the\n// one this module calls.\nconst scryptAsync = promisify(scrypt) as (\n password: string,\n salt: Buffer,\n keylen: number,\n options: ScryptOptions\n) => Promise<Buffer>;\n\n// Scrypt parameters (recommended values for 2024+).\n//\n// Passed explicitly. They were declared here and handed to neither call, which\n// happened to be harmless because they are also Node's defaults — so raising\n// any of them would have changed nothing while reading as though it had. That\n// is the failure worth avoiding: the next person to strengthen this needs the\n// edit to take effect.\nconst SALT_LENGTH = 32;\nconst KEY_LENGTH = 64;\nconst SCRYPT_PARAMS = { N: 16384, r: 8, p: 1 };\n\nexport interface PasswordValidationResult {\n valid: boolean;\n errors: string[];\n}\n\n/**\n * Password requirements:\n * - Minimum 8 characters\n * - At least 1 uppercase letter\n * - At least 1 lowercase letter\n * - At least 1 number\n */\nexport function validatePasswordStrength(password: string): PasswordValidationResult {\n const errors: string[] = [];\n\n if (password.length < 8) {\n errors.push(\"Password must be at least 8 characters long\");\n }\n\n if (!/[A-Z]/.test(password)) {\n errors.push(\"Password must contain at least one uppercase letter\");\n }\n\n if (!/[a-z]/.test(password)) {\n errors.push(\"Password must contain at least one lowercase letter\");\n }\n\n if (!/[0-9]/.test(password)) {\n errors.push(\"Password must contain at least one number\");\n }\n\n return {\n valid: errors.length === 0,\n errors\n };\n}\n\n/**\n * Hash a password using Node's built-in scrypt\n * Returns format: salt:hash (both hex encoded)\n */\nexport async function hashPassword(password: string): Promise<string> {\n const salt = randomBytes(SALT_LENGTH);\n const derivedKey = await scryptAsync(password, salt, KEY_LENGTH, SCRYPT_PARAMS);\n return `${salt.toString(\"hex\")}:${derivedKey.toString(\"hex\")}`;\n}\n\n/**\n * Verify a password against a scrypt hash\n * Expects format: salt:hash (both hex encoded)\n */\nexport async function verifyPassword(password: string, storedHash: string): Promise<boolean> {\n const [saltHex, hashHex] = storedHash.split(\":\");\n if (!saltHex || !hashHex) {\n return false;\n }\n\n const salt = Buffer.from(saltHex, \"hex\");\n const storedKey = Buffer.from(hashHex, \"hex\");\n // `timingSafeEqual` throws on buffers of different lengths, which turned\n // a stored key that is not ours — imported with another key length, or\n // not hex at all — into a 500 at login rather than a wrong password.\n // The length is not a secret: every hash this module writes has the same.\n if (storedKey.length !== KEY_LENGTH) {\n return false;\n }\n\n const derivedKey = await scryptAsync(password, salt, KEY_LENGTH, SCRYPT_PARAMS);\n\n // Use timing-safe comparison to prevent timing attacks\n return timingSafeEqual(derivedKey, storedKey);\n}\n","/**\n * AuthHooks\n *\n * Customize specific behaviors of the built-in Rebase auth system.\n *\n * Each method replaces one piece of the default implementation.\n * Unset methods fall through to the built-in defaults (scrypt passwords,\n * standard validation rules, etc.).\n *\n * This interface is intentionally open for extension — new hooks\n * can be added as optional methods without breaking existing configurations.\n *\n * @example bcrypt password support\n * ```ts\n * import bcrypt from \"bcrypt\";\n *\n * const hooks: AuthHooks = {\n * hashPassword: (pw) => bcrypt.hash(pw, 12),\n * verifyPassword: (pw, hash) => bcrypt.compare(pw, hash),\n * validatePasswordStrength: (pw) => ({\n * valid: pw.length >= 6,\n * errors: pw.length < 6 ? [\"Password must be at least 6 characters\"] : []\n * })\n * };\n * ```\n *\n * @example Override the entire login credential check\n * ```ts\n * const hooks: AuthHooks = {\n * verifyCredentials: async (email, password, repo) => {\n * const user = await repo.getUserByEmail(email);\n * if (!user || !user.passwordHash) return null;\n * const valid = await myCustomVerify(password, user.passwordHash);\n * return valid ? user : null;\n * }\n * };\n * ```\n */\n\nimport {\n hashPassword as defaultHashPassword,\n verifyPassword as defaultVerifyPassword,\n validatePasswordStrength as defaultValidatePasswordStrength\n} from \"./password\";\nimport type { PasswordValidationResult } from \"./password\";\nimport { ApiError } from \"../api/errors\";\nimport type { AuthRepository, UserData, CreateUserData } from \"./interfaces\";\nimport type { EmailService, EmailConfig } from \"../email\";\nimport type { AuthResponsePayload, TransformAuthResponseContext } from \"@rebasepro/types\";\n\n/**\n * Authentication method identifier for lifecycle hooks.\n */\nexport type AuthMethod = \"login\" | \"register\" | \"oauth\" | \"refresh\" | \"password-reset\" | \"anonymous\" | \"magic-link\" | \"otp\" | \"mfa\";\n\n/**\n * Hook specific parts of the built-in Rebase auth implementation.\n *\n * Every method is optional. The built-in defaults apply for any method\n * that is not provided.\n */\nexport interface AuthHooks {\n // ─── Password Operations ──────────────────────────────────────────────\n\n /**\n * Hash a cleartext password for storage.\n *\n * Default: scrypt (Node.js crypto, 64-byte key, random 32-byte salt).\n *\n * @param password - The cleartext password.\n * @returns The hashed password string (format is implementation-defined).\n */\n hashPassword?(password: string): Promise<string>;\n\n /**\n * Verify a cleartext password against a stored hash.\n *\n * Default: scrypt verification with timing-safe comparison.\n *\n * @param password - The cleartext password to check.\n * @param storedHash - The hash string retrieved from the database.\n * @returns `true` if the password matches the hash.\n */\n verifyPassword?(password: string, storedHash: string): Promise<boolean>;\n\n /**\n * Validate password strength before hashing.\n *\n * Default: minimum 8 characters, at least one uppercase, one lowercase, one digit.\n *\n * @param password - The cleartext password to validate.\n * @returns Validation result with `valid` flag and error messages.\n */\n validatePasswordStrength?(password: string): PasswordValidationResult;\n\n // ─── Credential Resolution ────────────────────────────────────────────\n\n /**\n * Override the complete credential verification during email/password login.\n *\n * When set, this replaces the default flow:\n * 1. Look up user by email\n * 2. Verify password hash\n *\n * The auth repository is provided for database access. Return the user\n * data if credentials are valid, or `null` to reject the login.\n *\n * Default: `getUserByEmail(email)` + `verifyPassword(password, user.passwordHash)`.\n */\n verifyCredentials?(email: string, password: string, repo: AuthRepository): Promise<UserData | null>;\n\n // ─── Lifecycle Hooks ──────────────────────────────────────────────────\n\n /**\n * Called after any successful authentication event: password login\n * (`login`), registration (`register`), an OAuth sign-in (`oauth`), a\n * token refresh (`refresh`), a password reset (`password-reset`), a\n * guest session (`anonymous`), a magic link (`magic-link`), an email code\n * (`otp`) and a second factor (`mfa`). Every value of {@link AuthMethod}\n * is passed by some route.\n *\n * Use for audit logging, syncing external state, updating\n * last-login timestamps, etc.\n *\n * This is fire-and-forget — errors are logged but do not fail the request.\n */\n onAuthenticated?(user: UserData, method: AuthMethod): Promise<void>;\n\n /**\n * Called before a new user is created: registration, an OAuth sign-in\n * that creates the account, a guest, and admin creation.\n *\n * Throw to refuse: the caller gets 400 `HOOK_REJECTED` with your message,\n * or the status your error carries (`ApiError`, or a 4xx `status`).\n *\n * Also called when a guest becomes an account through\n * `POST /auth/anonymous/link`, with the email and password hash it is\n * getting; the guest itself was created with `isAnonymous: true`.\n *\n * Return modified data to alter what gets stored, or throw an error\n * to reject the creation entirely.\n *\n * Default: passthrough (returns data unchanged).\n */\n beforeUserCreate?(data: CreateUserData): Promise<CreateUserData>;\n\n /**\n * Called after a new user is created.\n *\n * Use for provisioning external resources, sending notifications\n * to third-party systems, etc.\n *\n * This is fire-and-forget — errors are logged but do not fail the request.\n */\n afterUserCreate?(user: UserData): Promise<void>;\n\n // ─── Extended Lifecycle Hooks ─────────────────────────────────────────\n\n /**\n * Pre-login validation. Called before credential verification on every\n * sign-in: password (`login`), OAuth (`oauth`, with the provider's\n * address), and the requests for a magic link (`magic-link`) or an email\n * code (`otp`). Not on a token refresh, which is not a sign-in: to stop a\n * signed-in account, disable it (`PUT /admin/users/:uid { disabled: true }`).\n *\n * Throw to refuse: 400 `HOOK_REJECTED` with your message, or the status\n * your error carries.\n */\n beforeLogin?(email: string, method: AuthMethod): Promise<void>;\n\n /**\n * Post-logout cleanup.\n *\n * Called after a user's session has been invalidated.\n * Use for audit logging, cleanup of temporary resources, etc.\n *\n * This is fire-and-forget — errors are logged but do not fail the request.\n */\n afterLogout?(uid: string): Promise<void>;\n\n /**\n * Called after successful MFA verification.\n *\n * Use for audit logging, notifying external systems, etc.\n *\n * This is fire-and-forget — errors are logged but do not fail the request.\n */\n onMfaVerified?(uid: string, factorId: string): Promise<void>;\n\n /**\n * Customize JWT access token claims before signing.\n *\n * Return the modified claims object. The returned claims are merged into\n * the JWT payload BESIDE the identity claims, never over them: `uid`,\n * `roles` and `aal` are written last and a value returned for any of them\n * is discarded.\n *\n * That is not a limitation of the hook, it is what keeps it safe to have.\n * `uid` is who the whole request is — down to the identity the database\n * evaluates its policies against — `roles` is what the admin gate reads,\n * and `aal` is whether a second factor was actually passed. The obvious\n * implementation of this hook spreads the claims it was handed and adds a\n * field, and one that merges a user-controlled profile object returns\n * whatever that object happened to contain. Add facts about a session\n * here; the session's subject is decided by the server.\n *\n * @param claims - The default claims that would be included.\n * @param user - The authenticated user data.\n * @returns Modified claims to include in the JWT.\n */\n customizeAccessToken?(claims: Record<string, unknown>, user: UserData): Promise<Record<string, unknown>>;\n\n /**\n * Transform the auth response before sending it to the client.\n *\n * Called after successful login, register, refresh, OAuth, anonymous,\n * magic-link, and MFA flows. The hook receives the fully-formed\n * response and returns a (potentially enriched) response.\n *\n * Use cases:\n * - Inject tokens from external auth systems (custom provider tokens, etc.)\n * - Add project-specific metadata to the response\n * - Enrich the user object with data from external sources\n *\n * The hook runs in the request path — keep it fast.\n * Heavy work should be offloaded to `onAuthenticated` (fire-and-forget).\n */\n transformAuthResponse?(\n response: AuthResponsePayload,\n context: TransformAuthResponseContext\n ): Promise<AuthResponsePayload>;\n\n /**\n * Called after a successful password reset.\n *\n * Use for audit logging, sending confirmation notifications, etc.\n *\n * This is fire-and-forget — errors are logged but do not fail the request.\n */\n onPasswordReset?(uid: string): Promise<void>;\n\n /**\n * Called before a user is deleted.\n *\n * Throw an error to prevent deletion (e.g. for users with active\n * subscriptions, pending transactions, etc.).\n */\n beforeUserDelete?(uid: string): Promise<void>;\n\n /**\n * Called after a user is deleted.\n *\n * Use for cleaning up external resources, audit logging, etc.\n *\n * This is fire-and-forget — errors are logged but do not fail the request.\n */\n afterUserDelete?(uid: string): Promise<void>;\n\n /**\n * Called when a signed-in user asks to move their account to `newEmail`\n * (`POST /auth/change-email`), before anything is mailed. The address is\n * normalized, free and deliverable by then.\n *\n * The address rule `beforeUserCreate` enforces at sign-up — only your own\n * domain, say — belongs here too, or a member can sign up with an allowed\n * address and then move to any other.\n *\n * Throw to refuse: 400 `HOOK_REJECTED` with your message, or the status\n * your error carries.\n */\n beforeEmailChange?(user: UserData, newEmail: string): Promise<void>;\n\n /**\n * Optional hook to customize or override the default user creation flow via the admin panel/REST API.\n * When provided, this replaces the built-in password generation, hashing, and invitation email logic.\n */\n onAdminCreateUser?(\n values: Record<string, unknown>,\n ctx: {\n authRepo: AuthRepository;\n emailService?: EmailService;\n emailConfig?: EmailConfig;\n hashPassword: (password: string) => Promise<string>;\n }\n ): Promise<{\n values: Record<string, unknown>;\n temporaryPassword?: string;\n invitationSent?: boolean;\n }>;\n\n /**\n * Optional hook to customize or override the default password reset flow via the admin panel.\n * When provided, this replaces the built-in password reset token generation, hashing, and email logic.\n *\n * A `temporaryPassword` it returns becomes the account's password: the route hashes and writes\n * it (the same contract as a collection's `auth.onResetPassword`), so the hook need not write it\n * through `authRepo`. The account's existing sessions end whatever the hook returns.\n */\n onAdminResetPassword?(\n uid: string,\n ctx: {\n authRepo: AuthRepository;\n emailService?: EmailService;\n emailConfig?: EmailConfig;\n }\n ): Promise<{\n temporaryPassword?: string;\n invitationSent?: boolean;\n }>;\n}\n\n/**\n * Resolved auth hooks — password operations are guaranteed to exist,\n * all other hooks are passed through as-is (optional).\n *\n * Created by `resolveAuthHooks()` which merges user hooks\n * with built-in defaults.\n *\n * Consumers should use the resolved object exclusively —\n * never access the raw `AuthHooks` directly.\n */\nexport type ResolvedAuthHooks =\n Required<Pick<AuthHooks, \"hashPassword\" | \"verifyPassword\" | \"validatePasswordStrength\">>\n & Omit<AuthHooks, \"hashPassword\" | \"verifyPassword\" | \"validatePasswordStrength\">;\n\n/**\n * Merge user-provided hooks with the built-in defaults to produce\n * a complete set of resolved hooks.\n *\n * This is the single point where defaults are applied — all consumers\n * call this once and use the resolved hooks throughout.\n */\n/**\n * What a hook's thrown error answers.\n *\n * The hooks that refuse (`beforeUserCreate`, `beforeLogin`,\n * `beforeUserDelete`, `beforeEmailChange`) are documented as \"throw to reject\", and a plain\n * `Error` is what people throw. It reached the error handler as a 500\n * \"Internal Server Error\", so a deployment that limits sign-ups to its own\n * domain answered an outsider with a server fault. A refusal is the caller's\n * answer: 400 `HOOK_REJECTED` with the hook's message — or the status an\n * error carries, when the hook chose one (`ApiError`, or any error with a 4xx\n * `status`/`statusCode`).\n */\nexport function hookRefusal(error: unknown, hook: string): ApiError {\n if (error instanceof ApiError) return error;\n const carried = typeof error === \"object\" && error !== null\n ? (\"statusCode\" in error && typeof error.statusCode === \"number\" ? error.statusCode\n : \"status\" in error && typeof error.status === \"number\" ? error.status : undefined)\n : undefined;\n const status = carried !== undefined && carried >= 400 && carried < 500 ? carried : 400;\n const ownCode = typeof error === \"object\" && error !== null && \"code\" in error && typeof error.code === \"string\" && /^[A-Z][A-Z0-9_]+$/.test(error.code)\n ? error.code\n : undefined;\n const message = error instanceof Error ? error.message : typeof error === \"string\" ? error : `${hook} refused the request`;\n if (status === 400 && !ownCode) return ApiError.badRequest(message, \"HOOK_REJECTED\", { hook });\n return new ApiError(status, ownCode ?? \"HOOK_REJECTED\", message, { hook });\n}\n\n/** A refusing hook whose throw answers as {@link hookRefusal} says. */\nfunction refusable<A extends unknown[], R>(hook: string, fn: ((...args: A) => Promise<R>) | undefined): ((...args: A) => Promise<R>) | undefined {\n if (!fn) return undefined;\n return async (...args: A) => {\n try {\n return await fn(...args);\n } catch (error) {\n throw hookRefusal(error, hook);\n }\n };\n}\n\nexport function resolveAuthHooks(hooks?: AuthHooks): ResolvedAuthHooks {\n return {\n ...hooks,\n\n // Every door calls these through the resolved hooks, so a refusal\n // answers the same on all of them.\n beforeUserCreate: refusable(\"beforeUserCreate\", hooks?.beforeUserCreate?.bind(hooks)),\n beforeLogin: refusable(\"beforeLogin\", hooks?.beforeLogin?.bind(hooks)),\n beforeUserDelete: refusable(\"beforeUserDelete\", hooks?.beforeUserDelete?.bind(hooks)),\n beforeEmailChange: refusable(\"beforeEmailChange\", hooks?.beforeEmailChange?.bind(hooks)),\n\n hashPassword: hooks?.hashPassword\n ?? defaultHashPassword,\n\n verifyPassword: hooks?.verifyPassword\n ?? defaultVerifyPassword,\n\n validatePasswordStrength: hooks?.validatePasswordStrength\n ?? defaultValidatePasswordStrength\n };\n}\n","import type { Transporter } from \"nodemailer\";\nimport { EmailConfig, EmailSendOptions, EmailSendResult, EmailService } from \"./types\";\nimport { assertEmailLinkBases } from \"./link-base\";\nimport { logger } from \"../utils/logger\";\n\nlet _nodemailer: typeof import(\"nodemailer\") | undefined;\n\nasync function loadNodemailer() {\n if (!_nodemailer) {\n try {\n _nodemailer = await import(\"nodemailer\");\n } catch {\n throw new Error(\n \"nodemailer is required for SMTP email. \" +\n \"Install it: pnpm add nodemailer\"\n );\n }\n }\n return _nodemailer;\n}\n\n/**\n * Safely parse a hostname from a URL string\n */\nfunction getHostname(urlStr: string): string | undefined {\n try {\n const url = new URL(urlStr.includes(\"://\") ? urlStr : `https://${urlStr}`);\n return url.hostname;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Reject a header value that would end its own header.\n *\n * A CR or LF inside a value terminates the header and starts a new one, so any\n * field assembled from data the sender did not write — a subject echoed back, a\n * name from a form — is an injection point for `Bcc:`, a replaced body, or a\n * second message entirely.\n *\n * It throws rather than stripping the newline, because stripping produces a\n * message the caller did not write and tells nobody, and because a caller\n * reaching this line has a bug worth seeing. The name is checked too: RFC 5322\n * limits it to printable ASCII other than colon, and nodemailer will happily\n * pass through whatever it is given.\n */\nfunction assertSafeHeaders(headers: Record<string, string> | undefined): void {\n if (!headers) return;\n for (const [name, value] of Object.entries(headers)) {\n if (!/^[!-9;-~]+$/.test(name)) {\n throw new Error(`Invalid email header name ${JSON.stringify(name)}: expected printable ASCII without a colon.`);\n }\n if (/[\\r\\n]/.test(value)) {\n throw new Error(\n `Invalid value for email header \"${name}\": header values cannot contain CR or LF. `\n + \"A newline here would end the header and begin another one.\"\n );\n }\n }\n}\n\n/** Connections a burst of mail may hold open at once, unless `smtp.maxConnections` says otherwise. */\nconst DEFAULT_MAX_CONNECTIONS = 5;\n\n/**\n * Connect, greeting and socket-silence timeout for SMTP, unless\n * `smtp.timeoutMs` says otherwise. Under the 60 seconds a proxy commonly\n * allows, because the auth routes await their mail inside the request.\n */\nexport const DEFAULT_SMTP_TIMEOUT_MS = 30_000;\n\n/**\n * How long the connection pool may sit unused before it is closed.\n *\n * Long enough that a burst — a team invited at once, a `Promise.all` over\n * `email.send` — reuses its connections; short enough that a quiet server\n * holds no socket open to the provider, and that a script which sends one\n * message and finishes is not kept alive by an idle connection for the ten\n * minutes nodemailer would otherwise wait.\n */\nconst POOL_IDLE_CLOSE_MS = 5_000;\n\n/** `<abc@host>` → `abc@host`. See EmailSendResult.messageId for why. */\nfunction normalizeMessageId(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim().replace(/^<|>$/g, \"\").trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\n/**\n * SMTP Email Service implementation using Nodemailer\n */\nexport class SMTPEmailService implements EmailService {\n private transporter: Transporter | null = null;\n private config: EmailConfig;\n /**\n * The transporter being built, shared by every send that arrives while it\n * is.\n *\n * A flag set before the `await` said \"initialised\" while nodemailer was\n * still loading, so a second send in that window found no transporter and\n * threw \"Email service not configured\" — the first burst of mail after a\n * boot lost every message but one. Every caller awaits this one promise\n * instead. Cleared on failure, so one bad moment is not remembered for the\n * life of the process.\n */\n private transporterReady: Promise<void> | null = null;\n /** Sends between their start and their end — the pool is never closed under one. */\n private sending = 0;\n /** Closes the pool once it has been unused for {@link POOL_IDLE_CLOSE_MS}. */\n private idleTimer: ReturnType<typeof setTimeout> | undefined;\n\n constructor(config: EmailConfig) {\n this.config = config;\n }\n\n /**\n * Lazily initialize the SMTP transporter on first use\n */\n private ensureTransporter(): Promise<void> {\n if (!this.transporterReady) {\n this.transporterReady = this.buildTransporter().catch((error: unknown) => {\n this.transporterReady = null;\n throw error;\n });\n }\n return this.transporterReady;\n }\n\n private async buildTransporter(): Promise<void> {\n if (this.config.smtp) {\n const nodemailer = await loadNodemailer();\n\n let smtpName = this.config.smtp.name;\n if (!smtpName) {\n const urlsToTry = [\n process.env.FRONTEND_URL,\n this.config.resetPasswordUrl,\n this.config.verifyEmailUrl\n ];\n for (const urlStr of urlsToTry) {\n if (urlStr) {\n const hostname = getHostname(urlStr);\n if (hostname) {\n smtpName = hostname;\n break;\n }\n }\n }\n }\n\n // Pooled. Without it nodemailer opens a new connection for every\n // message — TCP, greeting, EHLO, STARTTLS and AUTH again each time —\n // and a burst of fifty opens fifty at once, which a provider that\n // caps connections per mailbox answers with 4xx refusals the\n // caller sees as failed sends. The pool queues past\n // `maxConnections` instead, and is closed when idle.\n this.transporter = nodemailer.createTransport({\n pool: true,\n maxConnections: this.config.smtp.maxConnections ?? DEFAULT_MAX_CONNECTIONS,\n name: smtpName,\n host: this.config.smtp.host,\n port: this.config.smtp.port,\n secure: this.config.smtp.secure ?? (this.config.smtp.port === 465),\n connectionTimeout: this.config.smtp.timeoutMs ?? DEFAULT_SMTP_TIMEOUT_MS,\n greetingTimeout: this.config.smtp.timeoutMs ?? DEFAULT_SMTP_TIMEOUT_MS,\n socketTimeout: this.config.smtp.timeoutMs ?? DEFAULT_SMTP_TIMEOUT_MS,\n auth: this.config.smtp.auth ? {\n user: this.config.smtp.auth.user,\n pass: this.config.smtp.auth.pass\n } : undefined\n });\n }\n }\n\n /**\n * Check if the email service is properly configured\n */\n isConfigured(): boolean {\n return !!(this.config.smtp || this.config.sendEmail);\n }\n\n /**\n * Send an email using SMTP or custom send function\n */\n async send(options: EmailSendOptions): Promise<EmailSendResult> {\n // Validated before either path, so a custom `sendEmail` provider cannot\n // be handed a header its own client will splice into the message.\n assertSafeHeaders(options.headers);\n\n // Use custom send function if provided\n if (this.config.sendEmail) {\n // A custom provider may report nothing — it is typed to allow\n // `void` so that an existing `async () => {}` still satisfies it —\n // and \"did not report an id\" is a normal outcome, not a failure.\n const result = await this.config.sendEmail(options);\n return result ?? {};\n }\n\n // Use SMTP transporter. Counted from before the transporter is built,\n // so the idle close can never land between building it and using it.\n this.sending++;\n clearTimeout(this.idleTimer);\n try {\n return await this.sendOverSmtp(options);\n } finally {\n this.sending--;\n this.closeWhenIdle();\n }\n }\n\n private async sendOverSmtp(options: EmailSendOptions): Promise<EmailSendResult> {\n await this.ensureTransporter();\n\n if (!this.transporter) {\n throw new Error(\"Email service not configured. Provide SMTP config or sendEmail function.\");\n }\n\n const to = Array.isArray(options.to) ? options.to.join(\", \") : options.to;\n\n try {\n const info = await this.transporter.sendMail({\n from: this.config.from,\n to,\n subject: options.subject,\n html: options.html,\n text: options.text,\n replyTo: options.replyTo,\n headers: options.headers\n });\n\n return {\n messageId: normalizeMessageId(info?.messageId),\n // Present on SMTP, absent on some transports. Passed through\n // as-is: a rejected recipient is the provider's report, not an\n // error, and the caller is the one who knows what to do with it.\n accepted: Array.isArray(info?.accepted) ? info.accepted.map(String) : undefined,\n rejected: Array.isArray(info?.rejected) ? info.rejected.map(String) : undefined\n };\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n logger.error(\"Failed to send email\", { detail: message });\n throw new Error(`Failed to send email: ${message}`);\n }\n }\n\n /** Arm the idle close once nothing is sending. */\n private closeWhenIdle(): void {\n if (this.sending > 0 || !this.transporter) return;\n clearTimeout(this.idleTimer);\n this.idleTimer = setTimeout(() => {\n this.idleTimer = undefined;\n if (this.sending === 0) this.releaseTransporter();\n }, POOL_IDLE_CLOSE_MS);\n this.idleTimer.unref?.();\n }\n\n /**\n * Close the pool and forget it; the next send builds a fresh one. A closed\n * nodemailer pool accepts nothing more, so it cannot be kept and reused.\n */\n private releaseTransporter(): void {\n const transporter = this.transporter;\n this.transporter = null;\n this.transporterReady = null;\n transporter?.close?.();\n }\n\n /**\n * Close the connection pool now, rather than when it next goes idle — for\n * a shutdown. A send after this opens a new pool.\n */\n close(): void {\n clearTimeout(this.idleTimer);\n this.idleTimer = undefined;\n if (this.sending === 0) this.releaseTransporter();\n }\n\n /**\n * Verify SMTP connection (useful for startup checks)\n */\n async verifyConnection(): Promise<boolean> {\n await this.ensureTransporter();\n\n if (!this.transporter) {\n return !!this.config.sendEmail;\n }\n\n try {\n await this.transporter.verify();\n return true;\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n logger.error(\"SMTP connection verification failed\", { detail: message });\n return false;\n }\n }\n}\n\n/**\n * Create an email service from configuration.\n *\n * This is the choke point every boot path goes through — the managed runtime,\n * both driver bootstrappers, and any app that passes `auth.email` — so it is\n * where the link bases are checked. A config that can send mail but cannot build\n * an absolute link fails the start rather than delivering dead `href=\"/…\"` links\n * and reporting success; see `assertEmailLinkBases`.\n */\nexport function createEmailService(config: EmailConfig): EmailService {\n assertEmailLinkBases(config);\n return new SMTPEmailService(config);\n}\n","/**\n * The development mail sink: what happens to auth email when no SMTP is set.\n *\n * ## Why this exists\n *\n * Three routes refused to work without a mail server — `POST /auth/magic-link`,\n * `POST /auth/forgot-password` and the verification resend — each answering\n * `503 EMAIL_NOT_CONFIGURED`. So the first thing a new project could not do was\n * log in, and the fix was to go and find an SMTP host. The token was never the\n * problem: it is minted, stored and valid. Only the delivery was missing.\n *\n * With this, delivery is the terminal. The message is captured and its links\n * are printed, so a developer follows the link from the log and the flow\n * completes end to end on minute one, with no account anywhere.\n *\n * ## Why it cannot reach production\n *\n * A captured password-reset mail contains a working reset token. Anything that\n * writes those to a log or holds them in memory is a credential store, so the\n * sink is wired only by {@link resolveEmailOptions}, only when `NODE_ENV` is not\n * `production`, and it says loudly what it is on first use. There is no\n * configuration that turns it on in production, deliberately: an operator who\n * wants mail in production wants a mail server, not a ring buffer.\n *\n * The buffer is also capped and in-process. It is a development convenience, not\n * a mailbox — a restart empties it, and nothing persists it.\n */\nimport type { EmailSendOptions, EmailSendResult } from \"./types\";\nimport { logger } from \"../utils/logger\";\n\n/** How many messages the ring buffer keeps before dropping the oldest. */\nconst DEFAULT_CAPACITY = 50;\n\nexport interface CapturedEmail {\n /** Monotonic within a process. Not stable across restarts. */\n id: number;\n /**\n * The synthetic Message-ID this sink reported to the caller.\n *\n * Real, in the sense that it is what `send()` returned and what an\n * application will have stored — so a flow that threads a reply against it\n * behaves the same here as against a mail server, which is the entire point\n * of the sink.\n */\n messageId: string;\n at: string;\n to: string;\n subject: string;\n html?: string;\n text?: string;\n /**\n * The absolute links found in the message, in document order.\n *\n * Extracted because this is the only part anyone needs: every auth mail\n * exists to carry one URL, and reading it out of an HTML body in a terminal\n * is miserable.\n */\n links: string[];\n}\n\nexport interface DevEmailSink {\n /** Drop-in for `EmailConfig.sendEmail`. */\n sendEmail: (options: EmailSendOptions) => Promise<EmailSendResult>;\n /** Most recent first. */\n list: () => CapturedEmail[];\n clear: () => void;\n}\n\n/**\n * Absolute http(s) URLs in a message body.\n *\n * Deliberately simple: it reads `href=\"…\"` first, because that is where a real\n * link lives, and falls back to bare URLs in the text part. Trailing markup and\n * punctuation are trimmed so the result can be pasted straight into a browser.\n */\nexport function extractLinks(html: string | undefined, text: string | undefined): string[] {\n const found: string[] = [];\n const push = (url: string) => {\n const cleaned = url.replace(/[)\\]}>.,;'\"]+$/, \"\");\n if (cleaned && !found.includes(cleaned)) found.push(cleaned);\n };\n\n if (html) {\n for (const match of html.matchAll(/href\\s*=\\s*[\"'](https?:\\/\\/[^\"']+)[\"']/gi)) {\n push(match[1]);\n }\n }\n if (text) {\n for (const match of text.matchAll(/https?:\\/\\/[^\\s<>\"']+/gi)) {\n push(match[0]);\n }\n }\n return found;\n}\n\n/** Whatever `to` was given as, rendered for a log line. */\nconst formatRecipient = (to: EmailSendOptions[\"to\"]): string =>\n Array.isArray(to) ? to.join(\", \") : to;\n\n/**\n * Create a sink. Each call is independent, which is what lets a test hold one\n * without touching whatever the process is using.\n */\nexport function createDevEmailSink(options: { capacity?: number } = {}): DevEmailSink {\n const capacity = options.capacity ?? DEFAULT_CAPACITY;\n const messages: CapturedEmail[] = [];\n let nextId = 1;\n let announced = false;\n\n const sendEmail = async (mail: EmailSendOptions): Promise<EmailSendResult> => {\n const id = nextId++;\n const captured: CapturedEmail = {\n id,\n // Shaped like a real one and unique per process, so code that\n // stores it and later matches a reply against it exercises the same\n // path in development as in production.\n messageId: `${id}.${Date.now()}.dev-sink@rebase.local`,\n at: new Date().toISOString(),\n to: formatRecipient(mail.to),\n subject: mail.subject,\n html: mail.html,\n text: mail.text,\n links: extractLinks(mail.html, mail.text)\n };\n\n messages.unshift(captured);\n if (messages.length > capacity) messages.length = capacity;\n\n // Said once, not per message: the first mail is where someone learns\n // that nothing was delivered, and repeating it every time would bury\n // the links this exists to show.\n if (!announced) {\n announced = true;\n logger.warn(\n \"No SMTP is configured, so auth email is being captured here instead of sent. \" +\n \"Follow the link below to complete the flow. Set SMTP_HOST to deliver mail for real; \" +\n \"this sink is unavailable when NODE_ENV=production.\"\n );\n }\n\n // One line per link rather than the body: the body is an HTML email.\n logger.info(\n `[email] ${captured.subject} → ${captured.to}` +\n (captured.links.length\n ? `\\n ${captured.links.join(\"\\n \")}`\n : \"\\n (no links in this message)\")\n );\n\n return { messageId: captured.messageId, accepted: [captured.to] };\n };\n\n return {\n sendEmail,\n list: () => [...messages],\n clear: () => {\n messages.length = 0;\n }\n };\n}\n\n// ── The active sink, and reading it back over HTTP ──────────────────────────\n//\n// `createDevEmailSink` is a factory so that a test can hold one privately. But\n// the mail a *running* server captured is only useful if something can read it,\n// and the thing that wants to read it — the admin panel — is nowhere near the\n// call that built the sink. So the boot path registers the one it wired, and\n// the admin route asks for it here.\n//\n// One per process, because there is one server per process and one mailbox for\n// it. Registration is refused under `NODE_ENV=production` for the same reason\n// the sink is never wired there: a captured reset mail carries a working token,\n// and this is the handle that would hand it out.\n\nlet activeSink: DevEmailSink | undefined;\n\n/**\n * Every sink registered, by the `sendEmail` it hands out: what lets a backend\n * serve the mailbox it was given rather than the process's last one. With two\n * backends in one development process, the slot above made B's admins read\n * A's users' reset and magic links.\n */\nconst sinksBySender = new WeakMap<object, DevEmailSink>();\n\n/**\n * Make `sink` the process's development mailbox.\n *\n * Returns the sink, so this can wrap the construction it replaces. In\n * production it registers nothing and warns — the caller still gets its sink\n * back and mail is still captured, it simply is not readable over HTTP.\n */\nexport function registerDevEmailSink(sink: DevEmailSink): DevEmailSink {\n if (process.env.NODE_ENV === \"production\") {\n logger.warn(\n \"[email] A development mail sink was registered under NODE_ENV=production. \" +\n \"It will not be served over HTTP: captured messages carry working credentials.\"\n );\n return sink;\n }\n activeSink = sink;\n sinksBySender.set(sink.sendEmail, sink);\n return sink;\n}\n\n/**\n * The registered mailbox whose `sendEmail` this is — the sink a backend's\n * `auth.email.sendEmail` came from — or `undefined` when it is some other\n * sender, or the process is production.\n */\nexport function devEmailSinkFor(sendEmail: object | undefined): DevEmailSink | undefined {\n if (process.env.NODE_ENV === \"production\" || !sendEmail) return undefined;\n return sinksBySender.get(sendEmail);\n}\n\n/** The registered mailbox, if there is one. */\nexport function activeDevEmailSink(): DevEmailSink | undefined {\n return process.env.NODE_ENV === \"production\" ? undefined : activeSink;\n}\n\n/** Forget the registered mailbox. For tests, and for a server shutting down. */\nexport function clearActiveDevEmailSink(): void {\n activeSink = undefined;\n}\n","/**\n * Where a rate limiter keeps its counts.\n *\n * Split out from the limiter itself because the two have different lifetimes:\n * the policy (how many, how often, keyed by what) is a property of the route,\n * and the storage is a property of the deployment. One process behind one\n * proxy is happy with a Map; several replicas behind a load balancer are not,\n * because a limit of 200 enforced independently by four of them is a limit of\n * 800.\n */\nexport interface RateLimitDecision {\n allowed: boolean;\n /** Requests left in the current window. Zero when `allowed` is false. */\n remaining: number;\n /** How long until the oldest hit falls out of the window. */\n retryAfterMs: number;\n}\n\nexport interface RateLimitStore {\n /**\n * Record a hit against `key` and say whether it is allowed.\n *\n * Called once per request, so it must not be chatty. Implementations are\n * expected to be atomic per key: two concurrent hits must not both read a\n * count of `limit - 1` and both be allowed.\n */\n hit(key: string, windowMs: number, limit: number): Promise<RateLimitDecision>;\n\n /** Release timers/connections. Tests need this; servers rarely do. */\n dispose?(): void;\n}\n\n/**\n * The default store: a sliding window in this process's memory.\n *\n * Sliding rather than fixed: a fixed window lets a caller spend its whole\n * allowance in the last second of one window and again in the first second of\n * the next, which is twice the limit over an instant. Timestamps outside the\n * window are dropped on read, and swept periodically so an idle key does not\n * pin its array forever.\n *\n * Per-process, so N replicas enforce N times the limit between them. That is\n * the honest reason {@link RateLimitStore} exists.\n */\nexport class MemoryRateLimitStore implements RateLimitStore {\n private store = new Map<string, HitWindow>();\n private cleanupInterval?: ReturnType<typeof setInterval>;\n private now: () => number;\n\n /**\n * `now` exists so the window can be driven directly instead of by sleeping:\n * a test that waits half a real window is a test that fails on a loaded\n * machine, where the sleep overruns and the hits it expects to still be in\n * the window have left it.\n */\n constructor(sweepMs = 15 * 60 * 1000, now: () => number = Date.now) {\n this.now = now;\n this.cleanupInterval = setInterval(() => this.sweep(sweepMs), sweepMs);\n // Never hold the process open for a cache sweep.\n this.cleanupInterval.unref?.();\n }\n\n async hit(key: string, windowMs: number, limit: number): Promise<RateLimitDecision> {\n const now = this.now();\n let window = this.store.get(key);\n if (!window) {\n window = { stamps: [], head: 0, ordered: true };\n this.store.set(key, window);\n }\n expire(window, now, windowMs);\n const live = window.stamps.length - window.head;\n\n if (live >= limit) {\n return {\n allowed: false,\n remaining: 0,\n retryAfterMs: Math.max(0, window.stamps[window.head] + windowMs - now)\n };\n }\n\n if (live > 0 && now < window.stamps[window.stamps.length - 1]) window.ordered = false;\n window.stamps.push(now);\n return {\n allowed: true,\n remaining: limit - (live + 1),\n retryAfterMs: 0\n };\n }\n\n private sweep(windowMs: number): void {\n const now = this.now();\n for (const [key, window] of this.store.entries()) {\n expire(window, now, windowMs);\n if (window.head === window.stamps.length) this.store.delete(key);\n }\n }\n\n dispose(): void {\n if (this.cleanupInterval) clearInterval(this.cleanupInterval);\n this.cleanupInterval = undefined;\n this.store.clear();\n }\n}\n\n/**\n * One key's hits, oldest first, from `head` on.\n *\n * Every hit used to `filter` the whole window into a new array — O(limit) work\n * and garbage per request, on the event loop: over a millisecond and 700 KB per\n * request at a limit of 100,000. Hits are recorded in the order they happen, so\n * the expired ones are a prefix: moving `head` past them is the same filter,\n * amortised O(1). The array is compacted once more than half of it is behind\n * `head`.\n *\n * `ordered` is false once a hit was recorded at an earlier time than the one\n * before it — a wall clock stepped back. Expiry is then no longer a prefix,\n * and the window is filtered as it always was until it empties.\n */\ninterface HitWindow {\n stamps: number[];\n head: number;\n ordered: boolean;\n}\n\n/** Drop the hits at least `windowMs` old — `now - t < windowMs` stays, as before. */\nfunction expire(window: HitWindow, now: number, windowMs: number): void {\n if (!window.ordered) {\n window.stamps = window.stamps.slice(window.head).filter(t => now - t < windowMs);\n window.head = 0;\n if (window.stamps.length === 0) window.ordered = true;\n return;\n }\n const { stamps } = window;\n let head = window.head;\n while (head < stamps.length && now - stamps[head] >= windowMs) head++;\n if (head === stamps.length) {\n window.stamps = [];\n window.head = 0;\n } else if (head >= 32 && head * 2 >= stamps.length) {\n window.stamps = stamps.slice(head);\n window.head = 0;\n } else {\n window.head = head;\n }\n}\n","import { MiddlewareHandler } from \"hono\";\nimport { getConnInfo } from \"@hono/node-server/conninfo\";\nimport { isAnonymousUid, type DataRateLimitCaller } from \"@rebasepro/types\";\nimport { HonoEnv } from \"../api/types\";\nimport { MemoryRateLimitStore, RateLimitDecision, RateLimitStore } from \"./rate-limit-store\";\nimport { extractBearerToken } from \"./bearer-token\";\nimport { jwtOf } from \"./jwt\";\nimport { safeCompare } from \"./crypto-utils\";\nimport { SERVICE_IDENTITY } from \"./rls-scope\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * Sliding-window rate limiting for Hono.\n *\n * The counting lives in a {@link RateLimitStore} — in this process's memory by\n * default, which is a per-replica limit and says so. See `rate-limit-store.ts`.\n */\n\nexport interface RateLimiterOptions {\n /** Time window in milliseconds (default: 15 minutes) */\n windowMs?: number;\n /** Maximum requests per window (default: 100) */\n limit?: number;\n /**\n * Key generator function. Defaults to IP-based keying.\n *\n * May return a promise: a generator that buckets by the *user* has to\n * verify a token to find one, and verification is asynchronous (see\n * `jwt-crypto.ts`). Returning a plain string is still fine, and is what\n * every IP-keyed generator does.\n */\n keyGenerator?: (c: Parameters<MiddlewareHandler<HonoEnv>>[0]) => string | Promise<string>;\n /** Custom message for rate limit responses */\n message?: string;\n /**\n * Where to keep the counts. Defaults to a private in-memory store — pass a\n * shared one to have several limiters (or several processes) agree.\n */\n store?: RateLimitStore;\n /**\n * Count in the process default's shared store once one is installed, under\n * this name. For a limiter declared at module load, which runs before any\n * configuration exists and so cannot be handed a `store` — see\n * {@link setSharedRateLimitStore}. The name namespaces the limiter's keys\n * there: two limiters keyed by the same IP must not share one count.\n * Ignored when `store` is given. A backend's own limiters are built by\n * {@link createAuthLimiters} and never read the process's store.\n */\n name?: string;\n /**\n * Per-request limit override, for buckets whose allowance is data rather\n * than config (an API key's own `rate_limit`). Returning `undefined` uses\n * `limit`; returning `null` skips the limiter for this request.\n */\n resolveLimit?: (c: Parameters<MiddlewareHandler<HonoEnv>>[0]) =>\n number | null | undefined | Promise<number | null | undefined>;\n /**\n * How many requests this one counts as. Default 1. For a request that\n * does the work of several — a batch — so batching is cheaper than the\n * requests it replaces without being free. Charged one hit at a time, and\n * refused at the first that does not fit; the hits before it stay charged,\n * as the requests a batch replaces would have been.\n */\n cost?: (c: Parameters<MiddlewareHandler<HonoEnv>>[0]) => number | Promise<number>;\n /**\n * Number of trusted reverse-proxy hops in front of this server. Each hop\n * appends the address it saw to `X-Forwarded-For`, so the real client IP is\n * the Nth entry from the right. Anything further left is client-supplied and\n * ignored — which is what stops a caller spoofing `X-Forwarded-For` to\n * rotate rate-limit keys.\n *\n * **Defaults to `0`: no proxy is trusted, and `X-Forwarded-For` and\n * `X-Real-IP` are both ignored in favour of the socket address.** A server\n * cannot tell from a request whether a proxy put that header there or the\n * caller did, so the number of hops is a deployment fact that has to be\n * declared. It previously defaulted to `1`, and nothing in the repo ever\n * set it — so on any directly-exposed server (`rebase dev`, a self-hosted\n * `rebase-server`, a passthrough load balancer) one header gave every\n * IP-keyed limiter an effective limit of one request per attacker-chosen\n * value.\n *\n * Set it — via this option or `TRUSTED_PROXY_HOPS` — to the number of\n * proxies you actually run. Behind one ingress or load balancer that is\n * `1`. Leaving it unset behind a proxy makes every client share the\n * proxy's address as one bucket, which the boot-time warning below tells\n * you about the first time it happens.\n */\n trustedProxyHops?: number;\n}\n\n/**\n * Resolve the number of trusted proxy hops from an explicit option, the\n * `TRUSTED_PROXY_HOPS` env var, or the fail-safe default of 0.\n *\n * Zero, not one: trusting a hop that is not there hands the rate-limit key to\n * the caller, and a server has no way to detect the difference from a request.\n */\nfunction resolveTrustedProxyHops(optionValue?: number): number {\n if (typeof optionValue === \"number\" && Number.isFinite(optionValue) && optionValue >= 0) {\n return Math.floor(optionValue);\n }\n const fromEnv = Number(process.env.TRUSTED_PROXY_HOPS);\n if (Number.isFinite(fromEnv) && fromEnv >= 0) {\n return Math.floor(fromEnv);\n }\n return 0;\n}\n\n/**\n * Warn once when the deployment looks proxied but has not said so.\n *\n * The safe default is the wrong answer for the other common topology: behind an\n * ingress, every request arrives from the proxy, so ignoring `X-Forwarded-For`\n * collapses every client into one bucket and the limiter starts locking real\n * users out of each other's quota. That is a loud failure in production and a\n * silent one in staging, so it gets a warning rather than a guess — guessing is\n * what made the header trustworthy in the first place.\n */\nlet warnedAboutUntrustedForwardedFor = false;\n\nfunction warnIfProxiedButUntrusted(hasForwardedFor: boolean): void {\n if (!hasForwardedFor || warnedAboutUntrustedForwardedFor) return;\n warnedAboutUntrustedForwardedFor = true;\n logger.warn(\n \"[RateLimit] Requests carry X-Forwarded-For but TRUSTED_PROXY_HOPS is 0, so it is \" +\n \"being ignored and every client behind the proxy shares one rate-limit bucket. \" +\n \"If this server really is behind a reverse proxy, set TRUSTED_PROXY_HOPS to the \" +\n \"number of proxies in front of it (1 for a single ingress or load balancer). \" +\n \"If it is exposed directly, this warning means someone is sending the header \" +\n \"themselves and 0 is the correct setting.\"\n );\n}\n\n/**\n * The store the process default's named limiters share their counts in, when\n * it has one.\n *\n * Module state, because the limiters that read it are module constants:\n * {@link defaultAuthLimiter} and its siblings are built at import time, before\n * any configuration exists, so they cannot be handed a store. A backend never\n * reads this — it builds its own limiters with {@link createAuthLimiters} — so\n * it is set only when a backend is installed as the process default.\n */\nlet sharedRateLimitStore: RateLimitStore | undefined;\n\n/**\n * Point every named limiter of the process default at a shared store — or,\n * with `undefined`, back at its own memory. `setDefaultBackend` calls it with\n * the store the installed backend shares (SQL, or one the operator supplied):\n * the default memory store is swept on the data window, which is not every\n * limiter's window.\n */\nexport function setSharedRateLimitStore(store: RateLimitStore | undefined): void {\n sharedRateLimitStore = store;\n}\n\n/** @internal The process default's shared store, to snapshot or restore it. */\nexport function sharedRateLimitStoreOfProcess(): RateLimitStore | undefined {\n return sharedRateLimitStore;\n}\n\n/** The deployment's store under `name` when there is one, this limiter's own memory otherwise. */\nfunction namedStore(name: string, windowMs: number): RateLimitStore {\n const local = new MemoryRateLimitStore(windowMs);\n return {\n hit: (key, window, limit) => sharedRateLimitStore\n ? sharedRateLimitStore.hit(`${name}:${key}`, window, limit)\n : local.hit(key, window, limit),\n dispose: () => local.dispose()\n };\n}\n\n/**\n * Create a rate-limiting middleware.\n *\n * Uses a sliding window: only hits within the last `windowMs` are counted.\n */\nexport function createRateLimiter(options: RateLimiterOptions = {}): MiddlewareHandler<HonoEnv> {\n const trustedProxyHops = resolveTrustedProxyHops(options.trustedProxyHops);\n const {\n windowMs = 15 * 60 * 1000,\n limit = 100,\n keyGenerator = (c: Parameters<MiddlewareHandler<HonoEnv>>[0]) => defaultKeyGenerator(c, trustedProxyHops),\n message = \"Too many requests, please try again later.\",\n name,\n store = name ? namedStore(name, windowMs) : new MemoryRateLimitStore(windowMs),\n resolveLimit,\n cost\n } = options;\n\n return async (c, next) => {\n const effectiveLimit = resolveLimit ? await resolveLimit(c) : limit;\n // `null` means \"not my bucket\" — e.g. an API-key limiter looking at a\n // request that carries no API key.\n if (effectiveLimit === null) return next();\n const activeLimit = effectiveLimit ?? limit;\n\n const key = await keyGenerator(c);\n let decision = await store.hit(key, windowMs, activeLimit);\n const charge = cost ? Math.max(1, Math.floor(await cost(c))) : 1;\n for (let hit = 1; hit < charge && decision.allowed; hit++) {\n decision = await store.hit(key, windowMs, activeLimit);\n }\n\n c.header(\"X-RateLimit-Limit\", String(activeLimit));\n c.header(\"X-RateLimit-Remaining\", String(decision.remaining));\n\n if (!decision.allowed) {\n const retryAfterSec = Math.ceil(decision.retryAfterMs / 1000);\n c.header(\"Retry-After\", String(retryAfterSec));\n c.header(\"X-RateLimit-Reset\", String(Math.ceil((Date.now() + decision.retryAfterMs) / 1000)));\n\n return c.json({\n error: {\n message,\n code: \"RATE_LIMITED\"\n }\n }, 429);\n }\n\n return next();\n };\n}\n\n/**\n * The address the socket is actually connected to, or `undefined` on a runtime\n * that cannot say. Unforgeable, unlike every header below.\n *\n * `getConnInfo` is Node-specific and throws elsewhere, so the failure is\n * swallowed: a runtime without connection info falls back to the shared bucket,\n * which is a availability trade the caller cannot exploit.\n */\nfunction socketAddress(c: Parameters<MiddlewareHandler<HonoEnv>>[0]): string | undefined {\n try {\n return getConnInfo(c).remote.address;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Default key generator: the client's address, from the most trustworthy source\n * this deployment has.\n *\n * `X-Forwarded-For` is a client-writable header; only the entries appended by\n * trusted reverse proxies can be believed. With `trustedProxyHops` proxies in\n * front, each appends the address it saw, so the real client IP is the\n * `trustedProxyHops`-th entry from the right — everything further left is\n * client-supplied and must be ignored. This is what prevents a caller from\n * spoofing `X-Forwarded-For` to spread its requests across many rate-limit keys.\n *\n * `X-Real-IP` is the *same* kind of header and needs the same rule, which it\n * did not have: it was read unconditionally, including under\n * `trustedProxyHops === 0` — the mode whose entire meaning is \"no proxy is in\n * front of me\". With no proxy there, nothing writes `X-Real-IP` except the\n * caller, so the key was theirs to choose: one header per request bought an\n * unlimited number of buckets, and the limiters on login, registration and\n * password reset counted to one. The reasoning had been done carefully for one\n * spelling of a proxy header and not carried to its twin.\n *\n * So `X-Real-IP` is now believed only where a proxy is declared to exist. With\n * none, the connection's own address is used — unforgeable, and available\n * because the server runs on `@hono/node-server`. `\"unknown\"` is the last\n * resort only, and it is a single shared bucket by design: better that\n * anonymous callers throttle each other than that any of them throttles nobody.\n */\n/**\n * Where a request comes from, as the rate limiters judge it: the socket's own\n * address, or — only behind as many proxies as `TRUSTED_PROXY_HOPS` declares —\n * the address they report. What a session row records as its IP, so the\n * sessions list shows the same address the limiter counted, rather than the\n * raw `X-Forwarded-For` (whose leftmost entry the caller chooses) or\n * `\"unknown\"` for every direct connection.\n */\nexport function requestClientAddress(c: Parameters<MiddlewareHandler<HonoEnv>>[0]): string {\n return defaultKeyGenerator(c);\n}\n\nfunction defaultKeyGenerator(\n c: Parameters<MiddlewareHandler<HonoEnv>>[0],\n trustedProxyHops: number = resolveTrustedProxyHops()\n): string {\n return clientAddress((name) => c.req.header(name), () => socketAddress(c), trustedProxyHops);\n}\n\n/**\n * {@link defaultKeyGenerator}'s rule, over a request's headers and connection\n * address rather than a Hono context — so a door that is not an HTTP request\n * (the realtime socket's upgrade request) reads the caller's address the same\n * way.\n */\nfunction clientAddress(\n header: (name: string) => string | undefined,\n connectionAddress: () => string | undefined,\n trustedProxyHops: number\n): string {\n if (trustedProxyHops === 0) {\n // Neither header is evidence of anything: both are trivially set by the\n // caller, and no proxy has been declared that would overwrite them.\n warnIfProxiedButUntrusted(!!header(\"x-forwarded-for\"));\n return connectionAddress() ?? \"unknown\";\n }\n\n {\n const forwardedFor = header(\"x-forwarded-for\");\n if (forwardedFor) {\n const ips = forwardedFor.split(\",\").map(s => s.trim()).filter(Boolean);\n if (ips.length > 0) {\n const idx = Math.max(0, ips.length - trustedProxyHops);\n return ips[idx];\n }\n }\n // A trusted proxy is declared to be in front, so this header is its to\n // set — and a proxy that sets only `X-Real-IP` (the stock nginx recipe)\n // is a normal deployment.\n const realIp = header(\"x-real-ip\");\n if (realIp) return realIp;\n }\n return connectionAddress() ?? \"unknown\";\n}\n\n/**\n * Pre-configured rate limiter for general auth endpoints (login, register).\n * 200 requests per 15 minutes per IP.\n */\nconst DEFAULT_AUTH_LIMIT: NamedRateLimiterOptions = {\n name: \"auth-default\",\n windowMs: 15 * 60 * 1000,\n limit: 200,\n message: \"Too many authentication attempts, please try again later.\"\n};\nexport const defaultAuthLimiter = createRateLimiter(DEFAULT_AUTH_LIMIT);\n\n/**\n * Pre-configured strict rate limiter for sensitive endpoints (password reset, verification).\n * 50 requests per 15 minutes per IP.\n */\nconst STRICT_AUTH_LIMIT: NamedRateLimiterOptions = {\n name: \"auth-strict\",\n windowMs: 15 * 60 * 1000,\n limit: 50,\n message: \"Too many requests to this sensitive endpoint, please try again later.\"\n};\nexport const strictAuthLimiter = createRateLimiter(STRICT_AUTH_LIMIT);\n\n/**\n * Limiter for `POST /auth/send-verification`, keyed by the authenticated user.\n *\n * That route had no limiter at all while every one of its email-sending\n * siblings had one, and being authenticated is not the protection it looks\n * like: registration does not verify the address it is given, so an attacker\n * registers a victim's address, signs in to the account they just made, and\n * loops the route. Each call mints a token and mails the victim.\n *\n * An IP limiter cannot express what is wanted here — the recipient is the\n * quantity being protected, not the caller — so this one keys on the uid, which\n * on this route is one-to-one with the recipient address (emails are unique per\n * account). `strictAuthLimiter` still runs in front of it to bound the caller by\n * IP; this bounds what any single address can be sent.\n *\n * Five per 15 minutes is generous for \"I didn't get the email, resend it\" and\n * useless as a mail bomb. Unauthenticated requests fall back to the IP bucket so\n * the limiter is never a no-op if it is ever mounted before the auth middleware.\n */\n/**\n * Read the recipient address out of the body, so a limiter can key on it.\n *\n * A limiter runs before the handler, and Hono caches a parsed body — so this\n * reads a clone, stashes the address, and lets the handler read the same\n * request normally. Without it the limiters below have no account to key on\n * and collapse into one global bucket, which is a limiter in name only.\n *\n * A malformed body is left to the handler, which reports it with the message\n * that names the field; failing here would answer 429 to bad JSON.\n */\nexport const captureRecipientEmail: MiddlewareHandler<HonoEnv> = async (c, next) => {\n try {\n // `c.req.json()`, NOT `c.req.raw.clone().json()`. Hono caches the parsed\n // body on the request, so reading it here is what lets the handler read\n // it again; cloning the raw Request instead marks the original stream\n // distributed, and the handler's own parse then fails — which surfaces\n // as a 500 on a route that was about to answer 503.\n const body = await c.req.json() as { email?: unknown };\n if (typeof body?.email === \"string\") {\n c.set(\"recipientEmail\", body.email.trim().toLowerCase());\n }\n } catch {\n // A malformed body is the handler's error to report, with the message\n // that names the field. Failing here would answer 429 to bad JSON.\n }\n await next();\n};\n\n/**\n * Per-ADDRESS throttle for a route that mails an address the caller named.\n *\n * `strictAuthLimiter` bounds a machine, and a machine is the attacker's to\n * multiply. The mailbox being filled belongs to somebody who cannot rotate\n * anything, and the domain the messages come from is this deployment's\n * reputation — so a distributed run against one address passed the IP limiter\n * untouched and cost the operator their sender score.\n *\n * Five per fifteen minutes: generous for \"I didn't get it, send it again\",\n * useless as a mail bomb. The same numbers as `verificationEmailLimiter`,\n * which bounds the same act for a signed-in caller.\n *\n * Falls back to the IP bucket for a request whose body named no address, so\n * the limiter is never silently a no-op.\n */\nconst RECIPIENT_EMAIL_LIMIT: NamedRateLimiterOptions = {\n name: \"auth-recipient-email\",\n windowMs: 15 * 60 * 1000,\n limit: 5,\n keyGenerator: (c) => {\n const email = c.get(\"recipientEmail\") as string | undefined;\n return email ? `recipient-email:${email}` : `recipient-email:ip:${defaultKeyGenerator(c)}`;\n },\n message: \"Too many emails requested for this address, please try again later.\"\n};\nexport const recipientEmailLimiter = createRateLimiter(RECIPIENT_EMAIL_LIMIT);\n\n/**\n * How long a route that answers \"if an account exists…\" takes, whoever asked.\n *\n * These routes deliberately answer the same words for a known and an unknown\n * address. They did not take the same TIME: a known address meant a token\n * insert, a template render and an SMTP round trip, and an unknown one meant\n * returning immediately. That difference is measurable from anywhere, so an\n * endpoint that refuses to say whether an account exists said it anyway, one\n * address at a time.\n *\n * The mail is sent off the response path at each call site, which removes the\n * largest and most variable part; this floor covers the rest without requiring\n * the two branches to be instruction-for-instruction equal, which is not a\n * property anyone can maintain across a refactor.\n *\n * 400ms is chosen to sit above the slow branch rather than to be pleasant: it\n * is a step a person takes once, and the alternative is an enumeration oracle.\n */\nexport const RECIPIENT_ROUTE_FLOOR_MS = 400;\n\n/** Hold a result until at least `floorMs` has passed since `startedAt`. */\nexport async function notBefore<T>(startedAt: number, floorMs: number, result: T): Promise<T> {\n const remaining = floorMs - (Date.now() - startedAt);\n if (remaining > 0) await new Promise((resolve) => setTimeout(resolve, remaining));\n return result;\n}\n\nconst VERIFICATION_EMAIL_LIMIT: NamedRateLimiterOptions = {\n name: \"auth-verification-email\",\n windowMs: 15 * 60 * 1000,\n limit: 5,\n keyGenerator: (c) => {\n const user = c.get(\"user\") as { uid?: string } | undefined;\n return user?.uid\n ? `verification-email:user:${user.uid}`\n : `verification-email:ip:${defaultKeyGenerator(c)}`;\n },\n message: \"Too many verification emails requested, please try again later.\"\n};\nexport const verificationEmailLimiter = createRateLimiter(VERIFICATION_EMAIL_LIMIT);\n\n/** A limiter's options, with the name it counts under in a shared store. */\nexport type NamedRateLimiterOptions = Omit<RateLimiterOptions, \"store\" | \"name\"> & { name: string };\n\n/**\n * One backend's auth limiters — login and registration, the sensitive\n * endpoints, the per-address and per-account mail budgets — and the way its\n * route modules build the rest of theirs (the OTP and MFA attempt limits).\n *\n * Every backend has its own set ({@link createAuthLimiters}), so a caller\n * spending one backend's allowance spends nothing of another's in the same\n * process. The module constants above ({@link defaultAuthLimiter} and its\n * siblings) are the process default's set, for a router built outside any\n * backend.\n */\nexport interface AuthLimiters {\n /** General auth endpoints (login, register): 200 requests per 15 minutes per address. */\n default: MiddlewareHandler<HonoEnv>;\n /** Sensitive endpoints (password reset, verification): 50 requests per 15 minutes per address. */\n strict: MiddlewareHandler<HonoEnv>;\n /** Mail to an address the caller named: 5 per 15 minutes per address. See {@link recipientEmailLimiter}. */\n recipientEmail: MiddlewareHandler<HonoEnv>;\n /** Verification mail for a signed-in caller: 5 per 15 minutes per account. See {@link verificationEmailLimiter}. */\n verificationEmail: MiddlewareHandler<HonoEnv>;\n /** Another limiter that counts where these do, under its own name. */\n named(options: NamedRateLimiterOptions): MiddlewareHandler<HonoEnv>;\n}\n\n/**\n * Build a backend's auth limiters.\n *\n * With `store` — a shared one: SQL, or what the operator passed as\n * `rateLimit.store` — every limiter counts there under its own name, so the\n * replicas of one deployment agree. Without one each limiter counts in its own\n * memory: the data limiter's memory store is swept on the data window, which\n * is not every limiter's window.\n */\nexport function createAuthLimiters(store?: RateLimitStore): AuthLimiters {\n const named = (options: NamedRateLimiterOptions): MiddlewareHandler<HonoEnv> => {\n const windowMs = options.windowMs ?? 15 * 60 * 1000;\n return createRateLimiter({\n ...options,\n windowMs,\n store: store\n ? { hit: (key, window, limit) => store.hit(`${options.name}:${key}`, window, limit) }\n : new MemoryRateLimitStore(windowMs)\n });\n };\n return {\n default: named(DEFAULT_AUTH_LIMIT),\n strict: named(STRICT_AUTH_LIMIT),\n recipientEmail: named(RECIPIENT_EMAIL_LIMIT),\n verificationEmail: named(VERIFICATION_EMAIL_LIMIT),\n named\n };\n}\n\n/**\n * Key generator for API-key-based rate limiting.\n *\n * Uses the API key ID (from `c.get(\"apiKey\")`) as the rate limit key.\n * Falls back to IP-based keying when the request is not authenticated\n * via an API key.\n */\nexport function apiKeyKeyGenerator(c: Parameters<MiddlewareHandler<HonoEnv>>[0]): string {\n const apiKey = c.get(\"apiKey\") as { id: string } | undefined;\n if (apiKey) {\n return `api-key:${apiKey.id}`;\n }\n return defaultKeyGenerator(c);\n}\n\n/** How the data API's limits are apportioned. All fields optional. */\nexport interface DataRateLimitConfig {\n /** Turn the whole thing off — for a deployment whose proxy already does it. */\n enabled?: boolean;\n /**\n * Trusted reverse-proxy hops, for the IP bucket. Same meaning and same\n * fail-safe default of 0 as {@link RateLimiterOptions.trustedProxyHops} —\n * threaded explicitly because this limiter builds its own key generator,\n * and before this existed the option was accepted by the surrounding\n * limiter and then ignored for the one bucket that keys on an address.\n */\n trustedProxyHops?: number;\n windowMs?: number;\n /** Fallback for an API key with no `rate_limit` of its own. Default 1000. */\n apiKey?: number;\n /** Per signed-in user. Default 1000. */\n user?: number;\n /**\n * Per IP, for requests with no principal at all. Default 300.\n * `null` disables the anonymous bucket entirely.\n */\n anonymous?: number | null;\n /**\n * Per IP, for anonymous requests to `/api/functions/*`. Default 3000 —\n * deliberately far looser than {@link anonymous}, because the functions\n * router is public by default for webhook receivers (Stripe, GitHub) whose\n * bursts arrive from a handful of provider IPs and would trip the data\n * API's ceiling.\n *\n * The looseness bounds the *value*, not the existence of the limit: this\n * router is the one that invites anonymous callers, so it is the last one\n * that should have no ceiling at all. `null` disables it — an explicit\n * choice, which is what the hardcoded `anonymous: null` here used to be\n * without any way to say otherwise.\n */\n anonymousFunctions?: number | null;\n /** Share counts across replicas. Defaults to this process's memory. */\n store?: RateLimitStore;\n /**\n * The deployment's service key, recognised as a Bearer before any auth\n * middleware has run — the storage router's limiter runs ahead of its\n * routes' auth — and never limited. See {@link dataRateLimitBuckets}.\n */\n serviceKey?: string;\n}\n\n/** @see DataRateLimitConfig.anonymousFunctions */\nexport const DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT = 3000;\n\n/**\n * Rate limiting for the data API.\n *\n * Every request is in exactly one bucket, resolved most-specific first: an API\n * key by its id, a signed-in user by their uid, anyone else by IP. Previously\n * only the first of those was limited at all — the middleware returned early\n * for any request without an API key — so JWT and anonymous traffic to\n * `/api/data/*` was unbounded, which is most of the traffic a BaaS gets.\n *\n * Per bucket, not per route: the point is to bound what one caller costs, and\n * they can spend it wherever they like.\n *\n * The defaults are deliberately loose. This is a floor against a runaway client\n * or a naive scraper, not a quota — a deployment that wants real quotas should\n * set them, and one that already has a proxy doing this should pass\n * `enabled: false` rather than pay for it twice.\n */\nexport function createDataRateLimiter(\n config: DataRateLimitConfig = {},\n /** See {@link RateLimiterOptions.cost}: what one request on this router counts as. */\n options: { cost?: RateLimiterOptions[\"cost\"] } = {}\n): MiddlewareHandler<HonoEnv> {\n const {\n windowMs = 15 * 60 * 1000,\n // One store for every bucket: the keys are already namespaced, and a\n // shared store is what makes a shared limit possible.\n store = new MemoryRateLimitStore(windowMs)\n } = config;\n const trustedProxyHops = resolveTrustedProxyHops(config.trustedProxyHops);\n const bucketFor = dataRateLimitBuckets(config);\n\n /**\n * Who this request is, for bucketing only.\n *\n * `c.get(\"user\")` is the answer whenever an auth middleware has already run.\n * On the storage router it has not: the limiter is registered before\n * `route(\"/\")`, and the JWT middlewares live inside those routes, so a\n * signed-in caller reached here indistinguishable from an anonymous one and\n * was bucketed `ip:` at the anonymous allowance. Everyone behind one NAT\n * shared 300 requests per window, and the admin panel — which mints a\n * download token per file — spent them on a single page of thumbnails.\n *\n * Reading the token here rather than moving the limiter is deliberate. The\n * limiter has to run *before* the routes to guard them, and pre-resolving\n * the user into the context instead would change authorization: the storage\n * adapter path enforces on `c.get(\"user\")` when its own `verifyRequest`\n * finds nobody, so seeding that key would let a Rebase-signed JWT satisfy a\n * deployment that delegates auth to Firebase or Clerk. Nothing here writes\n * to the context; an identity that fails to verify simply buckets by IP,\n * exactly as before.\n */\n const identify = async (c: Parameters<MiddlewareHandler<HonoEnv>>[0]): Promise<string | undefined> => {\n const user = c.get(\"user\") as { uid?: string } | undefined;\n if (user?.uid) return isAnonymousUid(user.uid) ? undefined : user.uid;\n\n // Only meaningful for Rebase-issued JWTs, verified by the backend\n // answering this request. A deployment authenticating through an\n // adapter has no JWT secret, and verifying would throw rather than\n // return null.\n const jwt = jwtOf(c);\n if (!jwt.isConfigured()) return undefined;\n const token = extractBearerToken(c.req.header(\"authorization\"));\n if (token === undefined) {\n // A file read by download token — an `<img src=…?token=…>`, which\n // carries no header. Charged to the user who minted the token, under\n // a mark of its own: bucketed by address, a page of thumbnails spent\n // the anonymous allowance of everyone behind the same NAT.\n const queryToken = c.req.query(\"token\");\n if (!queryToken) return undefined;\n const grant = await jwt.verifyDownloadToken(queryToken);\n return grant?.rl ? `file:${grant.rl}` : undefined;\n }\n const payload = await jwt.verifyAccessToken(token);\n if (!payload?.uid || isAnonymousUid(payload.uid)) return undefined;\n return payload.uid;\n };\n\n const isServiceKey = (c: Parameters<MiddlewareHandler<HonoEnv>>[0]): boolean => {\n if (!config.serviceKey) return false;\n const token = extractBearerToken(c.req.header(\"authorization\"));\n return token !== undefined && safeCompare(token, config.serviceKey);\n };\n\n // `createRateLimiter` asks for the key and for the limit separately, and\n // with no auth middleware ahead of it each answer verified the token again.\n // Remembered per request (keyed by the request's own Context, so it dies\n // with it), and only here: nothing is written to the context — see\n // `identify` for why that would change authorization.\n const bucketsByRequest = new WeakMap<object, Promise<DataRateLimitBucket | null>>();\n const bucketOf = (c: Parameters<MiddlewareHandler<HonoEnv>>[0]): Promise<DataRateLimitBucket | null> => {\n let bucket = bucketsByRequest.get(c);\n if (!bucket) {\n bucket = resolveBucket(c);\n bucketsByRequest.set(c, bucket);\n }\n return bucket;\n };\n\n const resolveBucket = async (c: Parameters<MiddlewareHandler<HonoEnv>>[0]): Promise<DataRateLimitBucket | null> => {\n // The service key is not a caller to bound — see `dataRateLimitBuckets`.\n // Read off the context, where the auth middleware put it — or, where no\n // auth middleware has run yet, off the request itself. It is not a JWT,\n // so verifying it as one bucketed `rebase.storage` from cron by address\n // and logged a refusal per call.\n const serviceIdentity = c.get(\"user\") as { uid?: string } | undefined;\n if (serviceIdentity?.uid === SERVICE_IDENTITY.uid) return null;\n if (isServiceKey(c)) return null;\n // Simulation and impersonation put the target in `user` (and a\n // simulated key in `apiKey`), but the administrator sent the request:\n // it counts in their bucket. Charged to the target, an admin browsing\n // \"Simulate access\" spent a production key's `rate_limit`, and the\n // integration presenting that key got 429.\n const impersonator = c.get(\"impersonator\");\n if (impersonator?.uid) {\n return bucketFor({ uid: impersonator.uid, address: () => defaultKeyGenerator(c, trustedProxyHops) });\n }\n const apiKey = c.get(\"apiKey\") as { id: string; rate_limit?: number | null } | undefined;\n return bucketFor({\n apiKey,\n // An API key's bucket is its own; no token to verify behind it.\n uid: apiKey ? undefined : await identify(c),\n address: () => defaultKeyGenerator(c, trustedProxyHops)\n });\n };\n\n return createRateLimiter({\n windowMs,\n store,\n message: \"Too many requests, please try again later.\",\n // Only asked when `resolveLimit` found a bucket.\n keyGenerator: async (c) => (await bucketOf(c))?.key ?? `ip:${defaultKeyGenerator(c, trustedProxyHops)}`,\n // `null` means \"skip the limiter\", which `createRateLimiter` honours\n // before it touches the store.\n resolveLimit: async (c) => (await bucketOf(c))?.limit ?? null,\n cost: options.cost\n });\n}\n\n/**\n * The data API's per-caller limit, as a check rather than a middleware — for\n * the door into the same data that is not an HTTP request.\n *\n * The realtime socket serves the rows the data API serves, one frame per\n * request. It counted them per connection only, so a caller bought more budget\n * by opening more sockets, and a frame never met the per-person limit its HTTP\n * twin does. Built from the same config and store as\n * {@link createDataRateLimiter}, it puts a caller in the same bucket under the\n * same key: one person's allowance, spent through either door.\n */\nexport function createDataRateLimitCheck(\n config: DataRateLimitConfig = {}\n): (caller: DataRateLimitCaller) => Promise<RateLimitDecision | null> {\n const {\n windowMs = 15 * 60 * 1000,\n store = new MemoryRateLimitStore(windowMs)\n } = config;\n const trustedProxyHops = resolveTrustedProxyHops(config.trustedProxyHops);\n const bucketFor = dataRateLimitBuckets(config);\n\n return async (caller) => {\n if (caller.uid === SERVICE_IDENTITY.uid) return null;\n const bucket = bucketFor({\n // A key's frames count where its HTTP requests do: its own bucket,\n // at its own `rate_limit`.\n apiKey: caller.apiKey,\n uid: caller.uid !== undefined && !isAnonymousUid(caller.uid) ? caller.uid : undefined,\n address: () => clientAddress(caller.header, () => caller.socketAddress, trustedProxyHops)\n });\n return bucket ? store.hit(bucket.key, windowMs, bucket.limit) : null;\n };\n}\n\n/** One caller's bucket in the data limiter's store, and its allowance there. */\ninterface DataRateLimitBucket {\n key: string;\n limit: number;\n}\n\n/**\n * Which bucket a data request counts in — the rule both doors share.\n *\n * Most specific first: an API key by its id, at its own `rate_limit`; a\n * signed-in user by their uid; anyone else by address at the anonymous\n * allowance, or not at all when that is `null`.\n *\n * The service key never reaches here: it is not a caller to bound. It is the\n * deployment's own credential and already reaches every row through\n * `SERVICE_IDENTITY`, so a limit on it protects nothing that is not already\n * open to whoever holds it — while reliably stopping the backfills,\n * migrations, imports and server-to-server jobs the key exists for, which\n * arrive as thousands of legitimate requests. A managed deployment has no\n * `rateLimit` surface to raise. Each door recognises it itself, where its\n * identity is known.\n */\nfunction dataRateLimitBuckets(config: DataRateLimitConfig): (who: {\n apiKey?: { id: string; rate_limit?: number | null };\n uid?: string;\n address: () => string;\n}) => DataRateLimitBucket | null {\n const {\n apiKey: apiKeyLimit = 1000,\n user: userLimit = 1000,\n anonymous: anonLimit = 300\n } = config;\n return (who) => {\n if (who.apiKey) return { key: `api-key:${who.apiKey.id}`, limit: who.apiKey.rate_limit ?? apiKeyLimit };\n if (who.uid) return { key: `user:${who.uid}`, limit: userLimit };\n if (anonLimit === null) return null;\n return { key: `ip:${who.address()}`, limit: anonLimit };\n };\n}\n","/**\n * Bot protection on the auth endpoints that cost something to hit.\n *\n * Rate limiting was the whole defence, and it bounds a single caller rather\n * than a botnet: a thousand addresses each sending one request per minute never\n * touch a per-IP window, and `/auth/register` and `/auth/forgot-password` both\n * send mail. So the bill for an unprotected signup form is paid in reputation on\n * a sending domain, not in CPU.\n *\n * This adds a challenge the caller has to have solved, verified server-side\n * against the provider before the route does any work.\n *\n * ## Fail closed, and why that is the safe direction here\n *\n * If the provider cannot be reached, verification fails and the request is\n * refused. That is the opposite of the choice made for the RLS audit, which\n * logs and continues, and the difference is what failure means: an audit that\n * does not run leaves you where you were, while a challenge that is not checked\n * leaves the door open for exactly as long as the outage lasts. An attacker who\n * can cause the outage would otherwise be able to turn the protection off.\n *\n * The cost is that a provider outage blocks sign-ups. That is visible, loud, and\n * recoverable by unsetting one config key — which is a better failure than a\n * silent one nobody notices until the mail domain is blocklisted.\n *\n * ## Why the verifier is injected\n *\n * Same reason as the RLS scanner: so the whole thing is testable without a\n * network, and so no provider SDK enters this package's dependency graph. The\n * built-in verifier is `fetch` and a form post, which is the entire protocol for\n * both providers this supports.\n */\nimport { createMiddleware } from \"hono/factory\";\nimport type { MiddlewareHandler } from \"hono\";\nimport { HonoEnv } from \"../api/types\";\nimport { ApiError } from \"../api/errors\";\nimport { logger } from \"../utils/logger\";\n\n/** The two providers whose verify endpoints share this exact shape. */\nexport type CaptchaProvider = \"turnstile\" | \"hcaptcha\";\n\nconst VERIFY_ENDPOINT: Record<CaptchaProvider, string> = {\n turnstile: \"https://challenges.cloudflare.com/turnstile/v0/siteverify\",\n hcaptcha: \"https://api.hcaptcha.com/siteverify\"\n};\n\nexport interface CaptchaVerifyRequest {\n token: string;\n /** The caller's address, which both providers accept as a cross-check. */\n remoteIp?: string;\n}\n\nexport interface CaptchaVerifyResult {\n success: boolean;\n /** Provider error codes, for the log line. Never returned to the caller. */\n errorCodes?: string[];\n}\n\nexport type CaptchaVerifier = (request: CaptchaVerifyRequest) => Promise<CaptchaVerifyResult>;\n\nexport interface CaptchaConfig {\n /** Off unless set. */\n enabled?: boolean;\n provider?: CaptchaProvider;\n /** The provider's secret. Required when enabled, unless `verify` is given. */\n secret?: string;\n /**\n * Which routes to protect. Defaults to the three that send mail or create\n * accounts: `register`, `forgotPassword`, `magicLink`.\n *\n * `login` is deliberately not on by default. A challenge on every sign-in is\n * a tax on every real user, and credential stuffing is what the rate limiter\n * and account lockout are for.\n */\n routes?: CaptchaRoute[];\n /**\n * Where the token is read from. Defaults to the `captchaToken` field of the\n * JSON body, with the `cf-turnstile-response` / `h-captcha-response` header\n * accepted as a fallback.\n */\n tokenField?: string;\n /** Override the verifier. Supplying it makes `secret` unnecessary. */\n verify?: CaptchaVerifier;\n /** Milliseconds before a verification attempt is abandoned. Default 5000. */\n timeoutMs?: number;\n}\n\n/**\n * A protectable auth route. `register` guards both ways an account is made\n * with a password: `POST /auth/register`, and `POST /auth/anonymous/link`,\n * which turns a guest into one.\n */\nexport type CaptchaRoute = \"register\" | \"login\" | \"forgotPassword\" | \"magicLink\" | \"emailOtp\";\n\nexport const DEFAULT_CAPTCHA_ROUTES: CaptchaRoute[] = [\"register\", \"forgotPassword\", \"magicLink\", \"emailOtp\"];\n\nconst DEFAULT_TIMEOUT_MS = 5000;\nconst DEFAULT_TOKEN_FIELD = \"captchaToken\";\n\n/** Header names the two widgets post by convention. */\nconst TOKEN_HEADERS = [\"cf-turnstile-response\", \"h-captcha-response\"];\n\n/**\n * The built-in verifier: a form post, which is the whole protocol.\n *\n * A non-200, a timeout or a malformed body are all \"not verified\" rather than\n * exceptions — the caller cannot distinguish them and should not be told which\n * one happened.\n */\nexport function createHttpCaptchaVerifier(options: {\n provider: CaptchaProvider;\n secret: string;\n timeoutMs?: number;\n fetchImpl?: typeof fetch;\n}): CaptchaVerifier {\n const endpoint = VERIFY_ENDPOINT[options.provider];\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const doFetch = options.fetchImpl ?? fetch;\n\n return async ({ token, remoteIp }) => {\n const body = new URLSearchParams({ secret: options.secret, response: token });\n if (remoteIp) body.set(\"remoteip\", remoteIp);\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n const response = await doFetch(endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n signal: controller.signal\n });\n if (!response.ok) {\n return { success: false, errorCodes: [`http-${response.status}`] };\n }\n const parsed = await response.json() as { success?: boolean; \"error-codes\"?: string[] };\n return {\n success: parsed?.success === true,\n errorCodes: parsed?.[\"error-codes\"]\n };\n } catch (err) {\n const reason = err instanceof Error && err.name === \"AbortError\" ? \"timeout\" : \"unreachable\";\n return { success: false, errorCodes: [reason] };\n } finally {\n clearTimeout(timer);\n }\n };\n}\n\n/**\n * Resolve a config into a verifier, or explain why it cannot be used.\n *\n * Returns `undefined` when the feature is off. Throws when it is on but\n * unusable — a misconfigured challenge must not boot as an absent one, because\n * the whole point is that it is there.\n */\nexport function resolveCaptchaVerifier(config: CaptchaConfig | undefined): CaptchaVerifier | undefined {\n if (!config?.enabled) return undefined;\n if (config.verify) return config.verify;\n\n if (!config.provider) {\n throw new Error(\n \"Captcha is enabled but no `provider` is set. Use \\\"turnstile\\\" or \\\"hcaptcha\\\", \" +\n \"or supply your own `verify` function.\"\n );\n }\n if (!(config.provider in VERIFY_ENDPOINT)) {\n throw new Error(\n `Captcha \\`provider\\` is \"${config.provider}\", which is not supported. ` +\n \"Use \\\"turnstile\\\" or \\\"hcaptcha\\\", or supply your own `verify` function.\"\n );\n }\n if (!config.secret) {\n throw new Error(\n `Captcha is enabled for ${config.provider} but no \\`secret\\` is set. ` +\n \"The token has to be verified server-side; a widget on its own protects nothing.\"\n );\n }\n\n return createHttpCaptchaVerifier({\n provider: config.provider,\n secret: config.secret,\n timeoutMs: config.timeoutMs\n });\n}\n\n/** Read the challenge token out of a request, body field first, then headers. */\nexport async function extractCaptchaToken(\n c: Parameters<MiddlewareHandler<HonoEnv>>[0],\n field: string\n): Promise<string | undefined> {\n for (const header of TOKEN_HEADERS) {\n const value = c.req.header(header);\n if (value) return value;\n }\n\n try {\n // Cached by Hono, so the route handler's own `c.req.json()` still works\n // — reading the body here must not consume it.\n const body = await c.req.json() as Record<string, unknown> | undefined;\n const value = body?.[field];\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n } catch {\n // Not JSON, or empty. Treated as \"no token\", which the middleware\n // refuses — the same as a wrong one.\n return undefined;\n }\n}\n\n/**\n * Middleware refusing a request whose challenge is missing or unverified.\n *\n * The response says only that the challenge failed. Which of \"absent\",\n * \"malformed\", \"already used\" or \"provider unreachable\" it was is a log line,\n * because telling a script which one it hit is telling it how to get closer.\n */\nexport function createCaptchaMiddleware(options: {\n verify: CaptchaVerifier;\n tokenField?: string;\n /** Named in the log so a failure says which route refused. */\n route: CaptchaRoute;\n}): MiddlewareHandler<HonoEnv> {\n const field = options.tokenField ?? DEFAULT_TOKEN_FIELD;\n\n return createMiddleware<HonoEnv>(async (c, next) => {\n const token = await extractCaptchaToken(c, field);\n\n if (!token) {\n logger.debug(`[captcha] ${options.route}: no token on the request`);\n throw ApiError.badRequest(\n \"A captcha challenge is required for this request.\",\n \"CAPTCHA_REQUIRED\"\n );\n }\n\n const result = await options.verify({\n token,\n remoteIp: c.req.header(\"cf-connecting-ip\") ?? undefined\n });\n\n if (!result.success) {\n logger.warn(\n `[captcha] ${options.route}: verification failed` +\n (result.errorCodes?.length ? ` (${result.errorCodes.join(\", \")})` : \"\")\n );\n throw ApiError.badRequest(\n \"The captcha challenge could not be verified.\",\n \"CAPTCHA_FAILED\"\n );\n }\n\n await next();\n });\n}\n\n/**\n * A middleware per protected route, or an empty map when the feature is off.\n *\n * Routes not named get nothing at all rather than a pass-through, so an unnamed\n * route costs exactly what it did before.\n */\nexport function buildCaptchaMiddlewares(\n config: CaptchaConfig | undefined\n): Partial<Record<CaptchaRoute, MiddlewareHandler<HonoEnv>>> {\n const verify = resolveCaptchaVerifier(config);\n if (!verify) return {};\n\n const routes = config?.routes ?? DEFAULT_CAPTCHA_ROUTES;\n const middlewares: Partial<Record<CaptchaRoute, MiddlewareHandler<HonoEnv>>> = {};\n for (const route of routes) {\n middlewares[route] = createCaptchaMiddleware({\n verify,\n tokenField: config?.tokenField,\n route\n });\n }\n return middlewares;\n}\n","/**\n * TOTP (Time-based One-Time Password) implementation.\n *\n * Pure Node.js crypto implementation — no external dependencies required.\n * Implements RFC 6238 (TOTP) and RFC 4226 (HOTP).\n */\n\nimport { createHmac, randomBytes, createHash } from \"crypto\";\n\n// ─── Base32 Encoding/Decoding ────────────────────────────────────────────────\n\nconst BASE32_ALPHABET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\";\n\n/**\n * Encode a Buffer to Base32 string (RFC 4648)\n */\nexport function base32Encode(buffer: Buffer): string {\n let bits = 0;\n let value = 0;\n let output = \"\";\n\n for (let i = 0; i < buffer.length; i++) {\n value = (value << 8) | buffer[i];\n bits += 8;\n\n while (bits >= 5) {\n output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];\n bits -= 5;\n }\n }\n\n if (bits > 0) {\n output += BASE32_ALPHABET[(value << (5 - bits)) & 31];\n }\n\n return output;\n}\n\n/**\n * Decode a Base32 string to Buffer\n */\nexport function base32Decode(encoded: string): Buffer {\n const cleanInput = encoded.replace(/=+$/, \"\").toUpperCase();\n const bytes: number[] = [];\n let bits = 0;\n let value = 0;\n\n for (let i = 0; i < cleanInput.length; i++) {\n const index = BASE32_ALPHABET.indexOf(cleanInput[i]);\n if (index === -1) continue;\n\n value = (value << 5) | index;\n bits += 5;\n\n if (bits >= 8) {\n bytes.push((value >>> (bits - 8)) & 255);\n bits -= 8;\n }\n }\n\n return Buffer.from(bytes);\n}\n\n// ─── HOTP/TOTP ───────────────────────────────────────────────────────────────\n\n/**\n * Generate an HOTP value per RFC 4226\n */\nfunction generateHotp(secret: Buffer, counter: bigint): string {\n const hmac = createHmac(\"sha1\", secret);\n const counterBuffer = Buffer.alloc(8);\n counterBuffer.writeBigInt64BE(counter);\n hmac.update(counterBuffer);\n const hash = hmac.digest();\n\n const offset = hash[hash.length - 1] & 0x0f;\n const code =\n (((hash[offset] & 0x7f) << 24) |\n (hash[offset + 1] << 16) |\n (hash[offset + 2] << 8) |\n hash[offset + 3]) %\n 1000000;\n\n return code.toString().padStart(6, \"0\");\n}\n\n/**\n * Generate a TOTP value for the current time step\n */\nexport function generateTotp(secret: Buffer, timeStep = 30): string {\n const counter = BigInt(Math.floor(Date.now() / 1000 / timeStep));\n return generateHotp(secret, counter);\n}\n\n/**\n * Verify a TOTP token with a configurable time window\n *\n * @param secret - The shared secret as a Buffer\n * @param token - The 6-digit TOTP code to verify\n * @param window - Number of time steps to check on each side (default: 1)\n * @returns true if the token is valid within the window\n */\nexport function verifyTotp(secret: Buffer, token: string, window = 1): boolean {\n return verifyTotpCounter(secret, token, window) !== null;\n}\n\n/**\n * Verify a TOTP token and report *which* time step matched.\n *\n * RFC 6238 §5.2 requires that an accepted OTP not be accepted a second time,\n * and a boolean cannot express what was accepted: with `window = 1` the same\n * six digits stay valid for up to 90 seconds, so a code observed once — by a\n * phishing relay, over a shoulder, in a pasted support message — can be\n * presented again for the rest of that window. The caller records the returned\n * counter against the factor and refuses anything at or below it.\n *\n * @param secret - The shared secret as a Buffer\n * @param token - The 6-digit TOTP code to verify\n * @param window - Number of time steps to check on each side (default: 1)\n * @returns The matched time step, or `null` when no step in the window matches\n */\nexport function verifyTotpCounter(secret: Buffer, token: string, window = 1): number | null {\n const timeStep = 30;\n const counter = BigInt(Math.floor(Date.now() / 1000 / timeStep));\n for (let i = -window; i <= window; i++) {\n const step = counter + BigInt(i);\n if (generateHotp(secret, step) === token) {\n return Number(step);\n }\n }\n return null;\n}\n\n// ─── Secret Generation ───────────────────────────────────────────────────────\n\n/**\n * Generate a new TOTP secret and return the setup information\n *\n * @param issuer - The issuer name (app name) for the QR code\n * @param accountName - The account identifier (usually email)\n * @returns Object with base32 secret and otpauth URI\n */\nexport function generateTotpSecret(issuer: string, accountName: string): {\n secret: string;\n uri: string;\n} {\n // Generate a 20-byte random secret (160 bits, recommended by RFC 4226)\n const secretBuffer = randomBytes(20);\n const secret = base32Encode(secretBuffer);\n\n // Build otpauth:// URI for QR code generation\n const encodedIssuer = encodeURIComponent(issuer);\n const encodedAccount = encodeURIComponent(accountName);\n const uri = `otpauth://totp/${encodedIssuer}:${encodedAccount}?secret=${secret}&issuer=${encodedIssuer}&algorithm=SHA1&digits=6&period=30`;\n\n return { secret,\nuri };\n}\n\n// ─── Recovery Codes ──────────────────────────────────────────────────────────\n\n/**\n * Generate a set of one-time recovery codes\n *\n * @param count - Number of recovery codes to generate (default: 10)\n * @returns Array of formatted recovery code strings (e.g. \"A1B2C-D3E4F\")\n */\nexport function generateRecoveryCodes(count = 10): string[] {\n return Array.from({ length: count }, () => {\n const raw = randomBytes(5).toString(\"hex\").toUpperCase();\n const parts = raw.match(/.{1,5}/g);\n return parts ? parts.join(\"-\") : raw;\n });\n}\n\n/**\n * Hash a recovery code for storage\n */\nexport function hashRecoveryCode(code: string): string {\n return createHash(\"sha256\").update(code.replace(/-/g, \"\").toUpperCase()).digest(\"hex\");\n}\n","/**\n * Envelope encryption for TOTP secrets using AES-256-GCM.\n *\n * - Derives a 32-byte key via SHA-256 from `MFA_ENCRYPTION_KEY` or `JWT_SECRET`.\n * - Generates a random 12-byte IV per encryption call.\n * - Stamps the key that made the ciphertext, so the key can be changed without\n * locking every enrolled user out — see \"Rotating the key\" below.\n *\n * ## Rotating the key\n *\n * A ciphertext used to be `iv:authTag:ciphertext` and recorded nothing about\n * which key produced it, while the key was re-derived from the environment on\n * every call. Setting `MFA_ENCRYPTION_KEY` on a deployment that had been\n * falling back to `JWT_SECRET` therefore made every stored factor undecryptable\n * — AES-GCM authenticates, so it did not return a wrong secret, it threw, and\n * every MFA sign-in failed until the variable was removed again.\n *\n * Now a ciphertext carries `v1.<keyId>`, where `keyId` is derived from the key\n * itself (no key names to keep in sync), and decryption runs against a list of\n * candidates: `MFA_ENCRYPTION_KEY`, then each comma-separated entry in\n * `MFA_ENCRYPTION_KEY_PREVIOUS`, then `JWT_SECRET`. To rotate:\n *\n * 1. **Deploy this code first.** It reads both formats. Doing it the other way\n * round — setting the key before the readers understand the old ciphertexts\n * — is the outage this exists to prevent.\n * 2. Move the old value into `MFA_ENCRYPTION_KEY_PREVIOUS` and set the new one\n * as `MFA_ENCRYPTION_KEY`.\n * 3. Each factor is re-wrapped with the current key the next time its owner\n * authenticates (`openTotpSecret` reports it via `rewrapped`).\n * 4. Once every enrolled user has signed in, drop `MFA_ENCRYPTION_KEY_PREVIOUS`.\n * Any factor still on the old key then fails loudly, naming the key id,\n * rather than being silently unreadable.\n *\n * Trial decryption across candidates is safe here: GCM's auth tag is a 128-bit\n * MAC, so a wrong key throws instead of yielding a plausible-looking secret.\n *\n * @module\n */\n\nimport { createCipheriv, createDecipheriv, createHash, randomBytes } from \"crypto\";\nimport { logger } from \"../utils/logger\";\nimport { processJwtService, type JwtService } from \"./jwt\";\n\nconst ALGORITHM = \"aes-256-gcm\";\nconst IV_BYTES = 12;\nconst AUTH_TAG_LENGTH = 16;\n\n/** Ciphertext prefix marking the key-stamped format. */\nconst VERSION = \"v1\";\n\n/** A key the process could decrypt with, and the id a ciphertext names it by. */\ninterface KeyCandidate {\n /** First 8 hex of `sha256(raw)` — derived, so no key registry to maintain. */\n id: string;\n key: Buffer;\n /** Where it came from, for error messages. */\n source: string;\n}\n\n/** Warn once, not once per sign-in. */\nlet warnedAboutFallback = false;\n\nfunction toCandidate(raw: string, source: string): KeyCandidate {\n const digest = createHash(\"sha256\").update(raw).digest();\n return { id: digest.toString(\"hex\").slice(0, 8),\nkey: digest,\nsource };\n}\n\n/**\n * Every key this backend can decrypt with, most-preferred first.\n *\n * The head of the list is the key new ciphertexts are written with; the rest\n * exist so that ciphertexts written before a rotation still open. `jwt` is the\n * backend's, whose secret is the last fallback.\n */\nfunction keyCandidates(jwt: JwtService): KeyCandidate[] {\n const candidates: KeyCandidate[] = [];\n const seen = new Set<string>();\n\n const add = (raw: string | undefined, source: string) => {\n const trimmed = raw?.trim();\n if (!trimmed || seen.has(trimmed)) return;\n seen.add(trimmed);\n candidates.push(toCandidate(trimmed, source));\n };\n\n add(process.env.MFA_ENCRYPTION_KEY, \"MFA_ENCRYPTION_KEY\");\n for (const previous of (process.env.MFA_ENCRYPTION_KEY_PREVIOUS ?? \"\").split(\",\")) {\n add(previous, \"MFA_ENCRYPTION_KEY_PREVIOUS\");\n }\n add(process.env.JWT_SECRET, \"JWT_SECRET\");\n // The secret the server was actually given. An app that passes\n // `auth.jwtSecret` in code, not under that environment name, had no key\n // at all, and every `/mfa/enroll` answered 500 with the reason only in the\n // log. Last, so a deployment that sets both keeps reading what it wrote.\n add(jwt.hmacSecret(), \"auth.jwtSecret\");\n\n if (candidates.length === 0) {\n throw new Error(\n \"Cannot encrypt or decrypt TOTP secrets: none of MFA_ENCRYPTION_KEY, \" +\n \"MFA_ENCRYPTION_KEY_PREVIOUS, JWT_SECRET or auth.jwtSecret is configured.\"\n );\n }\n\n if (!process.env.MFA_ENCRYPTION_KEY && !warnedAboutFallback) {\n warnedAboutFallback = true;\n logger.warn(\n \"[MFA-Crypto] MFA_ENCRYPTION_KEY is not set — falling back to JWT_SECRET. \" +\n \"Set a dedicated MFA_ENCRYPTION_KEY for production; see the rotation \" +\n \"steps in mfa-crypto.ts before you do it on a live deployment.\"\n );\n }\n\n return candidates;\n}\n\n/** The key new ciphertexts are written with. */\nfunction currentKey(jwt: JwtService): KeyCandidate {\n return keyCandidates(jwt)[0];\n}\n\n/**\n * Encrypt a plaintext TOTP secret.\n *\n * @param plaintext - The Base32 TOTP secret to encrypt.\n * @param jwt - The backend whose JWT secret is the last key fallback. The\n * process default when omitted.\n * @returns A string in the format `v1.<keyId>:iv_hex:authTag_hex:ciphertext_hex`.\n */\nexport function encryptTotpSecret(plaintext: string, jwt: JwtService = processJwtService): string {\n const { id, key } = currentKey(jwt);\n const iv = randomBytes(IV_BYTES);\n\n const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });\n const encrypted = Buffer.concat([cipher.update(plaintext, \"utf8\"), cipher.final()]);\n const authTag = cipher.getAuthTag();\n\n return `${VERSION}.${id}:${iv.toString(\"hex\")}:${authTag.toString(\"hex\")}:${encrypted.toString(\"hex\")}`;\n}\n\nfunction openWith(candidate: KeyCandidate, ivHex: string, authTagHex: string, encryptedHex: string): string {\n const decipher = createDecipheriv(ALGORITHM, candidate.key, Buffer.from(ivHex, \"hex\"), {\n authTagLength: AUTH_TAG_LENGTH\n });\n decipher.setAuthTag(Buffer.from(authTagHex, \"hex\"));\n return Buffer.concat([decipher.update(Buffer.from(encryptedHex, \"hex\")), decipher.final()]).toString(\"utf8\");\n}\n\n/** What a factor's stored secret opened to, and whether it should be re-stored. */\nexport interface OpenedTotpSecret {\n /** The Base32 TOTP secret. */\n secret: string;\n /**\n * Set when the secret opened with something other than the current key —\n * the same secret, re-encrypted under it. Persist this and the factor\n * stops depending on the old key. `null` when it is already current.\n */\n rewrapped: string | null;\n}\n\n/**\n * Decrypt a stored TOTP secret, reporting whether it wants re-wrapping.\n *\n * Accepts both the key-stamped format and the original unstamped\n * `iv:authTag:ciphertext`, which is tried against each candidate in turn.\n * `jwt` is the backend whose JWT secret is the last key fallback — the process\n * default when omitted.\n */\nexport function openTotpSecret(ciphertext: string, jwt: JwtService = processJwtService): OpenedTotpSecret {\n const parts = ciphertext.split(\":\");\n const candidates = keyCandidates(jwt);\n const current = candidates[0];\n\n if (parts.length === 4) {\n const [stamp, ivHex, authTagHex, encryptedHex] = parts;\n const [version, keyId] = stamp.split(\".\");\n if (version !== VERSION || !keyId) {\n throw new Error(`Unsupported encrypted TOTP secret version \"${stamp}\"`);\n }\n\n const named = candidates.find(c => c.id === keyId);\n if (!named) {\n // Loud on purpose. An unknown key id means a key this deployment\n // once used is no longer configured — recoverable by restoring it\n // to MFA_ENCRYPTION_KEY_PREVIOUS, but only if someone is told.\n throw new Error(\n `No configured key matches this TOTP secret (key id \"${keyId}\"). ` +\n \"Restore the key that encrypted it via MFA_ENCRYPTION_KEY_PREVIOUS. \" +\n `Configured key ids: ${candidates.map(c => `${c.id} (${c.source})`).join(\", \")}.`\n );\n }\n\n const secret = openWith(named, ivHex, authTagHex, encryptedHex);\n return { secret,\nrewrapped: named.id === current.id ? null : encryptTotpSecret(secret, jwt) };\n }\n\n if (parts.length !== 3) {\n throw new Error(\n \"Invalid encrypted TOTP secret format — expected v1.<keyId>:iv:authTag:ciphertext \" +\n \"or the legacy iv:authTag:ciphertext\"\n );\n }\n\n // Unstamped, from before key ids existed: it was written with whatever the\n // environment resolved to at the time, which we can only discover by trying.\n const [ivHex, authTagHex, encryptedHex] = parts;\n for (const candidate of candidates) {\n try {\n const secret = openWith(candidate, ivHex, authTagHex, encryptedHex);\n // Always re-wrapped: even when the key is unchanged, stamping it\n // is what lets the next rotation identify this ciphertext.\n return { secret,\nrewrapped: encryptTotpSecret(secret, jwt) };\n } catch {\n // Wrong key — GCM refused to authenticate. Try the next one.\n }\n }\n\n throw new Error(\n \"Could not decrypt TOTP secret with any configured key. Tried: \" +\n `${candidates.map(c => c.source).join(\", \")}.`\n );\n}\n\n/**\n * Decrypt a previously encrypted TOTP secret.\n *\n * Prefer {@link openTotpSecret} on any path that can persist the result —\n * this wrapper discards the re-wrapped form, so a factor read through it stays\n * on whichever key it was written with.\n *\n * @param ciphertext - A stored TOTP secret, in either format.\n * @param jwt - The backend whose JWT secret is the last key fallback.\n * @returns The original Base32 TOTP secret.\n */\nexport function decryptTotpSecret(ciphertext: string, jwt: JwtService = processJwtService): string {\n return openTotpSecret(ciphertext, jwt).secret;\n}\n","import type { Context } from \"hono\";\nimport type { AuthResponsePayload } from \"@rebasepro/types\";\nimport type { HonoEnv } from \"../api/types\";\nimport type { CookieAuthConfig } from \"./routes\";\nimport { jwtOf, MAX_COOKIE_AGE_MS } from \"./jwt\";\n\n/**\n * Cookie auth helpers for httpOnly refresh tokens.\n */\nexport function getCookieSettings(config: CookieAuthConfig | undefined) {\n const COOKIE_NAME = config?.cookieName || \"__rb_refresh\";\n const COOKIE_PATH = config?.path || \"/\";\n const COOKIE_SAMESITE = config?.sameSite || \"Lax\";\n const COOKIE_DOMAIN = config?.domain;\n const COOKIE_SECURE = config?.secure;\n\n return {\n name: COOKIE_NAME,\n path: COOKIE_PATH,\n sameSite: COOKIE_SAMESITE,\n domain: COOKIE_DOMAIN,\n secure: COOKIE_SECURE\n };\n}\n\n/**\n * Set the refresh token as an httpOnly cookie on the response.\n */\nexport function setRefreshCookie(c: Context<HonoEnv>, refreshToken: string, config: CookieAuthConfig | undefined): void {\n if (!config) return;\n\n const settings = getCookieSettings(config);\n // Secure unless the deployment explicitly says otherwise.\n //\n // This used to be inferred from `c.req.url`, which `@hono/node-server`\n // derives from `socket.encrypted` — so behind any TLS-terminating proxy,\n // which is the normal production topology, the server saw `http` and\n // omitted the flag. The refresh token is a credential with a lifetime\n // measured in months, and it was travelling in cleartext on every request\n // to a plain-http URL on the same host.\n //\n // Inverted rather than patched: no request header can downgrade this, since\n // `X-Forwarded-Proto` is written by whoever is talking to us. Only\n // `cookieAuth.secure: false` turns it off, which is a deployment saying so\n // deliberately. Local development does not need that escape hatch —\n // browsers treat `http://localhost` as a trustworthy origin and accept\n // Secure cookies there.\n const isSecure = settings.secure ?? true;\n\n let cookie = `${settings.name}=${encodeURIComponent(refreshToken)}; Path=${settings.path}; HttpOnly; SameSite=${settings.sameSite}`;\n if (isSecure) cookie += \"; Secure\";\n if (settings.domain) cookie += `; Domain=${settings.domain}`;\n // Match the server's refresh token expiry rather than assuming it. This\n // was hardcoded to 30 days, which quietly capped every deployment's\n // sessions at 30 days no matter what JWT_REFRESH_EXPIRES_IN said: the row\n // in the database stayed valid for as long as it was configured to, and\n // the browser threw away the cookie that pointed at it. Re-set on every\n // rotation, so an active session's cookie slides forward with it.\n const maxAgeSeconds = Math.floor(Math.min(jwtOf(c).getRefreshTokenTtlMs(), MAX_COOKIE_AGE_MS) / 1000);\n cookie += `; Max-Age=${maxAgeSeconds}`;\n c.header(\"Set-Cookie\", cookie, { append: true });\n}\n\n/**\n * Clear the refresh token cookie.\n */\nexport function clearRefreshCookie(c: Context<HonoEnv>, config: CookieAuthConfig | undefined): void {\n if (!config) return;\n\n const settings = getCookieSettings(config);\n let cookie = `${settings.name}=; Path=${settings.path}; HttpOnly; SameSite=${settings.sameSite}; Max-Age=0`;\n // Mirror the attributes the cookie was set with, so the clear matches it.\n if (settings.secure ?? true) cookie += \"; Secure\";\n if (settings.domain) cookie += `; Domain=${settings.domain}`;\n c.header(\"Set-Cookie\", cookie, { append: true });\n}\n\n/**\n * Read the refresh token from the request — cookie first, then body fallback.\n *\n * An empty string is no token, wherever it comes from. In cookie mode every\n * response this server sends carries `refreshToken: \"\"` (see\n * `redactRefreshToken`), and a client that echoes it back — `@rebasepro/client`\n * did, on every refresh from a live tab — is presenting nothing. Taking the\n * body's `\"\"` as the token, or refusing it at the schema, kept the cookie\n * beside it from ever being read.\n */\nexport function readRefreshToken(c: Context<HonoEnv>, body: { refreshToken?: string }, config: CookieAuthConfig | undefined): string | undefined {\n if (config) {\n const settings = getCookieSettings(config);\n const cookieHeader = c.req.header(\"cookie\") || \"\";\n const prefix = `${settings.name}=`;\n const cookies = cookieHeader.split(\";\");\n for (const part of cookies) {\n const trimmed = part.trim();\n if (trimmed.startsWith(prefix)) {\n const fromCookie = decodeURIComponent(trimmed.slice(prefix.length));\n if (fromCookie) return fromCookie;\n }\n }\n }\n return body.refreshToken || undefined;\n}\n\n/**\n * Redact the refresh token from the response body when cookie mode is active.\n */\nexport function redactRefreshToken(\n response: AuthResponsePayload,\n c: Context<HonoEnv>,\n refreshToken: string,\n config: CookieAuthConfig | undefined\n): AuthResponsePayload {\n if (!config) return response;\n\n setRefreshCookie(c, refreshToken, config);\n return {\n ...response,\n tokens: {\n ...response.tokens,\n refreshToken: \"\" // Omit from JSON body — the cookie carries it\n }\n };\n}\n","import { Hono, type Context, type MiddlewareHandler } from \"hono\";\nimport { z } from \"zod\";\nimport { ApiError } from \"../api/errors\";\nimport { HonoEnv } from \"../api/types\";\nimport { extractBearerToken } from \"./middleware\";\nimport { logger } from \"../utils/logger\";\nimport { requestClientAddress, type AuthLimiters } from \"./rate-limiter\";\nimport {\n generateTotpSecret,\n verifyTotpCounter,\n base32Decode,\n generateRecoveryCodes,\n hashRecoveryCode\n} from \"./mfa\";\nimport { encryptTotpSecret, openTotpSecret } from \"./mfa-crypto\";\nimport type { AccessTokenPayload, JwtService } from \"./jwt\";\nimport type { AuthModuleConfig, CreateSessionAndTokens } from \"./routes\";\nimport type { AuthRepository } from \"./interfaces\";\nimport { redactRefreshToken } from \"./cookie-utils\";\nimport { judgeAccessToken } from \"./token-revocation\";\nimport { resolveAuthHooks } from \"./auth-hooks\";\nimport type { AuthResponsePayload, TransformAuthResponseContext } from \"@rebasepro/types\";\n\n/**\n * How many failed verifications one challenge tolerates before it is spent.\n *\n * A 6-digit code with a ±1 step window accepts 3 of 1,000,000 values, so the\n * only thing standing between an attacker and the second factor is how many\n * guesses a challenge will take. Five, and then they must open another one —\n * which the per-user limiter below is counting.\n */\nconst MAX_CHALLENGE_ATTEMPTS = 5;\n\n/** Verification attempts one *account* may make per window, across all IPs. */\nconst MFA_VERIFICATION_ATTEMPTS_PER_WINDOW = 10;\n\ninterface MfaRoutesConfig {\n router: Hono<HonoEnv>;\n config: AuthModuleConfig;\n /** The backend these routes sign and verify for; its secret is also the TOTP key's last fallback. */\n jwt: JwtService;\n /** The backend's auth limiters. */\n limiters: AuthLimiters;\n /**\n * The caller's guard for a signed-in user, with the revocation watermark\n * consulted. Enrolling, confirming or removing a factor is a persistence\n * move — the second one the 2026-09-02 audit named — so a token the user\n * has signed out everywhere must not reach it. `routes.ts` builds this\n * from the same repository the refresh path checks; the plain\n * `requireAuth` only verifies the signature.\n */\n requireLiveSession: MiddlewareHandler<HonoEnv>;\n ops: ReturnType<typeof resolveAuthHooks>;\n parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T;\n buildAuthResponse: (\n user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; emailVerified?: boolean; isAnonymous?: boolean; metadata?: Record<string, unknown> | null },\n roleIds: string[],\n accessToken: string,\n refreshToken: string,\n providerId: string\n ) => unknown;\n createSessionAndTokens: CreateSessionAndTokens;\n applyTransformHook?: (\n response: AuthResponsePayload,\n method: TransformAuthResponseContext[\"method\"],\n request: Request,\n uid: string\n ) => Promise<AuthResponsePayload>;\n}\n\n/**\n * Who is asking, on the two routes that may be reached *before* a session\n * exists.\n *\n * `pending` marks the caller as one factor short of a session: they hold the\n * short-lived, purpose-scoped token issued alongside `MFA_REQUIRED`, which is\n * refused as a credential everywhere else in the server.\n */\ninterface StepUpPrincipal {\n uid: string;\n aal: \"aal1\" | \"aal2\";\n pending: boolean;\n /** When the presented credential was issued, for the revocation check. */\n iat?: number;\n}\n\n/**\n * Resolve the caller of a challenge route from either a full session or a\n * pre-auth token.\n *\n * Both are parsed here rather than by `requireAuth` because the whole point of\n * the challenge pair is that it runs before the session exists — a login by an\n * MFA-enrolled user never receives an access token until it is finished.\n */\n/**\n * Open a factor's stored secret, re-storing it under the current key if it\n * opened with an older one.\n *\n * This is what makes an `MFA_ENCRYPTION_KEY` rotation complete on its own: each\n * factor moves to the new key the next time its owner authenticates, so the\n * previous key can eventually be dropped. See `mfa-crypto.ts` for the sequence.\n *\n * A failed re-wrap must never fail the verification — the code the user typed\n * was already checked against the right secret, and the only cost of not\n * persisting is that this factor is re-wrapped again next time.\n */\nasync function openAndRewrap(\n authRepo: AuthRepository,\n jwt: JwtService,\n factorId: string,\n secretEncrypted: string\n): Promise<string> {\n const { secret, rewrapped } = openTotpSecret(secretEncrypted, jwt);\n if (!rewrapped) return secret;\n\n // Optional on the interface: a repository that cannot re-store a secret\n // keeps working, it just leaves the factor on its original key.\n if (typeof authRepo.updateMfaFactorSecret !== \"function\") return secret;\n\n try {\n await authRepo.updateMfaFactorSecret(factorId, rewrapped);\n logger.info(\"[MFA-Crypto] Re-wrapped a TOTP secret under the current key\", { factorId });\n } catch (error) {\n logger.warn(\"[MFA-Crypto] Could not re-wrap a TOTP secret; leaving it on its original key\", {\n factorId,\n error\n });\n }\n return secret;\n}\n\nasync function resolveStepUpPrincipal(c: Context<HonoEnv>, jwt: JwtService): Promise<StepUpPrincipal | null> {\n // Respect a principal an upstream middleware already resolved, exactly as\n // `requireAuth` does.\n const existing = c.get(\"user\") as AccessTokenPayload | undefined;\n if (existing?.uid) {\n return { uid: existing.uid,\naal: existing.aal === \"aal2\" ? \"aal2\" : \"aal1\",\npending: false,\niat: existing.iat };\n }\n\n const token = extractBearerToken(c.req.header(\"authorization\"));\n if (token === undefined) return null;\n\n const session = await jwt.verifyAccessToken(token);\n if (session) {\n return { uid: session.uid,\naal: session.aal === \"aal2\" ? \"aal2\" : \"aal1\",\npending: false,\niat: session.iat };\n }\n\n const pending = await jwt.verifyMfaPendingToken(token);\n if (pending) {\n return { uid: pending.uid,\naal: \"aal1\",\npending: true,\niat: pending.iat };\n }\n\n return null;\n}\n\nexport function mountMfaRoutes(opts: MfaRoutesConfig): void {\n const { router, config, jwt, limiters, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook, requireLiveSession } = opts;\n const authRepo = config.authRepo;\n const emailConfig = config.emailConfig;\n\n /**\n * Per-account throttle on code verification.\n *\n * Keyed on the uid rather than the IP because an IP is the attacker's to\n * rotate and the account under attack is not: a distributed run against\n * one account passes an IP-keyed limiter untouched. The IP limiter still\n * runs in front of this one — they bound different things.\n */\n const mfaVerificationLimiter: MiddlewareHandler<HonoEnv> = limiters.named({\n name: \"auth-mfa-verify\",\n windowMs: 15 * 60 * 1000,\n limit: MFA_VERIFICATION_ATTEMPTS_PER_WINDOW,\n keyGenerator: async (c) => `mfa-verify:${(await resolveStepUpPrincipal(c as Context<HonoEnv>, jwt))?.uid ?? \"unidentified\"}`,\n message: \"Too many verification attempts for this account, please try again later.\"\n });\n\n /**\n * The caller of a challenge route, refused if the user has since ended\n * every session.\n *\n * These two routes resolve their own caller, because a sign-in that\n * answered `MFA_REQUIRED` has no session yet, and so they skipped the\n * revocation mark that `requireLiveSession` checks everywhere else. A\n * stolen access token that had enrolled its own factor (allowed at `aal1`\n * while the account had none) kept working here after the victim signed\n * out everywhere or changed their password, and the verify route then\n * minted it a new session dated after the mark, which the mark could not\n * reach. The pending token is a first factor accepted at its `iat`, so it\n * is held to the same mark.\n */\n async function resolveLiveStepUpPrincipal(c: Context<HonoEnv>): Promise<StepUpPrincipal> {\n const principal = await resolveStepUpPrincipal(c, jwt);\n if (!principal) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n // Revoked, or an account deleted since the first factor was accepted —\n // the same judgement every other door makes. See `judgeAccessToken`.\n if (!(await judgeAccessToken(authRepo, principal)).live) {\n throw ApiError.unauthorized(\"Session has been revoked\", \"SESSION_REVOKED\");\n }\n return principal;\n }\n\n /**\n * Changing which factors an account trusts is itself a privileged act.\n *\n * An account that already has a verified factor may only gain or confirm\n * another one from a session that presented the existing one. Without this\n * the `aal2` gate on unenroll was self-service: an `aal1` session enrolled\n * a factor of its own, verified it with a code it computed from the secret\n * the enrol response had just handed it, stepped up on that, and deleted\n * the victim's real factor — arriving at `aal2` without ever holding the\n * second factor the account was protected by.\n *\n * The first enrolment on an account with no verified factor is deliberately\n * allowed at `aal1`: there is no second factor to demand yet.\n */\n async function requireStepUpForFactorChange(userCtx: AccessTokenPayload): Promise<void> {\n if (userCtx.aal === \"aal2\") return;\n if (!(await authRepo.hasVerifiedMfaFactors(userCtx.uid))) return;\n throw ApiError.forbidden(\n \"This account already has a verified second factor. Re-authenticate with it before changing enrolled factors.\",\n \"AAL2_REQUIRED\"\n );\n }\n\n /**\n * Spend a TOTP time step, or refuse it as already spent.\n *\n * RFC 6238 §5.2: an accepted OTP must not be accepted again. The window\n * that exists for clock drift is also a 90-second replay window, and a\n * replay here is not momentary — `challenge/verify` mints a refresh token,\n * so one observed code buys a durable session.\n */\n async function claimTotpStep(factorId: string, counter: number): Promise<boolean> {\n if (typeof authRepo.claimMfaFactorCounter !== \"function\") {\n logger.warn(\"[MFA] Auth repository cannot record spent TOTP steps; an accepted code stays replayable for its window\", {\n factorId\n });\n return true;\n }\n return authRepo.claimMfaFactorCounter(factorId, counter);\n }\n\n /**\n * POST /auth/mfa/enroll\n * Start MFA enrollment: generate TOTP secret and recovery codes\n */\n router.post(\"/mfa/enroll\", limiters.strict, requireLiveSession, async (c) => {\n const userCtx = c.get(\"user\") as AccessTokenPayload | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n\n await requireStepUpForFactorChange(userCtx);\n\n const body = (await c.req.json().catch(() => ({}))) as Record<string, unknown>;\n const friendlyName = typeof body.friendlyName === \"string\" ? body.friendlyName : undefined;\n const issuer = typeof body.issuer === \"string\" ? body.issuer : emailConfig?.appName || \"Rebase\";\n\n // Get user for account name\n const user = await authRepo.getUserById(userCtx.uid);\n if (!user) {\n throw ApiError.notFound(\"User not found\");\n }\n\n // Generate TOTP secret\n const { secret, uri } = generateTotpSecret(issuer, user.email);\n\n // Encrypt the TOTP secret before storage (AES-256-GCM envelope encryption)\n const encryptedSecret = encryptTotpSecret(secret, jwt);\n const factor = await authRepo.createMfaFactor(\n user.id,\n \"totp\",\n encryptedSecret,\n friendlyName\n );\n\n // Recovery codes only with the account's first factor. Generating\n // them here on every call replaced the codes a user had printed as\n // soon as they *started* adding a second authenticator — before it\n // was verified, and for good if they closed the dialog — so the one\n // way back in after losing the phone stopped working. An account that\n // already has a verified factor keeps the codes it has; new ones come\n // from `POST /auth/mfa/recovery-codes`, at `aal2`.\n let codes: string[] | null = null;\n if (!(await authRepo.hasVerifiedMfaFactors(user.id))) {\n codes = generateRecoveryCodes(10);\n await authRepo.createRecoveryCodes(user.id, codes.map(hashRecoveryCode));\n }\n\n return c.json(\n {\n factor: {\n id: factor.id,\n factorType: factor.factorType,\n friendlyName: factor.friendlyName\n },\n totp: {\n secret,\n uri,\n qrUri: uri\n },\n recoveryCodes: codes\n },\n 201\n );\n });\n\n /**\n * POST /auth/mfa/recovery-codes\n * Replace the account's recovery codes with ten new ones, shown once.\n *\n * The deliberate way to get new codes — after using several, or after\n * losing the printout. Needs `aal2` once a factor is verified, as every\n * factor change does: whoever holds only the password must not be able to\n * swap the codes for ones they know.\n */\n router.post(\"/mfa/recovery-codes\", limiters.strict, requireLiveSession, async (c) => {\n const userCtx = c.get(\"user\") as AccessTokenPayload | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n if (!(await authRepo.hasVerifiedMfaFactors(userCtx.uid))) {\n throw ApiError.badRequest(\"This account has no verified second factor, so recovery codes would recover nothing.\", \"MFA_NOT_ENROLLED\");\n }\n await requireStepUpForFactorChange(userCtx);\n\n const codes = generateRecoveryCodes(10);\n await authRepo.createRecoveryCodes(userCtx.uid, codes.map(hashRecoveryCode));\n return c.json({ recoveryCodes: codes });\n });\n\n /**\n * POST /auth/mfa/verify\n * Verify TOTP code to complete MFA enrollment\n */\n router.post(\"/mfa/verify\", limiters.strict, requireLiveSession, mfaVerificationLimiter, async (c) => {\n const userCtx = c.get(\"user\") as AccessTokenPayload | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n\n await requireStepUpForFactorChange(userCtx);\n\n const verifySchema = z.object({\n factorId: z.string().min(1, \"Factor ID is required\"),\n code: z.string().length(6, \"Code must be 6 digits\")\n });\n const { factorId, code } = parseBody(verifySchema, await c.req.json());\n\n // Get the factor\n const factor = await authRepo.getMfaFactorById(factorId);\n if (!factor || factor.uid !== userCtx.uid) {\n throw ApiError.notFound(\"MFA factor not found\");\n }\n\n if (factor.verified) {\n throw ApiError.badRequest(\"Factor is already verified\", \"ALREADY_VERIFIED\");\n }\n\n // Decrypt and verify the TOTP code\n const decryptedSecret = await openAndRewrap(authRepo, jwt, factor.id, factor.secretEncrypted);\n const secretBuffer = base32Decode(decryptedSecret);\n const matchedCounter = verifyTotpCounter(secretBuffer, code);\n\n if (matchedCounter === null || !(await claimTotpStep(factor.id, matchedCounter))) {\n throw ApiError.unauthorized(\"Invalid TOTP code\", \"INVALID_CODE\");\n }\n\n // Mark factor as verified\n await authRepo.verifyMfaFactor(factorId);\n\n return c.json({\n success: true,\n message: \"MFA factor verified and enrolled\"\n });\n });\n\n /**\n * POST /auth/mfa/challenge\n * Open a challenge — either to finish a sign-in that answered\n * `MFA_REQUIRED`, or to step an existing session up to `aal2`.\n */\n router.post(\"/mfa/challenge\", limiters.strict, async (c) => {\n const principal = await resolveLiveStepUpPrincipal(c);\n\n const challengeSchema = z.object({\n factorId: z.string().min(1, \"Factor ID is required\")\n });\n const { factorId } = parseBody(challengeSchema, await c.req.json());\n\n // Verify the factor belongs to this user and is verified\n const factor = await authRepo.getMfaFactorById(factorId);\n if (!factor || factor.uid !== principal.uid) {\n throw ApiError.notFound(\"MFA factor not found\");\n }\n\n if (!factor.verified) {\n throw ApiError.badRequest(\"MFA factor is not yet verified\", \"FACTOR_NOT_VERIFIED\");\n }\n\n const ipAddress = requestClientAddress(c);\n const challenge = await authRepo.createMfaChallenge(factorId, ipAddress);\n\n return c.json({\n challengeId: challenge.id,\n factorId: challenge.factorId,\n expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString()\n });\n });\n\n /**\n * POST /auth/mfa/challenge/verify\n * Answer a challenge: this is where an MFA-enrolled account's session is\n * minted, at `aal2`.\n */\n router.post(\"/mfa/challenge/verify\", limiters.strict, mfaVerificationLimiter, async (c) => {\n const principal = await resolveLiveStepUpPrincipal(c);\n\n const challengeVerifySchema = z.object({\n challengeId: z.string().min(1, \"Challenge ID is required\"),\n code: z.string().min(1, \"Code is required\")\n });\n const { challengeId, code } = parseBody(challengeVerifySchema, await c.req.json());\n\n // Find the challenge\n const challenge = await authRepo.getMfaChallengeById(challengeId);\n if (!challenge) {\n throw ApiError.badRequest(\"Invalid or expired challenge\", \"INVALID_CHALLENGE\");\n }\n\n // Get the factor and verify ownership\n const factor = await authRepo.getMfaFactorById(challenge.factorId);\n if (!factor || factor.uid !== principal.uid) {\n throw ApiError.notFound(\"MFA factor not found\");\n }\n\n // A challenge that has already been guessed at its limit is spent, and\n // stays spent for its remaining lifetime — otherwise one open challenge\n // is an unlimited number of guesses at six digits.\n //\n // The attempt is claimed BEFORE the code is judged, and the claim's\n // own count decides. Reading `challenge.attempts` and recording a\n // failure afterwards is check-then-act: guesses sent in parallel —\n // the shape a brute force takes — all read the same count and were\n // all judged. The read stays as the cheap refusal for a challenge\n // that is already spent.\n const attempts = (challenge.attempts ?? 0) >= MAX_CHALLENGE_ATTEMPTS\n ? MAX_CHALLENGE_ATTEMPTS + 1\n : typeof authRepo.recordMfaChallengeAttempt === \"function\"\n ? await authRepo.recordMfaChallengeAttempt(challengeId)\n : undefined;\n if (attempts !== undefined && attempts > MAX_CHALLENGE_ATTEMPTS) {\n logger.warn(\"[Security Audit] MFA challenge attempt limit reached\", {\n eventType: \"auth.mfa.challenge.exhausted\",\n uid: principal.uid,\n challengeId\n });\n throw ApiError.unauthorized(\"Too many failed attempts for this challenge\", \"CHALLENGE_EXHAUSTED\");\n }\n\n // Try TOTP verification first (standard 6-digit codes)\n const decryptedSecret = await openAndRewrap(authRepo, jwt, factor.id, factor.secretEncrypted);\n const secretBuffer = base32Decode(decryptedSecret);\n const matchedCounter = verifyTotpCounter(secretBuffer, code);\n let isValid = matchedCounter !== null && await claimTotpStep(factor.id, matchedCounter);\n\n // Fall back to recovery code verification if TOTP didn't match\n if (!isValid && matchedCounter === null) {\n const codeHash = hashRecoveryCode(code);\n isValid = await authRepo.useRecoveryCode(principal.uid, codeHash);\n }\n\n if (!isValid) {\n logger.warn(\"[Security Audit] MFA verification failed\", {\n eventType: \"auth.mfa.verify.failure\",\n uid: principal.uid,\n challengeId,\n attempts\n });\n throw ApiError.unauthorized(\"Invalid verification code\", \"INVALID_CODE\");\n }\n\n // Mark challenge as verified\n await authRepo.verifyMfaChallenge(challengeId);\n\n // A fresh authentication opens a fresh session, dated now — which is\n // what lets a later revocation void it. The gate is skipped because\n // this route IS the gate: the second factor was just presented.\n const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(\n principal.uid,\n c.req.header(\"user-agent\") || \"unknown\",\n requestClientAddress(c),\n { skipMfaGate: true, aal: \"aal2\", method: \"mfa\" }\n );\n\n if (ops.onAuthenticated) {\n const account = await authRepo.getUserById(principal.uid);\n if (account) {\n ops.onAuthenticated(account, \"mfa\").catch((err: unknown) => {\n logger.error(\"[AuthHooks] onAuthenticated error\", { error: err instanceof Error ? err.message : err });\n });\n }\n }\n\n // Fire onMfaVerified hook\n if (ops.onMfaVerified) {\n ops.onMfaVerified(principal.uid, factor.id).catch((err) => {\n logger.error(\"[AuthHooks] onMfaVerified error\", {\n error: err instanceof Error ? err.message : err\n });\n });\n }\n\n logger.info(\"[Security Audit] MFA verification success\", {\n eventType: \"auth.mfa.verify.success\",\n uid: principal.uid,\n factorId: factor.id,\n completedSignIn: principal.pending\n });\n\n // The caller who arrived with a pre-auth token is completing a sign-in\n // and has no user object yet, so return the same payload a login would\n // have returned. Best-effort: a session was minted either way, and\n // failing the response over the enrichment would strand the caller.\n const user = await authRepo.getUserById(principal.uid).catch(() => null);\n let mfaResponse: AuthResponsePayload = user\n ? buildAuthResponse(user, roleIds, accessToken, refreshToken, \"mfa\") as AuthResponsePayload\n : {\n tokens: {\n accessToken,\n refreshToken,\n accessTokenExpiresAt: jwt.getAccessTokenExpiry()\n }\n };\n if (applyTransformHook) {\n mfaResponse = await applyTransformHook(mfaResponse, \"mfa\", c.req.raw, principal.uid);\n }\n return c.json(redactRefreshToken(mfaResponse, c, refreshToken, config.cookieAuth));\n });\n\n /**\n * GET /auth/mfa/factors\n * List enrolled MFA factors for the current user\n */\n router.get(\"/mfa/factors\", limiters.strict, requireLiveSession, async (c) => {\n const userCtx = c.get(\"user\") as { uid: string; roles?: string[] } | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n\n const factors = await authRepo.getMfaFactors(userCtx.uid);\n return c.json({\n factors: factors.map((f) => ({\n id: f.id,\n factorType: f.factorType,\n friendlyName: f.friendlyName,\n verified: f.verified,\n createdAt: f.createdAt\n }))\n });\n });\n\n /**\n * DELETE /auth/mfa/unenroll\n * Remove an MFA factor\n */\n router.delete(\"/mfa/unenroll\", limiters.strict, requireLiveSession, async (c) => {\n const userCtx = c.get(\"user\") as AccessTokenPayload | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n\n // The same gate as enroll and verify: aal2 once the account has a\n // verified factor. Before that no session can reach aal2, so an\n // unconditional aal2 check left an abandoned first enrolment\n // impossible to remove — and every factor removable at aal1 here is\n // necessarily unverified.\n await requireStepUpForFactorChange(userCtx);\n\n const unenrollSchema = z.object({\n factorId: z.string().min(1, \"Factor ID is required\")\n });\n const { factorId } = parseBody(unenrollSchema, await c.req.json());\n\n // Verify ownership\n const factor = await authRepo.getMfaFactorById(factorId);\n if (!factor || factor.uid !== userCtx.uid) {\n throw ApiError.notFound(\"MFA factor not found\");\n }\n\n await authRepo.deleteMfaFactor(factorId, userCtx.uid);\n\n // If no more verified factors, clean up recovery codes\n const hasFactors = await authRepo.hasVerifiedMfaFactors(userCtx.uid);\n if (!hasFactors) {\n await authRepo.deleteAllRecoveryCodes(userCtx.uid);\n }\n\n return c.json({\n success: true,\n message: \"MFA factor removed\"\n });\n });\n}\n","/**\n * The gate between \"first factor accepted\" and \"session issued\".\n *\n * MFA used to be modelled as an optional step-up that no resource required: a\n * user could enrol TOTP, print recovery codes, and change nothing about what a\n * stolen password bought, because every session-minting route issued a full\n * `aal1` token without ever asking whether the account had a second factor.\n * This module is the ask. It lives apart from the routes so that there is one\n * decision, made once, that `createSessionAndTokens` cannot be extended past —\n * a new sign-in route inherits the gate by construction rather than by the\n * author remembering it.\n *\n * @module\n */\n\nimport { ApiError } from \"../api/errors\";\nimport { logger } from \"../utils/logger\";\nimport type { JwtService } from \"./jwt\";\nimport type { AuthRepository } from \"./interfaces\";\n\n/** Error code a client watches for to switch a sign-in into its second step. */\nexport const MFA_REQUIRED_CODE = \"MFA_REQUIRED\";\n\n/** Shape carried in `error.details` of an {@link MFA_REQUIRED_CODE} response. */\nexport interface MfaRequiredDetails {\n /**\n * Short-lived, purpose-scoped credential proving the first factor was\n * accepted. It is refused as a session everywhere (`verifyAccessToken`\n * rejects purpose-scoped tokens); `POST /auth/mfa/challenge` and\n * `POST /auth/mfa/challenge/verify` are the only routes that accept it.\n */\n mfaToken: string;\n /** The verified factors the caller may answer the challenge with. */\n factors: Array<{ id: string; factorType: string; friendlyName?: string }>;\n}\n\n/**\n * Refuse to mint a session for an account whose second factor has not been\n * presented, handing back what the client needs to present it.\n *\n * A 401 rather than a 200 with a different body: a client that has not been\n * taught about MFA must fail, not proceed. The credential in `details` is\n * useless for anything except the challenge routes, and is signed by `jwt` —\n * the backend's own key.\n */\nexport async function assertMfaSatisfied(authRepo: AuthRepository, uid: string, jwt: JwtService): Promise<void> {\n if (typeof authRepo.hasVerifiedMfaFactors !== \"function\") {\n // A custom repository from before MFA existed. It cannot hold a factor\n // either, so there is nothing to enforce — but say so once, because a\n // repository that *does* have factors and no way to report them would\n // be a silent bypass.\n logger.warn(\"[MFA] Auth repository cannot report enrolled factors; the login step-up gate is inert on this backend\");\n return;\n }\n\n if (!(await authRepo.hasVerifiedMfaFactors(uid))) return;\n\n const factors = (await authRepo.getMfaFactors(uid)).filter(f => f.verified);\n const details: MfaRequiredDetails = {\n mfaToken: await jwt.generateMfaPendingToken(uid),\n factors: factors.map(f => ({ id: f.id,\nfactorType: f.factorType,\nfriendlyName: f.friendlyName }))\n };\n\n logger.info(\"[Security Audit] MFA step-up required\", {\n eventType: \"auth.mfa.required\",\n uid\n });\n\n throw new ApiError(\n 401,\n MFA_REQUIRED_CODE,\n \"Multi-factor authentication is required to complete sign-in.\",\n details,\n // Routine: every sign-in by an enrolled user takes this path.\n true\n );\n}\n","import type { AuthRepository } from \"./interfaces\";\n\n/**\n * Whether a registration may proceed — computed in exactly one place.\n *\n * This predicate had drifted across three independent implementations, which is\n * the shape the bug always takes: one endpoint *advertises* that registration is\n * open, another *enforces* that it is closed, and the user lands on a form that\n * can only ever 403.\n *\n * 1. `POST /auth/register` (routes.ts) — enforces.\n * 2. `GET /auth/config` (session-routes.ts) — advertises.\n * 3. `getCapabilities()` (builtin-auth-adapter.ts) — also advertises, and is\n * the one that actually answers `GET /auth/config` on a real backend:\n * `init.ts` registers that path directly *before* mounting the auth router,\n * so Hono resolves it first and the session-routes copy never runs for it.\n *\n * Two of the three had already been fixed for the empty-database case when the\n * third was still returning `allowRegistration || needsSetup` — missing the kill\n * switch entirely. The fix is not to correct the third copy but to delete all\n * three: every caller now goes through this function, so the next term added\n * here reaches every surface at once.\n *\n * The rule itself:\n *\n * - `disableSelfRegistration` is absolute. It blocks even the first user, for\n * operators who provision accounts out of band and never want a public\n * first-come-first-admin window.\n * - Otherwise an **empty** user table always admits the first registration,\n * which auto-promotes to admin. Without this a backend deployed with\n * `allowRegistration: false` is a dead end: `POST /admin/bootstrap` needs an\n * authenticated caller, and an empty database cannot produce one.\n * - Otherwise `allowRegistration` decides, as it always did.\n */\nexport interface RegistrationPolicy {\n /** The hard kill switch. Blocks registration including first-user bootstrap. */\n disableSelfRegistration?: boolean;\n /** Steady-state self-registration, once at least one user exists. */\n allowRegistration?: boolean;\n /**\n * True when the user table is empty — the bootstrap window.\n *\n * Callers that already know this (the config endpoints compute it to report\n * `needsSetup`) pass it directly. `POST /auth/register` must not: it serves\n * anonymous callers, so it checks {@link isSteadyStateRegistrationOpen}\n * first and only pays for the count when that says no.\n */\n needsSetup: boolean;\n /**\n * Whether an empty table may still be claimed by whoever registers first.\n * Defaults to {@link isBootstrapWindowOpen}; tests pass it explicitly.\n */\n bootstrapWindowOpen?: boolean;\n}\n\n/**\n * Whether the first-come-first-admin window exists at all.\n *\n * On a laptop it should: the person at the keyboard is the operator, and a\n * fresh database that nobody can administer is a dead end. On anything with a\n * public hostname it must not: the shipped artifacts bring DNS and TLS up\n * before the operator has typed anything, `GET /auth/config` advertises\n * `needsSetup`, and whoever reaches `POST /auth/register` first owns the\n * deployment. So the window is open outside production and closed inside it,\n * where the operator names the first admin with `REBASE_ADMIN_EMAIL` /\n * `REBASE_ADMIN_PASSWORD` (see `seed-admin.ts`) or assigns the role with the\n * service key — two ways in, neither of which is a race.\n */\nexport function isBootstrapWindowOpen(env: { NODE_ENV?: string } = process.env): boolean {\n return env.NODE_ENV !== \"production\";\n}\n\ntype UserPresenceRepository = Pick<AuthRepository, \"firstUserIds\" | \"listUsersPaginated\">;\n\n/**\n * Whether the users table is empty — the question behind the bootstrap window.\n *\n * Through `AuthRepository.firstUserIds` when the repository has it: one\n * unordered `LIMIT 1` read. Otherwise through `listUsersPaginated`, which also\n * counts and sorts the whole table, so it is only the fallback.\n */\nexport async function isUserTableEmpty(repo: UserPresenceRepository): Promise<boolean> {\n if (repo.firstUserIds) return (await repo.firstUserIds(1)).length === 0;\n const { total } = await repo.listUsersPaginated({ limit: 1 });\n return total === 0;\n}\n\n/**\n * Whether the account just created is the only one there is. Two ids at most:\n * whether anyone else exists, and, when nobody does, whether it is this one.\n * Same fallback as {@link isUserTableEmpty}.\n */\nexport async function isOnlyUser(repo: UserPresenceRepository, uid: string): Promise<boolean> {\n if (repo.firstUserIds) {\n const ids = await repo.firstUserIds(2);\n return ids.length === 1 && ids[0] === uid;\n }\n const { users, total } = await repo.listUsersPaginated({ limit: 2 });\n return total === 1 && users[0]?.id === uid;\n}\n\n/**\n * What to tell a caller who hit an empty production database with the window\n * shut. Named here so the register route and the bootstrap route say the same\n * thing.\n */\nexport const SETUP_REQUIRED_MESSAGE =\n \"This deployment has no administrator yet, and in production the first account is not \" +\n \"promoted automatically. Set REBASE_ADMIN_EMAIL and REBASE_ADMIN_PASSWORD before the first \" +\n \"boot, or assign the admin role with the service key.\";\n\n/** The full predicate, for callers that already know whether setup is needed. */\nexport function isRegistrationOpen(policy: RegistrationPolicy): boolean {\n if (policy.disableSelfRegistration) return false;\n const window = policy.needsSetup && (policy.bootstrapWindowOpen ?? isBootstrapWindowOpen());\n return window || !!policy.allowRegistration;\n}\n\n/**\n * The same predicate with the bootstrap window assumed closed.\n *\n * Exists so `POST /auth/register` can reject the common case without counting\n * rows. A `false` here does **not** mean \"refuse\" — it means \"the answer depends\n * on whether the table is empty, so now go and look\".\n */\nexport function isSteadyStateRegistrationOpen(\n policy: Omit<RegistrationPolicy, \"needsSetup\">\n): boolean {\n return isRegistrationOpen({ ...policy, needsSetup: false });\n}\n\n/**\n * Whether `POST /auth/anonymous` may mint a user.\n *\n * Anonymous sign-in *is* registration: it inserts a `users` row and assigns\n * `defaultRole` exactly as `POST /auth/register` does. It simply never asked.\n * Both anonymous routes were mounted unconditionally and consulted none of the\n * gates above, so a backend with `allowRegistration: false` **and**\n * `disableSelfRegistration: true` — the documented hard kill switch — still\n * handed a permanent account to any caller in two unauthenticated requests:\n * `POST /auth/anonymous` for the row and the session, then\n * `POST /auth/anonymous/link` to put an email and password on it.\n *\n * Two terms, because the two questions are genuinely different:\n *\n * - `allowAnonymous` is the feature switch, and it is **opt-in**. There was no\n * key at all before, so a deployment could not turn anonymous auth off even\n * after finding it on; defaulting it to `true` here would keep that hole\n * open for everyone who never learns the key exists.\n * - `disableSelfRegistration` still wins over it. Its docstring calls it a\n * hard kill switch that blocks \"self-registration outright\", and an account\n * created without credentials is still an account created by the public.\n * An operator who sets both gets the kill switch they asked for.\n *\n * `allowRegistration` deliberately does **not** gate this. A public read-mostly\n * app that wants anonymous sessions but no sign-up forms is a real deployment,\n * and `allowAnonymous: true` says so precisely.\n */\nexport function isAnonymousAuthOpen(policy: {\n allowAnonymous?: boolean;\n disableSelfRegistration?: boolean;\n}): boolean {\n if (policy.disableSelfRegistration) return false;\n return !!policy.allowAnonymous;\n}\n","/**\n * What happens when the owner of an address proves it for the first time.\n *\n * Nothing verifies the address an account is made with: `POST /auth/register`\n * takes whatever it is given, and so does a sign-in through a provider that\n * does not vouch for the address (Spotify and Facebook never do; Discord and\n * multi-tenant Microsoft sometimes do not). So anyone can make an account for\n * someone else's address and leave a way in on it — a password, or the\n * provider identity they signed up with — then wait for the owner.\n *\n * The owner turns up by proving the address: a magic link, an email code or a\n * password reset, each read out of their inbox. That proof used to mark the\n * account verified and leave the attacker's way in where it was, so the owner\n * and the attacker shared the account from then on. Now the proof also removes\n * everything on the account that nobody proved: the password, every identity\n * whose provider did not vouch for this address, and every session.\n *\n * @module\n */\n\nimport { normalizeEmail } from \"@rebasepro/common\";\nimport { ApiError } from \"../api/errors\";\nimport { logger } from \"../utils/logger\";\nimport type { AuthRepository, OAuthProviderProfile, UserData, UserIdentityData } from \"./interfaces\";\nimport { replaceUserPassword } from \"./token-revocation\";\n\n/**\n * The profile stored with a linked OAuth identity.\n *\n * `emailVerified` is recorded so that a later proof of the address can tell an\n * identity whose provider vouched for it from one whose provider did not.\n */\nexport function identityProfileData(profile: OAuthProviderProfile): Record<string, unknown> {\n return { email: profile.email, emailVerified: profile.emailVerified === true };\n}\n\n/**\n * Did this identity's provider vouch for `email` when it was linked?\n *\n * Only whoever controls the inbox can hold a provider account that vouches for\n * the address, so such an identity is the owner's. An identity linked before\n * the flag was recorded reads as not vouching: it is detached, and if its\n * provider does vouch, the next sign-in with it links it again.\n */\nexport function identityVouchesForAddress(identity: UserIdentityData, email: string): boolean {\n const profile = identity.profileData;\n return profile?.emailVerified === true\n && typeof profile.email === \"string\"\n && normalizeEmail(profile.email) === normalizeEmail(email);\n}\n\n/**\n * Settle an account whose address its owner has just proven, and mark it\n * verified.\n *\n * Call it only for an account that is not yet verified, from a route that has\n * just consumed something mailed to the address. It detaches every identity\n * whose provider did not vouch for the address, sets the password to\n * `passwordHash` — the one the owner just chose on a reset, or `null` to remove\n * the one somebody else may have registered with — and ends every session, in\n * that order and before the account is marked verified.\n *\n * A repository that cannot detach an identity makes this refuse with 409 when\n * there is one to detach. Marking the account verified with the identity still\n * on it is the takeover itself, and leaving the account unverified while\n * signing the owner in would share the account all the same.\n */\nexport async function confirmAddressOwnership(\n authRepo: AuthRepository,\n user: Pick<UserData, \"id\" | \"email\" | \"passwordHash\">,\n passwordHash: string | null,\n options?: {\n /**\n * Whoever proved the address also holds a live session of this very\n * account, so nothing on it is a stranger's: it was all put there by\n * the person now holding the inbox. Nothing is removed and no session\n * ends; the account is only marked verified. See `GET /auth/verify-email`.\n */\n everythingProven?: boolean;\n }\n): Promise<AddressProofOutcome> {\n if (options?.everythingProven) {\n await authRepo.setEmailVerified(user.id, true);\n return { removedPassword: false, removedProviders: [] };\n }\n\n const unproven = (await authRepo.getUserIdentities(user.id))\n .filter(identity => !identityVouchesForAddress(identity, user.email));\n\n if (unproven.length > 0) {\n if (typeof authRepo.unlinkUserIdentity !== \"function\") {\n logger.error(\"[Security Audit] Proof of address refused: the account has sign-in methods nobody proved, and this auth repository cannot remove them\", {\n eventType: \"auth.address_proof.refused\",\n uid: user.id,\n providers: unproven.map(identity => identity.provider)\n });\n throw ApiError.conflict(\n \"This account has sign-in methods that were added before its email address was verified, \" +\n \"and this backend cannot remove them. Ask an administrator to review the account.\",\n \"UNVERIFIED_IDENTITIES\"\n );\n }\n for (const identity of unproven) {\n await authRepo.unlinkUserIdentity(user.id, identity.provider, identity.providerId);\n }\n }\n\n await replaceUserPassword(authRepo, user.id, passwordHash);\n await authRepo.setEmailVerified(user.id, true);\n\n const outcome: AddressProofOutcome = {\n // Removed means a password was there and is not any more; keeping the\n // same hash (the owner proved it) or setting a new one is not removal.\n removedPassword: Boolean(user.passwordHash) && passwordHash === null,\n removedProviders: unproven.map(identity => identity.provider)\n };\n if (unproven.length > 0 || user.passwordHash) {\n logger.info(\"[Security Audit] First proof of address removed sign-in methods nobody proved\", {\n eventType: \"auth.address_proof.cleared\",\n uid: user.id,\n removedProviders: outcome.removedProviders,\n replacedPassword: Boolean(user.passwordHash) && passwordHash !== user.passwordHash\n });\n }\n return outcome;\n}\n\n/** What a first proof of address took off the account. */\nexport interface AddressProofOutcome {\n /** A password was on the account, nobody proved it, and it is gone. */\n removedPassword: boolean;\n /** The providers whose identities were detached for not vouching for the address. */\n removedProviders: string[];\n}\n","/**\n * Addresses no mail can reach: the synthetic ones a guest and an X (Twitter)\n * account are given, because `email` is NOT NULL. Nothing is mailed to them,\n * and no account may move onto one.\n */\nexport function isDeliverableAddress(email: string): boolean {\n const domain = email.slice(email.lastIndexOf(\"@\") + 1).toLowerCase();\n return domain !== \"anonymous.local\" && domain !== \"twitter.placeholder.rebase\";\n}\n","/**\n * A signed-in user changes their own email address.\n *\n * `POST /auth/change-email { newEmail }` records the change and mails a link to\n * the new address, and a notice without a link to the old one.\n * `POST /auth/confirm-email-change { token }` — the link's landing screen calls\n * it — moves the account onto the new address.\n *\n * The address moves only when its inbox answers, so the account is never\n * pointed at an address nobody proved, and nothing is reserved meanwhile: a\n * pending change that held the address would let any account keep a stranger\n * from signing up with their own address. Whoever has the address when the link\n * is followed keeps it, and the link answers 409.\n *\n * On confirmation the address is the account's, verified. The identities whose\n * provider vouched for the old address are detached: the old address may be\n * the reason for the change — a job left, an inbox lost — and whoever controls\n * it now would otherwise sign in through that provider. The sessions stay: the\n * change was asked for from one, and the link proves the new inbox, not\n * anything about the others.\n *\n * @module\n */\n\nimport { Hono } from \"hono\";\nimport { z } from \"zod\";\nimport { normalizeEmail } from \"@rebasepro/common\";\nimport { ApiError } from \"../api/errors\";\nimport type { HonoEnv } from \"../api/types\";\nimport type { MiddlewareHandler } from \"hono\";\nimport { logger } from \"../utils/logger\";\nimport { getEmailChangeNoticeTemplate, getEmailChangeTemplate, resolveEmailBranding } from \"../email/templates\";\nimport { resolveEmailLinkBase } from \"../email/link-base\";\nimport { generateSecureToken, hashToken } from \"./admin-user-ops\";\nimport { identityVouchesForAddress } from \"./address-ownership\";\nimport type { resolveAuthHooks } from \"./auth-hooks\";\nimport type { AccessTokenPayload } from \"./jwt\";\nimport type { AuthLimiters } from \"./rate-limiter\";\nimport { isDeliverableAddress } from \"./deliverable-address\";\nimport type { AuthModuleConfig } from \"./routes\";\n\n/** How long an address-change link stays usable. */\nexport const EMAIL_CHANGE_TTL_MS = 24 * 60 * 60 * 1000;\n\ninterface EmailChangeRoutesConfig {\n router: Hono<HonoEnv>;\n config: AuthModuleConfig;\n ops: ReturnType<typeof resolveAuthHooks>;\n parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T;\n /** A signed-in user, with the revocation watermark consulted. */\n requireLiveSession: MiddlewareHandler<HonoEnv>;\n /** The backend's auth limiters. */\n limiters: AuthLimiters;\n}\n\nexport function mountEmailChangeRoutes(opts: EmailChangeRoutesConfig): void {\n const { router, config, ops, parseBody, requireLiveSession, limiters } = opts;\n const authRepo = config.authRepo;\n const { emailService, emailConfig } = config;\n\n const changeSchema = z.object({\n newEmail: z.string().email(\"Invalid email address\").max(255)\n });\n const confirmSchema = z.object({\n token: z.string().min(1, \"Token is required\")\n });\n\n /** The repository's address-change methods, or a 501 saying it has none. */\n function emailChangeStore() {\n const { setPendingEmailChange, findPendingEmailChange, applyPendingEmailChange } = authRepo;\n if (!setPendingEmailChange || !findPendingEmailChange || !applyPendingEmailChange) {\n throw new ApiError(501, \"NOT_SUPPORTED\", \"This backend's auth repository cannot change an account's email address.\");\n }\n return {\n setPendingEmailChange: setPendingEmailChange.bind(authRepo),\n findPendingEmailChange: findPendingEmailChange.bind(authRepo),\n applyPendingEmailChange: applyPendingEmailChange.bind(authRepo)\n };\n }\n\n /**\n * POST /auth/change-email { newEmail }\n *\n * Two limiters, as on `/send-verification`: per address of the caller's\n * machine, and per account, which is what bounds the mail one account can\n * have sent to addresses of its choosing.\n */\n router.post(\"/change-email\", limiters.strict, requireLiveSession, limiters.verificationEmail, async (c) => {\n const userCtx = c.get(\"user\") as AccessTokenPayload | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n const { newEmail } = parseBody(changeSchema, await c.req.json());\n const store = emailChangeStore();\n if (!emailService?.isConfigured()) {\n throw ApiError.serviceUnavailable(\"Email service not configured. The confirmation link cannot be sent.\", \"EMAIL_NOT_CONFIGURED\");\n }\n\n const user = await authRepo.getUserById(userCtx.uid);\n if (!user) {\n throw ApiError.notFound(\"User not found\");\n }\n if (user.isAnonymous) {\n throw ApiError.forbidden(\"A guest has no address to change. Make it an account first (POST /auth/anonymous/link).\", \"ANONYMOUS_USER\");\n }\n // Moving the address moves where every reset link and magic link\n // goes, so it is held to what changing a factor is held to.\n if (userCtx.aal !== \"aal2\" && await authRepo.hasVerifiedMfaFactors(user.id)) {\n throw ApiError.forbidden(\n \"This account has a second factor. Sign in with it before changing the email address.\",\n \"AAL2_REQUIRED\"\n );\n }\n\n const email = normalizeEmail(newEmail);\n if (email === normalizeEmail(user.email)) {\n throw ApiError.badRequest(\"That is already this account's email address.\", \"EMAIL_UNCHANGED\");\n }\n if (!isDeliverableAddress(email)) {\n throw ApiError.conflict(\"No mail reaches that address.\", \"UNDELIVERABLE_ADDRESS\");\n }\n if (await authRepo.getUserByEmail(email)) {\n throw ApiError.conflict(\"Email already registered\", \"EMAIL_EXISTS\");\n }\n if (ops.beforeEmailChange) {\n await ops.beforeEmailChange(user, email);\n }\n\n const token = generateSecureToken();\n await store.setPendingEmailChange(user.id, { email, tokenHash: hashToken(token) });\n\n const confirmUrl = `${resolveEmailLinkBase(emailConfig, \"verifyEmail\")}/confirm-email-change?token=${token}`;\n const { appName, logoUrl } = resolveEmailBranding(emailConfig);\n const recipient = { email: user.email, displayName: user.displayName };\n const confirmation = emailConfig?.templates?.emailChange\n ? emailConfig.templates.emailChange(confirmUrl, recipient, email)\n : getEmailChangeTemplate(confirmUrl, recipient, email, appName, logoUrl);\n // Awaited: the caller asked for this mail, and is told when it could\n // not be sent rather than left waiting for one that will not come.\n await emailService.send({ to: email, subject: confirmation.subject, html: confirmation.html, text: confirmation.text });\n\n // The old address hears of it, without a link. Not awaited: a notice\n // that fails does not undo the request.\n if (isDeliverableAddress(user.email)) {\n const notice = emailConfig?.templates?.emailChangeNotice\n ? emailConfig.templates.emailChangeNotice(recipient, email)\n : getEmailChangeNoticeTemplate(recipient, email, appName, logoUrl);\n emailService.send({ to: user.email, subject: notice.subject, html: notice.html, text: notice.text }).catch((err: unknown) => {\n logger.error(\"Failed to send the email change notice\", { error: err instanceof Error ? err.message : err });\n });\n }\n\n logger.info(\"[Security Audit] Email change requested\", {\n eventType: \"auth.email_change.requested\",\n uid: user.id\n });\n return c.json({\n success: true,\n pendingEmail: email,\n expiresAt: new Date(Date.now() + EMAIL_CHANGE_TTL_MS).toISOString()\n });\n });\n\n /**\n * POST /auth/confirm-email-change { token }\n *\n * No session: the link is often opened on another device, and it proves\n * the new inbox, which is all this needs.\n */\n router.post(\"/confirm-email-change\", limiters.strict, async (c) => {\n const { token } = parseBody(confirmSchema, await c.req.json());\n const store = emailChangeStore();\n const tokenHash = hashToken(token);\n const invalid = () => ApiError.badRequest(\"Invalid or expired email change link\", \"INVALID_TOKEN\");\n\n const found = await store.findPendingEmailChange(tokenHash);\n if (!found || Date.now() - found.change.sentAt.getTime() > EMAIL_CHANGE_TTL_MS) {\n throw invalid();\n }\n const { user } = found;\n const previousEmail = user.email;\n\n // Settled before the address moves, so a backend that cannot detach\n // them refuses with nothing changed rather than moving the address\n // and leaving the old address's way in on the account.\n const vouchingForOld = (await authRepo.getUserIdentities(user.id))\n .filter(identity => identityVouchesForAddress(identity, previousEmail));\n if (vouchingForOld.length > 0 && typeof authRepo.unlinkUserIdentity !== \"function\") {\n throw ApiError.conflict(\n \"This account signs in through a provider that vouched for its current address, and this backend \" +\n \"cannot detach it. Ask an administrator to change the address.\",\n \"UNVERIFIED_IDENTITIES\"\n );\n }\n\n // One write, held to the token: a second click, or a change replaced\n // since, finds nothing to apply. The unique index decides an address\n // somebody took meanwhile, as a 409.\n const updated = await store.applyPendingEmailChange(user.id, tokenHash);\n if (!updated) {\n throw invalid();\n }\n\n for (const identity of vouchingForOld) {\n await authRepo.unlinkUserIdentity?.(user.id, identity.provider, identity.providerId);\n }\n // A reset link still in the old inbox would set the password for the\n // rest of its hour.\n await authRepo.deleteAllPasswordResetTokensForUser(user.id);\n\n logger.info(\"[Security Audit] Email address changed\", {\n eventType: \"auth.email_change.confirmed\",\n uid: user.id,\n detachedProviders: vouchingForOld.map(identity => identity.provider)\n });\n return c.json({\n success: true,\n user: { uid: updated.id, email: updated.email, emailVerified: true },\n removedProviders: vouchingForOld.map(identity => identity.provider)\n });\n });\n}\n","import { Hono } from \"hono\";\nimport { normalizeEmail } from \"@rebasepro/common\";\nimport { z } from \"zod\";\nimport { randomBytes } from \"crypto\";\nimport { ApiError } from \"../api/errors\";\nimport { HonoEnv } from \"../api/types\";\nimport type { MiddlewareHandler } from \"hono\";\nimport { extractBearerToken } from \"./bearer-token\";\nimport { logger } from \"../utils/logger\";\nimport { requestClientAddress, type AuthLimiters } from \"./rate-limiter\";\nimport { hashRefreshToken, type JwtService } from \"./jwt\";\nimport type { AuthModuleConfig, CreateSessionAndTokens } from \"./routes\";\nimport type { AuthResponsePayload, TransformAuthResponseContext } from \"@rebasepro/types\";\nimport { readRefreshToken, clearRefreshCookie, redactRefreshToken } from \"./cookie-utils\";\nimport { isAnonymousAuthOpen, isSteadyStateRegistrationOpen } from \"./registration-policy\";\nimport { revokeAllSessions } from \"./token-revocation\";\nimport type { resolveAuthHooks } from \"./auth-hooks\";\nimport type { CreateUserData } from \"./interfaces\";\nimport { EMAIL_CHANGE_TTL_MS } from \"./email-change-routes\";\n\ninterface SessionRoutesConfig {\n router: Hono<HonoEnv>;\n /** The backend these routes sign and verify for. */\n jwt: JwtService;\n /** The backend's auth limiters. */\n limiters: AuthLimiters;\n config: AuthModuleConfig;\n /**\n * The caller's guard for a signed-in user, with the revocation watermark\n * consulted. Listing and revoking sessions, `/me`, user lookup and linking\n * an anonymous account all describe or change the account, and a token the\n * user has signed out everywhere must not reach them. `routes.ts` builds\n * this from the same repository the refresh path checks; the plain\n * `requireAuth` only verifies the signature.\n */\n requireLiveSession: MiddlewareHandler<HonoEnv>;\n ops: ReturnType<typeof resolveAuthHooks>;\n parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T;\n buildAuthResponse: (\n user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; emailVerified?: boolean; isAnonymous?: boolean; metadata?: Record<string, unknown> | null },\n roleIds: string[],\n accessToken: string,\n refreshToken: string,\n providerId: string\n ) => unknown;\n createSessionAndTokens: CreateSessionAndTokens;\n applyTransformHook: (\n response: AuthResponsePayload,\n method: TransformAuthResponseContext[\"method\"],\n request: Request,\n uid: string\n ) => Promise<AuthResponsePayload>;\n /**\n * The `register` captcha, built by the caller so a misconfiguration fails\n * the boot once. `/anonymous/link` is registration — it is where a guest\n * becomes an account — so it is guarded by the same challenge as\n * `/register`. Absent when captcha is off or `register` is not protected.\n */\n registerCaptcha?: MiddlewareHandler<HonoEnv>;\n /**\n * Mail the account a verification link, in the background. Upgrading a\n * guest is registration, and registration starts the address proof.\n */\n sendVerificationMail?: (user: { id: string; email: string; displayName?: string | null }) => void;\n}\n\nexport function mountSessionRoutes(opts: SessionRoutesConfig): void {\n const { router, config, jwt, limiters, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook, requireLiveSession, registerCaptcha, sendVerificationMail } = opts;\n const authRepo = config.authRepo;\n\n /**\n * Refuse the anonymous routes unless the deployment opted in.\n *\n * 403 rather than 404: the route exists, and a client that was told\n * `anonymousLogin: true` by `GET /auth/config` and then sees a 404 has no\n * way to tell a disabled feature from a version skew. The error names the\n * key, because the failure this closes was an operator believing they had\n * already switched registration off.\n */\n function assertAnonymousAuthOpen(): void {\n if (isAnonymousAuthOpen({\n allowAnonymous: config.allowAnonymous,\n disableSelfRegistration: config.disableSelfRegistration\n })) return;\n throw ApiError.forbidden(\n config.disableSelfRegistration\n ? \"Anonymous sign-in is disabled: disableSelfRegistration blocks it.\"\n : \"Anonymous sign-in is disabled. Set `auth.allowAnonymous: true` to enable it.\",\n \"ANONYMOUS_AUTH_DISABLED\"\n );\n }\n\n /**\n * How the caller's session was signed in, as `providerId` reports it.\n *\n * Read off the session's refresh tokens, which carry it from sign-in\n * through every rotation, found by the access token's `sid`. A token from\n * before `sid`, or a session whose rows say nothing, reads `\"password\"` —\n * what `/me` always answered.\n */\n async function sessionMethod(userCtx: { uid: string; sid?: string }): Promise<string> {\n if (!userCtx.sid) return \"password\";\n const tokens = await authRepo.listRefreshTokensForUser(userCtx.uid).catch(() => []);\n const own = tokens.find(token => (token.sessionId ?? token.id) === userCtx.sid && token.method);\n return own?.method ?? \"password\";\n }\n\n /**\n * The address the account is moving to, while its link is still live —\n * so a settings screen opened later still says a change is waiting.\n * `null` otherwise, and on a repository that cannot change addresses.\n */\n async function pendingEmail(uid: string): Promise<string | null> {\n const change = await authRepo.getPendingEmailChange?.(uid).catch(() => null);\n if (!change || Date.now() - change.sentAt.getTime() > EMAIL_CHANGE_TTL_MS) return null;\n return change.email;\n }\n\n const logoutSchema = z.object({\n refreshToken: z.string().optional()\n });\n\n const linkSchema = z.object({\n email: z.string().email(\"Invalid email address\").max(255),\n password: z.string().min(1, \"Password is required\").max(128)\n });\n\n const updateProfileSchema = z.object({\n displayName: z.string().max(255).optional(),\n photoURL: z.string().url().max(2048).optional()\n });\n\n const findUserSchema = z.object({\n email: z.string().email(\"Invalid email address\").max(255)\n });\n\n /**\n * POST /auth/logout\n */\n router.post(\"/logout\", async (c) => {\n const body = await c.req.json().catch(() => ({}));\n const parsed = parseBody(logoutSchema, body);\n const refreshToken = readRefreshToken(c, parsed, config.cookieAuth);\n\n // Whose sign-out this is: the refresh token's row says, and it is what\n // the SDK and the CMS send. Read from the bearer token alone, as it\n // was, `afterLogout` never fired for either of them.\n let signedOutUid: string | undefined;\n if (refreshToken) {\n const tokenHash = await hashRefreshToken(refreshToken);\n // Kill the whole sign-in, not just the token that happened to be\n // presented. Rotation can leave siblings alive (a second tab, a\n // replay inside the reuse window), and deleting one row would\n // leave the others perfectly usable — a logout that does not log\n // you out is worse than no logout at all.\n const stored = await authRepo.findRefreshTokenByHash(tokenHash).catch(() => null);\n signedOutUid = stored?.uid;\n const sessionId = stored?.sessionId;\n if (sessionId && authRepo.revokeRefreshTokenSession) {\n await authRepo.revokeRefreshTokenSession(sessionId);\n } else {\n await authRepo.deleteRefreshToken(tokenHash);\n }\n }\n\n // Always clear the cookie if in cookie mode\n clearRefreshCookie(c, config.cookieAuth);\n\n // Call afterLogout hook (fire-and-forget). The refresh token's owner,\n // or failing that the access token's.\n if (ops.afterLogout) {\n if (!signedOutUid) {\n const accessToken = extractBearerToken(c.req.header(\"authorization\"));\n if (accessToken !== undefined) {\n signedOutUid = (await jwt.verifyAccessToken(accessToken))?.uid;\n }\n }\n if (signedOutUid) {\n ops.afterLogout(signedOutUid).catch((err: unknown) => {\n logger.error(\"[AuthHooks] afterLogout error\", {\n error: err instanceof Error ? err.message : err\n });\n });\n }\n }\n\n return c.json({ success: true });\n });\n\n /**\n * GET /auth/sessions\n * Get active refresh tokens (sessions) for the current user\n */\n router.get(\"/sessions\", requireLiveSession, async (c) => {\n const userCtx = c.get(\"user\") as { uid: string; roles?: string[]; sid?: string } | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n\n // Which of these is the caller's own: the access token says, by its\n // `sid`. The `x-refresh-token` header was the only way before, and no\n // first-party client sent it — in cookie mode none could — so every\n // row read \"not this device\", and revoking your own row signed nobody\n // out until the next request failed. Kept for a token minted before\n // `sid` existed.\n const currentRefreshToken = userCtx.sid ? undefined : c.req.header(\"x-refresh-token\");\n const currentTokenHash = currentRefreshToken ? await hashRefreshToken(currentRefreshToken) : null;\n\n const tokens = await authRepo.listRefreshTokensForUser(userCtx.uid);\n\n // One entry per SIGN-IN, not per token row. Rotation leaves several\n // rows behind for one session (the live token, its superseded parent\n // inside the reuse window, a sibling minted for a second tab), and a\n // user reading \"your devices\" must not be shown three copies of the\n // laptop they are sitting at. Revoked rows are tombstones and never\n // appear at all.\n const bySession = new Map<string, typeof tokens[number]>();\n for (const token of tokens) {\n if (token.revoked) continue;\n const key = token.sessionId ?? token.id;\n const existing = bySession.get(key);\n // Keep the newest row of the session as its representative, so the\n // id offered for revocation belongs to a token that still exists.\n if (!existing || existing.createdAt < token.createdAt) bySession.set(key, token);\n }\n\n const mappedSessions = [...bySession.values()].map((s) => ({\n id: s.id,\n userAgent: s.userAgent,\n ipAddress: s.ipAddress,\n // The sign-in, not the last rotation — otherwise every session\n // looks like it started an hour ago, whatever its real age.\n createdAt: s.sessionStartedAt ?? s.createdAt,\n isCurrentSession: userCtx.sid\n ? (s.sessionId ?? s.id) === userCtx.sid\n : currentTokenHash\n ? tokens.some(t => t.tokenHash === currentTokenHash && (t.sessionId ?? t.id) === (s.sessionId ?? s.id))\n : false\n }));\n\n return c.json({ sessions: mappedSessions });\n });\n\n /**\n * DELETE /auth/sessions\n * Delete all refresh tokens for the current user (remote logout every device)\n */\n router.delete(\"/sessions\", requireLiveSession, async (c) => {\n const userCtx = c.get(\"user\") as { uid: string; roles?: string[] } | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n\n // The rows and the watermark both: a refresh already in flight can\n // insert a rotated token a moment after the delete has run and walk\n // away with a live session, and the watermark voids it on its next use.\n await revokeAllSessions(authRepo, userCtx.uid);\n return c.json({\n success: true,\n message: \"All sessions revoked successfully\"\n });\n });\n\n /**\n * DELETE /auth/sessions/:id\n * Delete a specific refresh token (remote logout)\n */\n router.delete(\"/sessions/:id\", requireLiveSession, async (c) => {\n const userCtx = c.get(\"user\") as { uid: string; roles?: string[] } | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n\n const id = c.req.param(\"id\");\n if (!id) {\n throw ApiError.badRequest(\"Session ID is required\", \"INVALID_INPUT\");\n }\n\n // The id names one token row, but the user is revoking a device — so\n // resolve it to its session and take the whole family down. Deleting\n // the single row would leave that device holding a live sibling.\n const owned = await authRepo.listRefreshTokensForUser(userCtx.uid).catch(() => []);\n const target = owned.find(t => t.id === id);\n if (target?.sessionId && authRepo.revokeRefreshTokenSession) {\n await authRepo.revokeRefreshTokenSession(target.sessionId);\n } else {\n await authRepo.deleteRefreshTokenById(id, userCtx.uid);\n }\n return c.json({\n success: true,\n message: \"Session revoked successfully\"\n });\n });\n\n /**\n * GET /auth/me\n * Get current authenticated user\n */\n router.get(\"/me\", requireLiveSession, async (c) => {\n const userCtx = c.get(\"user\") as { uid: string; roles?: string[]; sid?: string } | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n\n const result = await authRepo.getUserWithRoles(userCtx.uid);\n if (!result) {\n throw ApiError.notFound(\"User not found\");\n }\n\n return c.json({\n user: {\n uid: result.user.id,\n email: result.user.email,\n displayName: result.user.displayName,\n photoURL: result.user.photoUrl,\n providerId: await sessionMethod(userCtx),\n isAnonymous: result.user.isAnonymous ?? false,\n emailVerified: result.user.emailVerified,\n pendingEmail: await pendingEmail(userCtx.uid),\n roles: result.roles,\n metadata: result.user.metadata ?? {}\n }\n });\n });\n\n /**\n * POST /auth/find-user\n * Resolve an email to a MINIMAL public profile (uid, displayName, photoURL).\n * Opt-in (`auth.allowUserLookup`) and authenticated-only — powers\n * invite-by-email flows without exposing the users table or a custom admin\n * server function. Never returns the email, roles, metadata, or any other\n * field of the looked-up user.\n */\n if (config.allowUserLookup) {\n router.post(\"/find-user\", limiters.default, requireLiveSession, async (c) => {\n const userCtx = c.get(\"user\") as { uid: string } | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n const { email } = parseBody(findUserSchema, await c.req.json());\n const user = await authRepo.getUserByEmail(normalizeEmail(email));\n return c.json({\n user: user\n ? { uid: user.id, displayName: user.displayName ?? null, photoURL: user.photoUrl ?? null }\n : null\n });\n });\n }\n\n /**\n * PATCH /auth/me\n * Update current authenticated user profile\n */\n router.patch(\"/me\", requireLiveSession, async (c) => {\n const userCtx = c.get(\"user\") as { uid: string; roles?: string[]; sid?: string } | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n\n const { displayName, photoURL } = parseBody(updateProfileSchema, await c.req.json());\n\n const updatedUser = await authRepo.updateUser(userCtx.uid, {\n displayName: displayName !== undefined ? displayName : undefined,\n photoUrl: photoURL !== undefined ? photoURL : undefined\n });\n\n if (!updatedUser) {\n throw ApiError.notFound(\"User not found\");\n }\n\n const result = await authRepo.getUserWithRoles(userCtx.uid);\n if (!result) {\n throw ApiError.notFound(\"User not found\");\n }\n\n return c.json({\n user: {\n uid: result.user.id,\n email: result.user.email,\n displayName: result.user.displayName,\n photoURL: result.user.photoUrl,\n providerId: await sessionMethod(userCtx),\n isAnonymous: result.user.isAnonymous ?? false,\n emailVerified: result.user.emailVerified,\n pendingEmail: await pendingEmail(userCtx.uid),\n roles: result.roles,\n metadata: result.user.metadata ?? {}\n }\n });\n });\n\n /**\n * POST /auth/anonymous\n * Create an anonymous user with temporary credentials\n */\n router.post(\"/anonymous\", limiters.strict, async (c) => {\n assertAnonymousAuthOpen();\n\n // `email` is NOT NULL, so an anonymous user needs a synthetic address —\n // and it lands under a unique index, which makes the width of this\n // string a correctness property rather than cosmetics. It used to keep 8\n // of the 32 hex characters generated here: a 2^32 space, where a\n // collision is ~1% likely by 9,000 anonymous users and ~50% by 77,000,\n // surfacing as an unexplained 500 on sign-in for one unlucky visitor.\n // The entropy was already generated; there was no reason to discard it.\n const anonEmail = `anon_${randomBytes(16).toString(\"hex\")}@anonymous.local`;\n\n let createData: CreateUserData = {\n email: anonEmail,\n emailVerified: false,\n isAnonymous: true\n };\n\n if (ops.beforeUserCreate) {\n createData = await ops.beforeUserCreate(createData);\n }\n\n const user = await authRepo.createUser(createData);\n\n // Assign default role (follow register route pattern, but never auto-admin)\n if (config.defaultRole) {\n await authRepo.assignDefaultRole(user.id, config.defaultRole);\n }\n\n const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(\n user.id,\n c.req.header(\"user-agent\") || \"unknown\",\n requestClientAddress(c),\n { method: \"anonymous\" }\n );\n\n // Fire afterUserCreate hook\n if (ops.afterUserCreate) {\n ops.afterUserCreate(user).catch((err: unknown) => {\n logger.error(\"[AuthHooks] afterUserCreate error\", {\n error: err instanceof Error ? err.message : err\n });\n });\n }\n\n // Fire onAuthenticated hook\n if (ops.onAuthenticated) {\n ops.onAuthenticated(user, \"anonymous\").catch((err: unknown) => {\n logger.error(\"[AuthHooks] onAuthenticated error\", {\n error: err instanceof Error ? err.message : err\n });\n });\n }\n\n const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken, \"anonymous\") as AuthResponsePayload;\n const transformedResponse = await applyTransformHook(authResponse, \"anonymous\", c.req.raw, user.id);\n return c.json(redactRefreshToken(transformedResponse, c, refreshToken, config.cookieAuth), 201);\n });\n\n /**\n * POST /auth/anonymous/link\n * Upgrade an anonymous user to a permanent account with email/password\n *\n * This is registration: before it the row is a guest, after it an account\n * that signs in with a password. So it answers to registration's controls,\n * the `register` captcha and `beforeUserCreate` on the credentials the\n * account is getting. `/anonymous` carries neither, deliberately, because\n * apps mint guests on page load; the two in a row used to be a way to make\n * an account that passed neither.\n */\n router.post(\"/anonymous/link\", limiters.strict, requireLiveSession, ...(registerCaptcha ? [registerCaptcha] : []), async (c) => {\n // Gated on the same predicate as `/anonymous`: if anonymous auth is\n // off, any session reaching here predates the switch, and letting it\n // finish would be a second way to reach the state the switch exists to\n // prevent.\n assertAnonymousAuthOpen();\n // And on registration's, because this is registration: the guest\n // becomes an account that signs in with a password. Gated on anonymous\n // auth alone, a backend with registration closed still made password\n // accounts for anyone — sign in as a guest, then link.\n if (!isSteadyStateRegistrationOpen({\n disableSelfRegistration: config.disableSelfRegistration,\n allowRegistration: config.allowRegistration ?? false\n })) {\n throw ApiError.forbidden(\n \"A guest cannot become an account here: registration is disabled on this backend.\",\n \"REGISTRATION_DISABLED\"\n );\n }\n\n const userCtx = c.get(\"user\") as { uid: string; roles?: string[] } | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n\n const user = await authRepo.getUserById(userCtx.uid);\n if (!user?.isAnonymous) {\n throw ApiError.badRequest(\"User is not anonymous\", \"NOT_ANONYMOUS\");\n }\n\n const { email, password } = parseBody(linkSchema, await c.req.json());\n\n // Validate password strength\n const passwordValidation = ops.validatePasswordStrength(password);\n if (!passwordValidation.valid) {\n throw ApiError.badRequest(passwordValidation.errors.join(\". \"), \"WEAK_PASSWORD\");\n }\n\n // Check if email is already taken\n const existingUser = await authRepo.getUserByEmail(normalizeEmail(email));\n if (existingUser) {\n throw ApiError.conflict(\"Email already registered\", \"EMAIL_EXISTS\");\n }\n\n // Hash password\n const passwordHash = await ops.hashPassword(password);\n\n // The hook sees what `/register` would hand it, and may refuse the\n // address or change what is stored, as it does there.\n let linkData: CreateUserData = {\n email: normalizeEmail(email),\n passwordHash\n };\n if (ops.beforeUserCreate) {\n linkData = await ops.beforeUserCreate(linkData);\n }\n\n // Update user: set email, password, remove anonymous flag. The hash is\n // named rather than left to the spread, because this is the one\n // `updateUser` that writes a password and the check in\n // `password-change-revokes-sessions.test.ts` reads it by name.\n const updatedUser = await authRepo.updateUser(user.id, {\n ...linkData,\n email: normalizeEmail(linkData.email),\n passwordHash: linkData.passwordHash,\n isAnonymous: false\n });\n\n if (!updatedUser) {\n throw ApiError.notFound(\"User not found\");\n }\n sendVerificationMail?.(updatedUser);\n\n // Generate new tokens with updated identity\n const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(\n user.id,\n c.req.header(\"user-agent\") || \"unknown\",\n requestClientAddress(c),\n { method: \"password\" }\n );\n\n const authResponse = buildAuthResponse(updatedUser, roleIds, accessToken, refreshToken, \"password\") as AuthResponsePayload;\n const transformedResponse = await applyTransformHook(authResponse, \"anonymous\", c.req.raw, user.id);\n return c.json(redactRefreshToken(transformedResponse, c, refreshToken, config.cookieAuth));\n });\n}\n","/**\n * Sign-up by magic link or email code (`auth.magicLinkCreatesUsers`).\n *\n * Both passwordless doors refuse an address with no account — silently, so\n * they do not say which addresses have one. A passwordless-only app then had\n * no way to create accounts at all. With the option on, and registration open,\n * an unknown address gets an account at request time — no password, unverified\n * — and the link or code mailed to it is what proves the address and signs it\n * in, exactly as for an existing account. Supabase's `shouldCreateUser`.\n *\n * @module\n */\n\nimport { normalizeEmail } from \"@rebasepro/common\";\nimport { logger } from \"../utils/logger\";\nimport type { AuthModuleConfig } from \"./routes\";\nimport type { ResolvedAuthHooks } from \"./auth-hooks\";\nimport type { CreateUserData, UserData } from \"./interfaces\";\nimport { isSteadyStateRegistrationOpen } from \"./registration-policy\";\n\n/**\n * The account a passwordless request for `email` should mail, creating it\n * when the deployment lets passwordless requests sign people up. `null` when\n * there is none and none may be made — the caller answers as for any unknown\n * address.\n *\n * Registration's controls hold: the kill switch, `allowRegistration` (with no\n * first-user exception: the first admin is made by registering or by the\n * operator, never by whoever asks for a link first), `beforeUserCreate`,\n * `afterUserCreate` and the default role.\n */\nexport async function accountForPasswordlessRequest(\n config: AuthModuleConfig,\n ops: ResolvedAuthHooks,\n email: string\n): Promise<UserData | null> {\n const authRepo = config.authRepo;\n const existing = await authRepo.getUserByEmail(email);\n if (existing || !config.magicLinkCreatesUsers) return existing;\n if (!isSteadyStateRegistrationOpen({\n disableSelfRegistration: config.disableSelfRegistration,\n allowRegistration: config.allowRegistration ?? false\n })) {\n return null;\n }\n\n let createData: CreateUserData = { email: normalizeEmail(email) };\n if (ops.beforeUserCreate) createData = await ops.beforeUserCreate(createData);\n\n let user: UserData;\n try {\n user = await authRepo.createUser(createData);\n } catch (error) {\n // Two requests for one new address raced; the other one made it.\n const made = await authRepo.getUserByEmail(email);\n if (made) return made;\n throw error;\n }\n if (config.defaultRole) await authRepo.assignDefaultRole(user.id, config.defaultRole);\n if (ops.afterUserCreate) {\n ops.afterUserCreate(user).catch((err: unknown) => {\n logger.error(\"[AuthHooks] afterUserCreate error\", { error: err instanceof Error ? err.message : err });\n });\n }\n return user;\n}\n","import { Hono } from \"hono\";\nimport type { MiddlewareHandler } from \"hono\";\nimport type { AuthModuleConfig, CreateSessionAndTokens } from \"./routes\";\nimport { accountForPasswordlessRequest } from \"./passwordless-signup\";\nimport type { ResolvedAuthHooks } from \"./auth-hooks\";\nimport type { HonoEnv } from \"../api/types\";\nimport { ApiError } from \"../api/errors\";\nimport { generateSecureToken, hashToken } from \"./admin-user-ops\";\nimport { getMagicLinkTemplate, resolveEmailBranding } from \"../email/templates\";\nimport { resolveEmailLinkBase } from \"../email/link-base\";\nimport {\n RECIPIENT_ROUTE_FLOOR_MS,\n captureRecipientEmail,\n notBefore,\n requestClientAddress,\n type AuthLimiters } from \"./rate-limiter\";\nimport { z } from \"zod\";\nimport { logger } from \"../utils/logger\";\nimport { redactRefreshToken } from \"./cookie-utils\";\nimport { confirmAddressOwnership } from \"./address-ownership\";\nimport type { AuthResponsePayload, TransformAuthResponseContext } from \"@rebasepro/types\";\n\n/**\n * Magic link token expiry (15 minutes from now)\n */\nfunction getMagicLinkExpiry(): Date {\n return new Date(Date.now() + 15 * 60 * 1000);\n}\n\n/**\n * Mount magic link routes onto the auth router.\n *\n * Follows the same delegation pattern as `mountMfaRoutes()` and `mountSessionRoutes()`.\n */\nexport function mountMagicLinkRoutes(deps: {\n router: Hono<HonoEnv>;\n config: AuthModuleConfig;\n /** The backend's auth limiters. */\n limiters: AuthLimiters;\n ops: ResolvedAuthHooks;\n parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T;\n buildAuthResponse: (\n user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; emailVerified?: boolean; isAnonymous?: boolean; metadata?: Record<string, unknown> | null },\n roleIds: string[],\n accessToken: string,\n refreshToken: string,\n providerId: string\n ) => unknown;\n createSessionAndTokens: CreateSessionAndTokens;\n applyTransformHook: (\n response: AuthResponsePayload,\n method: TransformAuthResponseContext[\"method\"],\n request: Request,\n uid: string\n ) => Promise<AuthResponsePayload>;\n /**\n * Built by the caller so a misconfiguration fails the boot once, rather\n * than being resolved again here. Absent when captcha is off, or when\n * `magicLink` is not among the protected routes.\n */\n captchaMiddleware?: MiddlewareHandler<HonoEnv>;\n}) {\n const { router, config, limiters, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook, captchaMiddleware } = deps;\n const { authRepo, emailService, emailConfig } = config;\n\n const magicLinkSchema = z.object({\n email: z.string().email(\"Invalid email address\").max(255)\n });\n\n const verifyMagicLinkSchema = z.object({\n token: z.string().min(1, \"Token is required\")\n });\n\n function isEmailConfigured(): boolean {\n return !!(emailService && emailService.isConfigured());\n }\n\n /**\n * POST /auth/magic-link\n * Request a magic link email\n *\n * Answers the same words whether or not the address has an account, and\n * is held to the same three things as its siblings `/otp` and\n * `/forgot-password` so that nothing else answers the question either:\n * the same response time, a per-address send limit, and `beforeLogin` on\n * every attempt. This route had none of the three — the SMTP send was\n * awaited only for a real account, and the only limit was per IP — so its\n * timing named the customers and a spread-out run could fill one inbox.\n */\n router.post(\n \"/magic-link\",\n limiters.strict,\n // Per-address as well as per-IP: an IP is the attacker's to rotate,\n // the mailbox being filled is not. See `recipientEmailLimiter`.\n captureRecipientEmail,\n limiters.recipientEmail,\n ...(captchaMiddleware ? [captchaMiddleware] : []),\n async (c) => {\n const startedAt = Date.now();\n const { email } = parseBody(magicLinkSchema, await c.req.json());\n\n // Require email service\n if (!isEmailConfigured()) {\n throw ApiError.serviceUnavailable(\"Email service not configured. Magic link login is not available.\", \"EMAIL_NOT_CONFIGURED\");\n }\n\n // Fired for every attempt, not only the ones that name a real account,\n // for the reason `/otp` gives: an enumeration run is made entirely of\n // addresses that do not exist, and a hook called only for real ones\n // is itself the answer.\n if (ops.beforeLogin) {\n await ops.beforeLogin(email, \"magic-link\");\n }\n\n // Always return success (security: don't reveal if email exists). With\n // `magicLinkCreatesUsers`, an unknown address gets its account here.\n const user = await accountForPasswordlessRequest(config, ops, email);\n\n if (user) {\n // Generate magic link token\n const token = generateSecureToken();\n const tokenHash = hashToken(token);\n const expiresAt = getMagicLinkExpiry();\n\n await authRepo.createMagicLinkToken(user.id, tokenHash, expiresAt);\n\n // Build magic link URL\n const baseUrl = resolveEmailLinkBase(emailConfig, \"magicLink\");\n const magicLinkUrl = `${baseUrl}/auth/magic-link?token=${token}`;\n\n // Get email template\n const { appName, logoUrl } = resolveEmailBranding(emailConfig);\n const templateFn = emailConfig?.templates?.magicLink;\n const emailContent = templateFn\n ? templateFn(magicLinkUrl, { email: user.email, displayName: user.displayName })\n : getMagicLinkTemplate(magicLinkUrl, { email: user.email, displayName: user.displayName }, appName, logoUrl);\n\n // Deliberately NOT awaited: an SMTP round trip happens only on the\n // branch where the address exists, so awaiting it made the\n // response time the answer the response text refuses to give. A\n // failure was already withheld from the caller for the same reason.\n void emailService!.send({\n to: user.email,\n subject: emailContent.subject,\n html: emailContent.html,\n text: emailContent.text\n }).catch((emailError: unknown) => {\n logger.error(\"Failed to send magic link email\", { error: emailError instanceof Error ? emailError.message : emailError });\n });\n }\n\n // Held to a floor, so a known address and an unknown one take the same\n // time to answer. See `RECIPIENT_ROUTE_FLOOR_MS`.\n return notBefore(startedAt, RECIPIENT_ROUTE_FLOOR_MS, c.json({\n success: true,\n message: \"If an account with that email exists, a magic link has been sent.\"\n }));\n });\n\n /**\n * POST /auth/magic-link/verify\n * Verify magic link token and create session\n */\n router.post(\"/magic-link/verify\", limiters.strict, async (c) => {\n const { token } = parseBody(verifyMagicLinkSchema, await c.req.json());\n\n // Find valid token\n const tokenHash = hashToken(token);\n const storedToken = await authRepo.findValidMagicLinkToken(tokenHash);\n\n if (!storedToken) {\n throw ApiError.badRequest(\"Invalid or expired magic link\", \"INVALID_TOKEN\");\n }\n\n // Mark token as used (one-time use)\n await authRepo.markMagicLinkTokenUsed(tokenHash);\n\n // Get user\n let user = await authRepo.getUserById(storedToken.uid);\n if (!user) {\n throw ApiError.badRequest(\"Invalid or expired magic link\", \"INVALID_TOKEN\");\n }\n\n // Clicking a magic link proves email ownership. On an account whose\n // address nobody had proven, that proof also removes whatever way in\n // somebody else left on it — see `confirmAddressOwnership`.\n if (!user.emailVerified) {\n await confirmAddressOwnership(authRepo, user, null);\n user = { ...user, emailVerified: true, passwordHash: null };\n }\n\n // Create session\n const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(\n user.id,\n c.req.header(\"user-agent\") || \"unknown\",\n requestClientAddress(c),\n { method: \"magic-link\" }\n );\n\n // Fire onAuthenticated hook (fire-and-forget)\n if (ops.onAuthenticated) {\n ops.onAuthenticated(user, \"magic-link\").catch(err => {\n logger.error(\"[AuthHooks] onAuthenticated error\", { error: err instanceof Error ? err.message : err });\n });\n }\n\n const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken, \"magic-link\") as AuthResponsePayload;\n const transformedResponse = await applyTransformHook(authResponse, \"magic-link\", c.req.raw, user.id);\n return c.json(redactRefreshToken(transformedResponse, c, refreshToken, config.cookieAuth));\n });\n}\n","/**\n * Email one-time codes: six digits, typed in, instead of a link to click.\n *\n * Magic link already logs somebody in from their inbox, and on a desktop it is\n * the better flow. It stops being the better flow the moment the two devices\n * are not the same one: the link opens the session on the phone that has the\n * mail, not on the television, the terminal, or the browser the person is\n * actually sitting in front of. A code crosses that gap because a human carries\n * it.\n *\n * ```\n * POST /auth/otp { \"email\": \"…\" } → always 200\n * POST /auth/otp/verify { \"email\": \"…\", \"code\": \"…\" } → a session\n * ```\n *\n * ## Six digits is a small number, so the shape matters\n *\n * A million possibilities is nothing to a machine, which is why each of these\n * is load-bearing rather than decorative:\n *\n * - **the code is bound to the address.** What is stored is a hash of\n * `otp:<email>:<code>`, so `verify` needs both halves and there is no\n * \"guess any valid code in the system\" — the search space is one account's,\n * not the deployment's. A lookup keyed on the code alone would make every\n * account in the database a target of the same million guesses;\n * - **attempts are limited per address**, not only per IP. An IP is the\n * attacker's to rotate and the account under attack is not — the same\n * reasoning `mfaVerificationLimiter` is built on, and the same numbers;\n * - **ten minutes**, and single use. The window is what the brute force gets;\n * - **the digits are uniform.** `randomInt` rather than `random() * 1e6` or a\n * modulo of random bytes, both of which make some codes likelier.\n *\n * ## Why it reuses the magic-link token store\n *\n * A one-time code and a magic-link token are the same object: a hashed secret\n * for one account, with an expiry and a used flag. Reusing the store means no\n * schema change, no second cleanup path, and no second place for \"was this\n * already used?\" to be answered differently. What is hashed differs, and that\n * is the whole difference — a code cannot be presented as a link token because\n * the hash carries `otp:` and the address.\n */\nimport { Hono } from \"hono\";\nimport type { MiddlewareHandler } from \"hono\";\nimport { randomInt } from \"../utils/portable-crypto\";\nimport { z } from \"zod\";\n\nimport type { AuthModuleConfig, CreateSessionAndTokens } from \"./routes\";\nimport { accountForPasswordlessRequest } from \"./passwordless-signup\";\nimport type { ResolvedAuthHooks } from \"./auth-hooks\";\nimport type { HonoEnv } from \"../api/types\";\nimport { ApiError } from \"../api/errors\";\nimport { hashToken } from \"./admin-user-ops\";\nimport { getEmailOtpTemplate, resolveEmailBranding } from \"../email/templates\";\nimport { requestClientAddress, type AuthLimiters, type NamedRateLimiterOptions } from \"./rate-limiter\";\nimport { logger } from \"../utils/logger\";\nimport { redactRefreshToken } from \"./cookie-utils\";\nimport { confirmAddressOwnership } from \"./address-ownership\";\nimport type { AuthResponsePayload, TransformAuthResponseContext } from \"@rebasepro/types\";\n\n/** How long a code is good for. */\nexport const OTP_TTL_MS = 10 * 60 * 1000;\n\n/** How many digits. Six is what people expect to be asked to type. */\nconst OTP_DIGITS = 6;\n\n/** Verification attempts allowed per address per window. */\nconst OTP_ATTEMPTS_PER_WINDOW = 5;\n\n/**\n * A uniformly random code, as a zero-padded string.\n *\n * `randomInt` over the whole range rather than digit-by-digit or a modulo of\n * random bytes: both of those are ways to make some codes likelier than others,\n * and one of them is the classic modulo bias.\n */\nexport function generateOtpCode(): string {\n return String(randomInt(10 ** OTP_DIGITS)).padStart(OTP_DIGITS, \"0\");\n}\n\n/**\n * What gets hashed and stored.\n *\n * The address is part of the secret, which is what makes `verify` a question\n * about one account rather than about the whole table. Lower-cased and trimmed\n * so that the address the user types matches the one they were mailed at.\n */\nexport function otpTokenMaterial(email: string, code: string): string {\n return `otp:${email.trim().toLowerCase()}:${code}`;\n}\n\n/**\n * Per-address throttle on code verification.\n *\n * Keyed on the address rather than the IP, for the reason\n * `mfaVerificationLimiter` gives: an IP is the attacker's to rotate and the\n * account is not, so a distributed run against one address passes an IP-keyed\n * limiter untouched. `strictAuthLimiter` still runs in front — they bound\n * different things.\n *\n * Like every limiter here, this counts in the store the deployment configured:\n * per replica by default, shared with `REBASE_RATE_LIMIT_STORE=sql`. On several\n * replicas with the default, the effective budget is five attempts per replica.\n */\nconst OTP_VERIFICATION_LIMIT: NamedRateLimiterOptions = {\n name: \"auth-otp-verify\",\n windowMs: 15 * 60 * 1000,\n limit: OTP_ATTEMPTS_PER_WINDOW,\n keyGenerator: (c) => {\n const email = (c.get(\"otpEmail\") as string | undefined) ?? \"unidentified\";\n return `otp-verify:${email}`;\n },\n message: \"Too many verification attempts for this account, please try again later.\"\n};\n\n/** Codes a single address may be MAILED per window. */\nconst OTP_SENDS_PER_WINDOW = 5;\n\n/**\n * Per-address throttle on code *delivery*.\n *\n * The verification limiter below has always been keyed per address; the send\n * route had only `strictAuthLimiter`, which is keyed per IP. An IP is the\n * attacker's to rotate and the mailbox under attack is not, so a distributed\n * run put an unbounded number of messages into one person's inbox — from this\n * deployment's domain, which is the reputation that pays for it.\n *\n * Five per fifteen minutes is generous for \"I didn't get it, send it again\"\n * and useless as a mail bomb, matching `verificationEmailLimiter`.\n */\nconst OTP_SEND_LIMIT: NamedRateLimiterOptions = {\n name: \"auth-otp-send\",\n windowMs: 15 * 60 * 1000,\n limit: OTP_SENDS_PER_WINDOW,\n keyGenerator: (c) => {\n const email = (c.get(\"otpEmail\") as string | undefined) ?? \"unidentified\";\n return `otp-send:${email}`;\n },\n message: \"Too many sign-in codes requested for this account, please try again later.\"\n};\n\n/**\n * How long the send route takes, whoever asked.\n *\n * The route answers the same words for a known and an unknown address, which is\n * the point — but it did not take the same TIME. A known address meant a token\n * insert, a template render and an SMTP round trip; an unknown one meant\n * returning immediately. The difference is measurable from anywhere, so the\n * endpoint that refuses to say whether an account exists said it anyway, one\n * address at a time and at whatever rate the IP limiter allowed.\n *\n * Two things close it, and both are needed. The mail is sent off the response\n * path (below), which removes the largest and most variable part. And the\n * handler is held to a floor, which covers the rest — the token write, the\n * template — without needing the two branches to be instruction-for-instruction\n * equal, which is not a property anyone can maintain.\n *\n * 400ms is chosen to sit above the slow branch's normal cost rather than to\n * be pleasant: it is a login step a person takes once, and the alternative is\n * an enumeration oracle.\n */\nconst OTP_SEND_FLOOR_MS = 400;\n\n/** Hold a result until at least `floorMs` has passed since `startedAt`. */\nasync function notBefore<T>(startedAt: number, floorMs: number, result: T): Promise<T> {\n const remaining = floorMs - (Date.now() - startedAt);\n if (remaining > 0) await new Promise((resolve) => setTimeout(resolve, remaining));\n return result;\n}\n\n/**\n * Read the address out of the body and put it where the limiter can key on it.\n *\n * The limiter runs before the handler, and Hono has already consumed nothing —\n * so this parses the JSON body first, stashes the address, and lets the handler\n * re-read it from the same cached request. Without it the limiter has no\n * account to key on and falls back to one global bucket, which is a limiter in\n * name only.\n */\nconst captureOtpEmail: MiddlewareHandler<HonoEnv> = async (c, next) => {\n try {\n const body = await c.req.raw.clone().json() as { email?: unknown };\n if (typeof body?.email === \"string\") {\n c.set(\"otpEmail\", body.email.trim().toLowerCase());\n }\n } catch {\n // A malformed body is the handler's error to report, with the message\n // that names the field. Failing here would answer 429 to bad JSON.\n }\n await next();\n};\n\nexport function mountOtpRoutes(deps: {\n router: Hono<HonoEnv>;\n config: AuthModuleConfig;\n /** The backend's auth limiters; the per-address ones count where they do. */\n limiters: AuthLimiters;\n ops: ResolvedAuthHooks;\n parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T;\n buildAuthResponse: (\n user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; emailVerified?: boolean; isAnonymous?: boolean; metadata?: Record<string, unknown> | null },\n roleIds: string[],\n accessToken: string,\n refreshToken: string,\n providerId: string\n ) => unknown;\n createSessionAndTokens: CreateSessionAndTokens;\n applyTransformHook: (\n response: AuthResponsePayload,\n method: TransformAuthResponseContext[\"method\"],\n request: Request,\n uid: string\n ) => Promise<AuthResponsePayload>;\n /** Built by the caller, as for magic link. Absent when captcha is off. */\n captchaMiddleware?: MiddlewareHandler<HonoEnv>;\n}) {\n const { router, config, limiters, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook, captchaMiddleware } = deps;\n const { authRepo, emailService, emailConfig } = config;\n const otpVerificationLimiter = limiters.named(OTP_VERIFICATION_LIMIT);\n const otpSendLimiter = limiters.named(OTP_SEND_LIMIT);\n\n const requestSchema = z.object({\n email: z.string().email(\"Invalid email address\").max(255)\n });\n\n const verifySchema = z.object({\n email: z.string().email(\"Invalid email address\").max(255),\n code: z.string().regex(/^\\d{6}$/, \"A code is six digits\")\n });\n\n const isEmailConfigured = (): boolean => !!(emailService && emailService.isConfigured());\n\n /**\n * POST /auth/otp — send a code.\n *\n * Answers the same thing whether or not the address has an account. The\n * alternative is an oracle that turns this endpoint into a way to ask \"is\n * this person a customer?\", one address at a time.\n */\n router.post(\n \"/otp\",\n limiters.strict,\n // Per-address, in front of the handler, for the reason `otpSendLimiter`\n // gives: the IP limiter above bounds a machine, and the mailbox being\n // filled belongs to somebody who cannot rotate anything.\n captureOtpEmail,\n otpSendLimiter,\n ...(captchaMiddleware ? [captchaMiddleware] : []),\n async (c) => {\n const startedAt = Date.now();\n const { email } = parseBody(requestSchema, await c.req.json());\n\n if (!isEmailConfigured()) {\n throw ApiError.serviceUnavailable(\n \"Email service not configured. One-time code login is not available.\",\n \"EMAIL_NOT_CONFIGURED\"\n );\n }\n\n // Fired for every attempt, not only the ones that name a real account.\n //\n // This hook is where a deployment blocks or audits sign-in attempts,\n // and half of them were invisible to it: an enumeration run consists\n // ENTIRELY of addresses that do not exist, so the hook could not see\n // the thing it would most want to. It also means the hook cannot be\n // used to infer whether an address is registered, which it could when\n // its being called was itself the answer.\n if (ops.beforeLogin) {\n await ops.beforeLogin(email, \"otp\");\n }\n\n // With `magicLinkCreatesUsers`, an unknown address gets its account here.\n const user = await accountForPasswordlessRequest(config, ops, email);\n\n if (user) {\n const code = generateOtpCode();\n await authRepo.createMagicLinkToken(\n user.id,\n hashToken(otpTokenMaterial(user.email, code)),\n new Date(Date.now() + OTP_TTL_MS)\n );\n\n const { appName, logoUrl } = resolveEmailBranding(emailConfig);\n const templateFn = emailConfig?.templates?.emailOtp;\n const content = templateFn\n ? templateFn(code, { email: user.email, displayName: user.displayName })\n : getEmailOtpTemplate(\n code,\n { email: user.email, displayName: user.displayName },\n appName,\n logoUrl\n );\n\n // Deliberately NOT awaited.\n //\n // An SMTP round trip is the largest and most variable thing this\n // handler does, and it only happens on the branch where the address\n // exists — so awaiting it made the response time the answer to the\n // question the response text refuses to answer. Off the response\n // path, the remaining difference is a token insert, which the floor\n // below covers.\n //\n // A failure was already not reported to the caller, for the same\n // reason; the only change is that the log line now arrives after\n // the response rather than before it.\n void emailService!.send({\n to: user.email,\n subject: content.subject,\n html: content.html,\n text: content.text\n }).catch((emailError: unknown) => {\n logger.error(\"Failed to send one-time code email\", {\n error: emailError instanceof Error ? emailError.message : emailError\n });\n });\n }\n\n // Held to a floor, so a known address and an unknown one take the same\n // time to answer. See OTP_SEND_FLOOR_MS.\n return notBefore(startedAt, OTP_SEND_FLOOR_MS, c.json({\n success: true,\n message: \"If an account with that email exists, a sign-in code has been sent.\",\n expiresInSeconds: OTP_TTL_MS / 1000\n }));\n });\n\n /**\n * POST /auth/otp/verify — trade a code for a session.\n */\n router.post(\"/otp/verify\", limiters.strict, captureOtpEmail, otpVerificationLimiter, async (c) => {\n const { email, code } = parseBody(verifySchema, await c.req.json());\n\n // The lookup and the binding are the same act: a hash that does not\n // include this address cannot be found by this request.\n const tokenHash = hashToken(otpTokenMaterial(email, code));\n const storedToken = await authRepo.findValidMagicLinkToken(tokenHash);\n\n if (!storedToken) {\n throw ApiError.badRequest(\"Invalid or expired code\", \"INVALID_CODE\");\n }\n\n await authRepo.markMagicLinkTokenUsed(tokenHash);\n\n let user = await authRepo.getUserById(storedToken.uid);\n if (!user) {\n throw ApiError.badRequest(\"Invalid or expired code\", \"INVALID_CODE\");\n }\n\n // Reading a code out of the inbox proves the address, exactly as\n // following a link does — with the same consequences for whatever\n // somebody else left on an account nobody had proven.\n if (!user.emailVerified) {\n await confirmAddressOwnership(authRepo, user, null);\n user = { ...user, emailVerified: true, passwordHash: null };\n }\n\n const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(\n user.id,\n c.req.header(\"user-agent\") || \"unknown\",\n requestClientAddress(c),\n { method: \"otp\" }\n );\n\n if (ops.onAuthenticated) {\n ops.onAuthenticated(user, \"otp\").catch(err => {\n logger.error(\"[AuthHooks] onAuthenticated error\", {\n error: err instanceof Error ? err.message : err\n });\n });\n }\n\n const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken, \"otp\") as AuthResponsePayload;\n const transformedResponse = await applyTransformHook(authResponse, \"otp\", c.req.raw, user.id);\n return c.json(redactRefreshToken(transformedResponse, c, refreshToken, config.cookieAuth));\n });\n}\n\n","/**\n * The two decisions the OAuth sign-in route makes before it will hand a caller\n * an existing account, extracted so they are stated once for all twelve\n * providers and can be tested without a provider, a network or a database.\n */\n\n/** Why an incoming OAuth identity may not be auto-attached to an existing account. */\nexport type AutoLinkRefusal =\n /** The provider did not report that it verified the address. */\n | \"provider-email-unverified\";\n\nexport type AutoLinkDecision =\n | {\n allowed: true;\n /**\n * The local account's address was never proven, and this sign-in\n * proves it. The route settles the account the way an email code does\n * (`confirmAddressOwnership`) before it links the identity: the\n * password and every identity whose provider did not vouch for the\n * address go, and every session ends.\n */\n provesAddress: boolean;\n }\n | { allowed: false; reason: AutoLinkRefusal };\n\n/** The part of an existing user row the decision depends on. */\nexport interface AutoLinkExistingUser {\n emailVerified: boolean;\n passwordHash?: string | null;\n}\n\n/**\n * May an OAuth identity be attached to a pre-existing account found *by email*?\n *\n * Only when the **provider** verified the address. Otherwise the caller has\n * not shown they control it, and linking would hand them the account.\n *\n * When it did, the caller has proven the address exactly as reading a code out\n * of the inbox does: only the inbox's owner can hold a provider account that\n * vouches for it. If the **local account**'s address was never verified,\n * whoever made it never proved the address, and every way in they left on it —\n * a password, an identity from a provider that did not vouch for the address,\n * a live session — would go on opening the account the owner is about to be\n * signed into. So that case answers `provesAddress: true`, and the route\n * removes those ways in before it links (the pre-hijack defence). It used to\n * refuse instead, which left the owner of the address locked out of their own\n * address until they found a magic link or a reset.\n */\nexport function decideOAuthAutoLink(args: {\n providerEmailVerified: boolean | undefined;\n existingUser: AutoLinkExistingUser;\n}): AutoLinkDecision {\n if (args.providerEmailVerified !== true) {\n return { allowed: false, reason: \"provider-email-unverified\" };\n }\n return { allowed: true, provesAddress: !args.existingUser.emailVerified };\n}\n\n/**\n * Is `redirectUri` one the operator authorised?\n *\n * The provider's own registered-URI match is a real control but a coarse one:\n * it authorises *every* URI registered on that OAuth client, so a `localhost`\n * entry kept for development, or a second product sharing the client id, can\n * mint codes this backend accepts. An empty or absent allowlist keeps the old\n * behaviour (the provider's check is the only one) so existing deployments are\n * unaffected; setting one narrows it to the origins this backend serves.\n *\n * Comparison is on origin plus path, with the origin lowercased and a trailing\n * slash ignored — query strings and fragments are not part of the identity of a\n * redirect URI, and neither is the case of the host.\n */\nexport function isRedirectUriAllowed(redirectUri: string, allowlist?: string[]): boolean {\n if (!allowlist || allowlist.length === 0) return true;\n const candidate = canonicalRedirectUri(redirectUri);\n if (!candidate) return false;\n return allowlist.some((allowed) => canonicalRedirectUri(allowed) === candidate);\n}\n\nfunction canonicalRedirectUri(uri: string): string | null {\n let parsed: URL;\n try {\n parsed = new URL(uri);\n } catch {\n return null;\n }\n const path = parsed.pathname.replace(/\\/+$/, \"\");\n return `${parsed.protocol.toLowerCase()}//${parsed.host.toLowerCase()}${path}`;\n}\n","import { Hono } from \"hono\";\nimport { normalizeEmail } from \"@rebasepro/common\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport { randomBytes, randomUUID } from \"crypto\";\nimport { generateSecureToken, hashToken } from \"./admin-user-ops\";\nimport type { AuthRepository, OAuthProvider, OAuthProviderProfile, CreateUserData } from \"./interfaces\";\nimport { generateRefreshToken, hashRefreshToken, processJwtService, type JwtService } from \"./jwt\";\nimport { extractBearerToken } from \"./bearer-token\";\nimport type { AuthHooks } from \"./auth-hooks\";\nimport { resolveAuthHooks } from \"./auth-hooks\";\nimport { createRequireAuth, requireAuth } from \"./middleware\";\nimport { judgeAccessToken, replaceUserPassword } from \"./token-revocation\";\nimport { EmailService, EmailConfig, resolveEmailLinkBase } from \"../email\";\nimport { getPasswordResetTemplate, getEmailVerificationTemplate, getWelcomeEmailTemplate, resolveEmailBranding } from \"../email/templates\";\nimport { HonoEnv } from \"../api/types\";\nimport {\n RECIPIENT_ROUTE_FLOOR_MS,\n captureRecipientEmail,\n createRateLimiter,\n defaultAuthLimiter,\n notBefore,\n recipientEmailLimiter,\n strictAuthLimiter,\n verificationEmailLimiter, requestClientAddress, type AuthLimiters } from \"./rate-limiter\";\nimport { buildCaptchaMiddlewares, type CaptchaConfig } from \"./captcha\";\nimport { z } from \"zod\";\nimport { logger } from \"../utils/logger\";\nimport { mountMfaRoutes } from \"./mfa-routes\";\nimport { assertMfaSatisfied } from \"./mfa-gate\";\nimport { mountSessionRoutes } from \"./session-routes\";\nimport { mountMagicLinkRoutes } from \"./magic-link-routes\";\nimport { mountOtpRoutes } from \"./otp-routes\";\nimport { isBootstrapWindowOpen, isOnlyUser, isSteadyStateRegistrationOpen, isUserTableEmpty, SETUP_REQUIRED_MESSAGE } from \"./registration-policy\";\nimport { decideOAuthAutoLink, isRedirectUriAllowed } from \"./oauth-signin-policy\";\nimport { confirmAddressOwnership, identityProfileData, identityVouchesForAddress } from \"./address-ownership\";\nimport type { AuthResponsePayload, TransformAuthResponseContext } from \"@rebasepro/types\";\nimport { ADMIN_ROLE, isAdminScope, parseScope } from \"@rebasepro/types\";\nimport { processAccessControl, type AccessControl } from \"./access\";\nimport type { Context } from \"hono\";\nimport { readRefreshToken, redactRefreshToken, clearRefreshCookie } from \"./cookie-utils\";\nimport { isDeliverableAddress } from \"./deliverable-address\";\nimport { mountEmailChangeRoutes } from \"./email-change-routes\";\n\nexport { isDeliverableAddress };\n\n/**\n * Shared configuration for auth and admin route factories.\n */\nexport interface AuthModuleConfig {\n authRepo: AuthRepository;\n /**\n * The backend these routes sign sessions for: its JWT keys and token\n * lifetimes. The process default when omitted — the one `configureJwt`\n * sets.\n */\n jwt?: JwtService;\n /** The backend's auth limiters, counting in its own store. The process default's set when omitted. */\n limiters?: AuthLimiters;\n /**\n * The backend's access model — what the default role may hold is checked\n * against it. The process default when omitted.\n */\n access?: AccessControl;\n /**\n * Bot protection. Off unless `enabled`.\n *\n * Rate limiting bounds one caller; a botnet of a thousand addresses sending\n * one request each never touches a per-IP window, and `/register` and\n * `/forgot-password` both send mail. See `captcha.ts`.\n */\n captcha?: CaptchaConfig;\n emailService?: EmailService;\n emailConfig?: EmailConfig;\n /** Allow new user registration (default: false). */\n allowRegistration?: boolean;\n /** Expose the authenticated email→minimal-profile lookup route (default: false). */\n allowUserLookup?: boolean;\n /** Default role ID to assign to new users (default: none). Must NOT be \"admin\". */\n defaultRole?: string;\n /** Optional array of OAuth providers */\n oauthProviders?: OAuthProvider<unknown>[];\n /**\n * Redirect URIs the OAuth routes will accept, for every provider.\n *\n * Left unset, the only check is the provider's own registered-URI match,\n * which authorises every URI registered on that OAuth client. Compared on\n * origin plus path; query and fragment are ignored, as is a trailing slash.\n */\n allowedRedirectUris?: string[];\n /** When true, blocks all self-registration regardless of `allowRegistration`. */\n disableSelfRegistration?: boolean;\n /**\n * Opt-in: allow `POST /auth/anonymous` to mint a user without credentials.\n *\n * Off by default. See {@link isAnonymousAuthOpen} for why this is opt-in\n * rather than opt-out, and why `disableSelfRegistration` overrides it.\n */\n allowAnonymous?: boolean;\n /**\n * Auth hooks for customizing password hashing, credential\n * verification, lifecycle hooks, etc.\n */\n authHooks?: AuthHooks;\n /** Enable magic link (passwordless email) login. Requires email service. */\n enableMagicLink?: boolean;\n /**\n * Enable email one-time codes: `POST /auth/otp` and `/auth/otp/verify`.\n * Requires email to be configured, as magic link does.\n */\n enableEmailOtp?: boolean;\n /**\n * Opt-in httpOnly cookie mode for refresh tokens.\n *\n * When set, the refresh token is delivered as an `httpOnly`, `Secure`,\n * `SameSite` cookie instead of in the JSON response body. This\n * prevents XSS from stealing the long-lived refresh token.\n *\n * The access token remains in the JSON body so the client can use it\n * in `Authorization: Bearer` headers for API calls.\n *\n * **Requires** `credentials: \"include\"` on client-side fetch calls to\n * auth endpoints, and CORS must allow credentials (no `origin: \"*\"`).\n */\n cookieAuth?: CookieAuthConfig;\n /**\n * How long a refresh token stays usable after it has been rotated away,\n * in seconds. Default 10, matching GoTrue's `refresh_token_reuse_interval`.\n *\n * Rotation is only safe if the client is guaranteed to receive the\n * replacement, and no network guarantees that. A pod rolls mid-response, a\n * laptop suspends, a second tab boots at the same instant — and the client\n * is left holding a token the database has moved past. Within this window\n * that client is handed a fresh token of the same session instead of a\n * 401, which is the difference between a hiccup and being silently signed\n * out of an app you were using.\n *\n * Widen it if your clients are flaky or your deploys are long; the cost is\n * how long a captured token stays useful to someone who copied it.\n */\n refreshTokenReuseIntervalSeconds?: number;\n /**\n * Let a magic-link or email-code request for an address with no account\n * create one (no password, unverified until the link or code is used),\n * while registration is open. Off by default. See `passwordless-signup.ts`.\n */\n magicLinkCreatesUsers?: boolean;\n /**\n * What a refresh token presented after its reuse window does to its\n * session. See `RebaseAuthConfig.refreshTokenReuse`. Default `\"reject\"`.\n */\n refreshTokenReuse?: RefreshTokenReusePolicy;\n /**\n * Refuse password sign-in until the account's address is verified, and\n * register confirm-first: `POST /auth/register` answers the same \"check\n * your inbox\" whether or not the address already has an account, and\n * signs nobody in. Off by default. Needs email; the boot refuses it\n * without. See `RebaseAuthConfig.requireEmailVerification`.\n */\n requireEmailVerification?: boolean;\n}\n\n\n/** What a refresh token replayed after its reuse window does to its session. */\nexport type RefreshTokenReusePolicy = \"reject\" | \"revoke-session\";\nconst REFRESH_TOKEN_REUSE_POLICIES: readonly RefreshTokenReusePolicy[] = [\"reject\", \"revoke-session\"];\n\n/** How long an email-verification link stays usable. */\nexport const EMAIL_VERIFICATION_TTL_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Configuration for httpOnly refresh-token cookies.\n */\nexport interface CookieAuthConfig {\n /** Cookie name (default: \"__rb_refresh\"). */\n cookieName?: string;\n /** Cookie domain. Omit to use the current domain. */\n domain?: string;\n /** Cookie path (default: \"/\"). */\n path?: string;\n /** SameSite attribute (default: \"Lax\"). */\n sameSite?: \"Strict\" | \"Lax\" | \"None\";\n /**\n * The Secure flag. Defaults to `true`, and `false` is the only way off —\n * see `AUTH_COOKIE_SECURE`. It used to be inferred from the request\n * protocol, which reads `http` behind any TLS-terminating proxy, so a\n * months-lived credential travelled in cleartext in the commonest\n * production topology. Set it `false` only for a deployment genuinely\n * served over plain http.\n */\n secure?: boolean;\n}\n\n/**\n * Build the standard auth response, with the access token's expiry as `jwt` —\n * the backend that minted it — reckons it.\n */\nfunction buildAuthResponseWith(\n jwt: JwtService,\n user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; emailVerified?: boolean; isAnonymous?: boolean; metadata?: Record<string, unknown> | null },\n roleIds: string[],\n accessToken: string,\n refreshToken: string,\n providerId: string\n): AuthResponsePayload {\n return {\n user: {\n uid: user.id,\n email: user.email,\n displayName: user.displayName ?? null,\n photoURL: user.photoUrl ?? null,\n providerId,\n isAnonymous: user.isAnonymous ?? false,\n emailVerified: user.emailVerified ?? false,\n roles: roleIds,\n metadata: user.metadata ?? {}\n },\n tokens: {\n accessToken,\n refreshToken,\n accessTokenExpiresAt: jwt.getAccessTokenExpiry()\n }\n };\n}\n\n\n/**\n * What {@link CreateSessionAndTokens} is told about the sign-in it opens.\n *\n * `method` is what `providerId` says for the session from now on — stored on\n * its refresh token and carried across every rotation — so each door names\n * its own rather than leaving it to a default.\n */\nexport interface SessionOptions {\n /** `\"password\"`, `\"anonymous\"`, `\"magic-link\"`, `\"otp\"`, `\"mfa\"` or a provider id. */\n method: string;\n /** Only for the route that has just seen the second factor. */\n skipMfaGate?: boolean;\n aal?: \"aal1\" | \"aal2\";\n}\n\n/** Mint a session, as every sign-in door does. See `createSessionAndTokens`. */\nexport type CreateSessionAndTokens = (\n uid: string,\n userAgent: string,\n ipAddress: string,\n options: SessionOptions\n) => Promise<{ roleIds: string[]; accessToken: string; refreshToken: string }>;\n\n/**\n * Get password reset token expiry (1 hour from now)\n */\nfunction getPasswordResetExpiry(): Date {\n return new Date(Date.now() + 60 * 60 * 1000); // 1 hour\n}\n\nexport function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {\n // Refused at boot, by name: a misspelt policy would otherwise be the\n // default one, silently, which is the opposite of what was asked for.\n if (config.refreshTokenReuse !== undefined && !REFRESH_TOKEN_REUSE_POLICIES.includes(config.refreshTokenReuse)) {\n throw new Error(\n `auth.refreshTokenReuse is \"${String(config.refreshTokenReuse)}\"; it must be one of ` +\n `${REFRESH_TOKEN_REUSE_POLICIES.map(p => `\"${p}\"`).join(\", \")}.`\n );\n }\n if (config.refreshTokenReuseIntervalSeconds !== undefined\n && !(Number.isFinite(config.refreshTokenReuseIntervalSeconds) && config.refreshTokenReuseIntervalSeconds >= 0)) {\n throw new Error(`auth.refreshTokenReuseIntervalSeconds is ${String(config.refreshTokenReuseIntervalSeconds)}; give a number of seconds, 0 or more.`);\n }\n // Every registrant gets the default role, so it may carry nothing a\n // stranger should hold: not `admin`, and no declared role with an\n // admin-plane scope — from `users:write`, real `admin` is one edit away.\n // Whose keys sign, whose limits count, whose roles are judged: the\n // backend's own, or the process default's for a router built outside one.\n const jwt = config.jwt ?? processJwtService;\n const access = config.access ?? processAccessControl;\n const limiters: AuthLimiters = config.limiters ?? {\n default: defaultAuthLimiter,\n strict: strictAuthLimiter,\n recipientEmail: recipientEmailLimiter,\n verificationEmail: verificationEmailLimiter,\n named: createRateLimiter\n };\n const buildAuthResponse = (\n user: Parameters<typeof buildAuthResponseWith>[1],\n roleIds: string[],\n accessToken: string,\n refreshToken: string,\n providerId: string\n ): AuthResponsePayload => buildAuthResponseWith(jwt, user, roleIds, accessToken, refreshToken, providerId);\n\n if (config.defaultRole) {\n const declared = access.model.roles;\n const adminPlane = config.defaultRole === ADMIN_ROLE\n ? [\"every scope\"]\n : Object.prototype.hasOwnProperty.call(declared, config.defaultRole)\n ? declared[config.defaultRole].scopes.filter(scope => isAdminScope(parseScope(scope)?.scope ?? \"\"))\n : [];\n if (adminPlane.length > 0) {\n throw new Error(\n `CRITICAL SECURITY ERROR: defaultRole cannot be '${config.defaultRole}': every registrant would hold ` +\n `${adminPlane.join(\", \")}. Give the default role no admin-plane scopes, and use the ` +\n \"POST /admin/bootstrap endpoint to promote the initial administrator.\"\n );\n }\n }\n\n // Built here rather than per request: `resolveCaptchaVerifier` throws on a\n // configuration that is enabled but unusable, and that has to fail the boot.\n // A challenge that silently is not there is the one failure mode this\n // feature cannot have.\n const captcha = buildCaptchaMiddlewares(config.captcha);\n\n const router = new Hono<HonoEnv>();\n\n // Attach Rebase error handler to ensure ApiError exceptions are correctly\n // formatted instead of caught by Hono's default error handler.\n // Hono's onError does NOT propagate from parent to child routers.\n router.onError(errorHandler);\n\n const authRepo = config.authRepo;\n\n /**\n * `requireAuth`, plus the question of whether the session still exists.\n *\n * Plain `requireAuth` verifies the signature and the expiry and stops\n * there, so an access token stays good for its full hour after the account\n * behind it has signed out everywhere or reset its password — the\n * revocation watermark those write is checked on the data plane and was not\n * checked here. On the three self-service routes below that mattered\n * disproportionately: `/link/<provider>` attaches an OAuth identity to the\n * account, so a stolen token could be turned into a permanent way back in\n * AFTER the victim had done the one thing that is supposed to end it.\n */\n const requireLiveSession = createRequireAuth({ revocationRepo: authRepo });\n const { emailService, emailConfig, allowRegistration = false } = config;\n const ops = resolveAuthHooks(config.authHooks);\n\n /**\n * Apply the `transformAuthResponse` hook if provided.\n *\n * Errors are caught and logged — the untransformed response is returned\n * as a graceful fallback so auth never breaks due to a hook failure.\n */\n async function applyTransformHook(\n response: AuthResponsePayload,\n method: TransformAuthResponseContext[\"method\"],\n request: Request,\n uid: string\n ): Promise<AuthResponsePayload> {\n if (!ops.transformAuthResponse) return response;\n try {\n return await ops.transformAuthResponse(response, { uid, method, request });\n } catch (err) {\n logger.error(\"[AuthHooks] transformAuthResponse error\", {\n error: err instanceof Error ? err.message : err\n });\n return response;\n }\n }\n\n // ── Zod input schemas ──────────────────────────────────────────────\n const registerSchema = z.object({\n email: z.string().email(\"Invalid email address\").max(255),\n password: z.string().min(1, \"Password is required\").max(128),\n displayName: z.string().max(255).optional()\n });\n const loginSchema = z.object({\n email: z.string().email(\"Invalid email address\").max(255),\n password: z.string().min(1, \"Password is required\").max(128)\n });\n const forgotPasswordSchema = z.object({\n email: z.string().email(\"Invalid email address\").max(255)\n });\n const resetPasswordSchema = z.object({\n token: z.string().min(1, \"Token is required\"),\n password: z.string().min(1, \"Password is required\").max(128)\n });\n const verifyEmailSchema = z.object({\n token: z.string().min(1, \"Token is required\"),\n password: z.string().min(1).max(128).optional(),\n removeUnproven: z.boolean().optional()\n });\n const changePasswordSchema = z.object({\n oldPassword: z.string().min(1, \"Old password is required\").max(128),\n newPassword: z.string().min(1, \"New password is required\").max(128)\n });\n const refreshSchema = z.object({\n // Always optional. The token may arrive in the body or, under cookieAuth,\n // in an httpOnly cookie, so \"is a token present\" cannot be decided at the\n // schema — `readRefreshToken` decides it, and the handler answers a\n // missing one with 401 NO_SESSION. Making the body field required in\n // non-cookie mode meant an anonymous visitor (who sends no body) got a\n // 400 INVALID_INPUT logged at warn on every page load, when the honest\n // answer is simply \"not signed in\".\n //\n // And no `.min(1)`: an empty string is how this server's own cookie-mode\n // responses spell \"the cookie has it\", so a client echoing `\"\"` back is\n // presenting no body token, not a malformed one. Refusing it here\n // answered 400 before `readRefreshToken` could read the cookie, which\n // broke every in-tab refresh under cookieAuth; it now treats `\"\"` as\n // absent, wherever it comes from.\n refreshToken: z.string().optional()\n });\n // `logoutSchema` and `updateProfileSchema` were declared here and used by\n // nothing: `/auth/logout` and `PATCH /auth/me` live in `session-routes.ts`,\n // which declares its own and applies them. Two copies of a validation rule,\n // one of them unreachable, is a rule that will be tightened in the wrong\n // file some day.\n\n /** Parse a Zod schema against the request body, throwing ApiError on failure */\n function parseBody<T>(schema: z.ZodSchema<T>, body: unknown): T {\n const result = schema.safeParse(body);\n if (!result.success) {\n const messages = result.error.issues.map(e => `${e.path.join(\".\")}: ${e.message}`).join(\". \");\n throw ApiError.badRequest(messages, \"INVALID_INPUT\");\n }\n return result.data;\n }\n\n /**\n * Check if email service is configured\n */\n function isEmailConfigured(): boolean {\n return !!(emailService && emailService.isConfigured());\n }\n\n /**\n * Whether registration is open without consulting the user table.\n *\n * The rule lives in `registration-policy.ts` and is shared with both config\n * endpoints, so what this route enforces and what they advertise cannot\n * drift apart — which is exactly how the empty-database dead end happened.\n *\n * `false` here does not mean \"refuse\": it means the answer depends on\n * whether the table is empty, which `POST /auth/register` checks only at\n * that point, because it serves anonymous callers and a count per rejected\n * attempt is a free hit on the database.\n */\n function isRegistrationAllowed(): boolean {\n return isSteadyStateRegistrationOpen({\n disableSelfRegistration: config.disableSelfRegistration,\n allowRegistration\n });\n }\n\n /**\n * Send welcome email to a newly registered user (fire-and-forget).\n */\n function sendWelcomeEmail(user: { email: string; displayName?: string | null }) {\n // An X account's placeholder address reaches nobody.\n if (!isEmailConfigured() || !isDeliverableAddress(user.email)) return;\n const { appName, logoUrl } = resolveEmailBranding(emailConfig);\n const loginUrl = resolveEmailLinkBase(emailConfig, \"resetPassword\"); // reuse base URL → the login / app page\n const templateFn = emailConfig?.templates?.welcomeEmail;\n const emailContent = templateFn\n ? templateFn(user, appName)\n // The app's own address. `<base>/app` is a route no frontend has —\n // the CMS answered it with its not-found page, after sign-in —\n // and it was the button in every self-registered user's first mail.\n : getWelcomeEmailTemplate(user, appName, loginUrl || undefined, logoUrl);\n\n emailService!.send({\n to: user.email,\n subject: emailContent.subject,\n html: emailContent.html,\n text: emailContent.text\n }).catch(err => {\n logger.error(\"Failed to send welcome email\", { error: err instanceof Error ? err.message : err });\n });\n }\n\n /**\n * Mail a verification link for `user`'s address, and record its token.\n *\n * Sent at registration as well as on request, so a self-registered\n * account proves its address on its owner's terms — and keeps the\n * password its owner chose, which a later first proof by link or code\n * would otherwise have to remove (see `confirmAddressOwnership`).\n * Returns whether a mail was handed to the email service.\n */\n async function sendVerificationMail(user: { id: string; email: string; displayName?: string | null }): Promise<boolean> {\n if (!isEmailConfigured() || !isDeliverableAddress(user.email)) return false;\n const token = generateSecureToken();\n await authRepo.setVerificationToken(user.id, hashToken(token));\n\n // `verifyEmailUrl` is set by no boot path, so this used to be `\"\"` — a\n // relative href, dead in every mail client. The resolver falls back to\n // the reset base, and `createEmailService` refuses to boot when neither\n // is absolute.\n const baseUrl = resolveEmailLinkBase(emailConfig, \"verifyEmail\");\n const verifyUrl = `${baseUrl}/verify-email?token=${token}`;\n const { appName, logoUrl } = resolveEmailBranding(emailConfig);\n const templateFn = emailConfig?.templates?.emailVerification;\n const emailContent = templateFn\n ? templateFn(verifyUrl, { email: user.email, displayName: user.displayName })\n : getEmailVerificationTemplate(verifyUrl, { email: user.email, displayName: user.displayName }, appName, logoUrl);\n await emailService!.send({\n to: user.email,\n subject: emailContent.subject,\n html: emailContent.html,\n text: emailContent.text\n });\n return true;\n }\n\n /** {@link sendVerificationMail}, not awaited: a failure is logged, never answered. */\n function sendVerificationMailInBackground(user: { id: string; email: string; displayName?: string | null }): void {\n sendVerificationMail(user).catch((err: unknown) => {\n logger.error(\"Failed to send verification email\", { error: err instanceof Error ? err.message : err });\n });\n }\n\n /**\n * A hash of nothing anyone knows, made with this deployment's own\n * `hashPassword`, so verifying against it costs what verifying a real\n * password costs. Made once, on first use.\n */\n let dummyHash: Promise<string> | undefined;\n function dummyPasswordHash(): Promise<string> {\n dummyHash ??= ops.hashPassword(randomBytes(24).toString(\"hex\"));\n return dummyHash;\n }\n\n /**\n * Helper to generate and store session tokens.\n *\n * Every route that signs somebody in comes through here — password login,\n * register, each OAuth provider, magic link, anonymous and anonymous-link —\n * which is why the second-factor gate is *inside* it rather than repeated at\n * each call site. An account with a verified factor gets no session from\n * this function at all: it throws `MFA_REQUIRED`, and the only thing that\n * mints a session for that user is `POST /auth/mfa/challenge/verify`.\n *\n * `skipMfaGate` exists for exactly one caller: the challenge-verify route\n * itself, which has just seen the second factor and mints at `aal2`.\n */\n async function createSessionAndTokens(\n uid: string,\n userAgent: string,\n ipAddress: string,\n options: SessionOptions\n ) {\n if (!options.skipMfaGate) {\n await assertMfaSatisfied(authRepo, uid, jwt);\n }\n const aal = options.aal ?? \"aal1\";\n const roleIds = await authRepo.getUserRoleIds(uid);\n\n // Is this session a GUEST — anonymous sign-in rather than an account?\n //\n // It goes into the token because everything that needs it downstream\n // has no database: the RLS identity carries it into\n // `rebase.is_anonymous()`, and the WebSocket path has only the token.\n // Read once here rather than per request.\n //\n // The lookup below already happens whenever a custom-claims hook is\n // configured; this is the same read, hoisted so it happens either way.\n const sessionUser = await authRepo.getUserById(uid);\n const isAnonymous = sessionUser?.isAnonymous === true;\n // Every door that signs somebody in comes through here, so a disabled\n // account is refused on all of them at once.\n if (sessionUser?.disabled) {\n throw ApiError.forbidden(\"This account has been disabled. Contact an administrator.\", \"ACCOUNT_DISABLED\");\n }\n\n // Allow customization of access token claims via hook\n let customClaims: Record<string, unknown> | undefined;\n if (ops.customizeAccessToken) {\n const user = sessionUser;\n if (user) {\n const defaultClaims: Record<string, unknown> = { uid,\nroles: roleIds,\naal };\n customClaims = await ops.customizeAccessToken(defaultClaims, user);\n }\n }\n\n // A sign-in opens a session; every token later rotated out of it\n // inherits this id and start time. `startedAt` is what\n // `tokens_valid_after` is judged against, so it must NOT advance on\n // rotation — otherwise a session could stay one step ahead of a\n // revocation forever simply by refreshing. The access token carries\n // the id too (`sid`), so signing this one device out reaches it.\n const sessionId = randomUUID();\n const accessToken = await jwt.generateAccessToken(uid, roleIds, aal, customClaims, isAnonymous, sessionId);\n const refreshToken = generateRefreshToken();\n\n await authRepo.createRefreshToken(\n uid,\n await hashRefreshToken(refreshToken),\n jwt.getRefreshTokenExpiry(),\n userAgent,\n ipAddress,\n { id: sessionId, startedAt: new Date(), aal, method: options.method }\n );\n\n return { roleIds,\naccessToken,\nrefreshToken };\n }\n\n /**\n * POST /auth/register\n * Create a new account with email/password\n */\n router.post(\"/register\", limiters.default, ...(captcha.register ? [captcha.register] : []), async (c) => {\n const startedAt = Date.now();\n const { email, password, displayName } = parseBody(registerSchema, await c.req.json());\n\n // Hard kill switch — blocks registration regardless of allowRegistration,\n // including the empty-database bootstrap exception below.\n if (config.disableSelfRegistration) {\n throw ApiError.forbidden(\"Registration is disabled\", \"REGISTRATION_DISABLED\");\n }\n\n // Bootstrap exception: an empty user table always admits the first\n // registration, even with `allowRegistration: false` — see\n // isRegistrationAllowed() for why refusing here is a dead end. The\n // moment one user exists the flag is enforced again.\n let bootstrapRegistration = false;\n if (!isRegistrationAllowed()) {\n // One id at most, on purpose: this runs for anonymous callers, and\n // anything that counts or lists the table would hand them a\n // full-table scan per rejected attempt.\n bootstrapRegistration = await isUserTableEmpty(authRepo);\n if (!bootstrapRegistration) {\n throw ApiError.forbidden(\"Registration is disabled\", \"REGISTRATION_DISABLED\");\n }\n // An empty table admits the first registration because someone has\n // to become the admin — but only where that someone is the person\n // at the keyboard. In production the operator names the admin\n // instead (`isBootstrapWindowOpen`), so there is nothing to admit.\n if (!isBootstrapWindowOpen()) {\n throw ApiError.forbidden(SETUP_REQUIRED_MESSAGE, \"SETUP_REQUIRED\");\n }\n }\n\n // Validate password strength\n const passwordValidation = ops.validatePasswordStrength(password);\n if (!passwordValidation.valid) {\n throw ApiError.badRequest(passwordValidation.errors.join(\". \"), \"WEAK_PASSWORD\");\n }\n\n // Confirm-first registration answers the same whether or not the\n // address has an account, held to the same floor as every route that\n // mails an address, so neither the words nor the time say which. An\n // existing account that never confirmed gets its link again — the\n // registrant whose first mail went missing — and its password is not\n // touched: whoever follows the link proves which password they know.\n const confirmFirst = () => notBefore(startedAt, RECIPIENT_ROUTE_FLOOR_MS, c.json({\n success: true,\n confirmationRequired: true,\n message: \"Check your inbox: follow the link we sent to confirm your address, then sign in.\"\n }));\n\n // Check if email already exists\n const existingUser = await authRepo.getUserByEmail(email);\n if (existingUser) {\n if (config.requireEmailVerification) {\n if (!existingUser.emailVerified) sendVerificationMailInBackground(existingUser);\n return confirmFirst();\n }\n throw ApiError.conflict(\"Email already registered\", \"EMAIL_EXISTS\");\n }\n\n // Create user\n const passwordHash = await ops.hashPassword(password);\n let createData: import(\"./interfaces\").CreateUserData = {\n email: normalizeEmail(email),\n passwordHash,\n displayName: displayName || undefined\n };\n if (ops.beforeUserCreate) {\n createData = await ops.beforeUserCreate(createData);\n }\n const user = await authRepo.createUser(createData);\n\n // Auto-bootstrap: if this is the very first user in the system, promote to admin.\n // This avoids the chicken-and-egg problem where the first user has no permissions\n // and no way to access the bootstrap endpoint from the UI.\n const isFirstUser = await isOnlyUser(authRepo, user.id);\n\n if (bootstrapRegistration && !isFirstUser) {\n // Two registrations raced through the empty-table check. Only the\n // genuine first user may ride the bootstrap exception — the loser\n // would otherwise become a regular account created while\n // registration is disabled. Undo it and report the gate.\n await authRepo.deleteUser(user.id);\n throw ApiError.forbidden(\"Registration is disabled\", \"REGISTRATION_DISABLED\");\n }\n\n if (isFirstUser && isBootstrapWindowOpen()) {\n await authRepo.setUserRoles(user.id, [\"admin\"]);\n } else {\n if (isFirstUser) {\n // Registration was open, so the first account was created as an\n // ordinary one. The boot log already said how to get an admin.\n logger.warn(\"[Auth] First account registered in production without promotion; \" +\n \"set REBASE_ADMIN_EMAIL/REBASE_ADMIN_PASSWORD or use the service key to assign admin.\");\n }\n if (config.defaultRole) {\n // Assign configured default role (never auto-assign admin via registration)\n await authRepo.assignDefaultRole(user.id, config.defaultRole);\n }\n }\n\n // The address is proven by its owner from here, at registration, on\n // the owner's terms: followed while signed in, the link keeps the\n // password they just chose. Left for later, the first proof was a\n // magic link or an email code, which has to remove a password nobody\n // proved — the owner's own included.\n if (config.requireEmailVerification) {\n // Confirm-first: no session until the address is proven.\n sendVerificationMailInBackground(user);\n if (ops.afterUserCreate) {\n try {\n await ops.afterUserCreate(user);\n } catch (err) {\n logger.error(\"[AuthHooks] afterUserCreate error\", { error: err instanceof Error ? err.message : err });\n }\n }\n return confirmFirst();\n }\n\n const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(\n user.id,\n c.req.header(\"user-agent\") || \"unknown\",\n requestClientAddress(c),\n { method: \"password\" }\n );\n\n // Send welcome email (fire-and-forget, don't block registration)\n sendWelcomeEmail({ email: user.email,\ndisplayName: user.displayName });\n sendVerificationMailInBackground(user);\n\n // Fire afterUserCreate hook\n if (ops.afterUserCreate) {\n try {\n await ops.afterUserCreate(user);\n } catch (err) {\n logger.error(\"[AuthHooks] afterUserCreate error\", { error: err instanceof Error ? err.message : err });\n }\n }\n\n // Fire onAuthenticated hook (fire-and-forget)\n if (ops.onAuthenticated) {\n ops.onAuthenticated(user, \"register\").catch(err => {\n logger.error(\"[AuthHooks] onAuthenticated error\", { error: err instanceof Error ? err.message : err });\n });\n }\n\n const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken, \"password\");\n const transformedResponse = await applyTransformHook(authResponse, \"register\", c.req.raw, user.id);\n const finalResponse = redactRefreshToken(transformedResponse, c, refreshToken, config.cookieAuth);\n return c.json(finalResponse, 201);\n });\n\n /**\n * POST /auth/login\n * Login with email/password\n */\n router.post(\"/login\", limiters.default, ...(captcha.login ? [captcha.login] : []), async (c) => {\n const { email, password } = parseBody(loginSchema, await c.req.json());\n\n // Call beforeLogin hook if provided (throw to reject)\n if (ops.beforeLogin) {\n await ops.beforeLogin(email, \"login\");\n }\n\n let user;\n\n if (ops.verifyCredentials) {\n // Full credential verification override\n user = await ops.verifyCredentials(email, password, authRepo);\n if (!user) {\n throw ApiError.unauthorized(\"Invalid email or password\", \"INVALID_CREDENTIALS\");\n }\n } else {\n // Default: email lookup + password hash verification\n user = await authRepo.getUserByEmail(email);\n if (!user || !user.passwordHash) {\n // One key derivation either way, so the time to answer does\n // not say whether the address has an account. Refusing before\n // any hash work answered an unknown address ~15× faster than a\n // wrong password, which is the question every other route that\n // takes an address is careful not to answer.\n await ops.verifyPassword(password, await dummyPasswordHash());\n throw ApiError.unauthorized(\"Invalid email or password\", \"INVALID_CREDENTIALS\");\n }\n\n const isValidPassword = await ops.verifyPassword(password, user.passwordHash);\n if (!isValidPassword) {\n logger.warn(\"[Security Audit] Auth login failure\", {\n eventType: \"auth.login.failure\",\n email,\n uid: user.id\n });\n throw ApiError.unauthorized(\"Invalid email or password\", \"INVALID_CREDENTIALS\");\n }\n }\n\n // Only after the password is proven, so the answer tells nobody but\n // its holder that the address is unconfirmed.\n if (config.requireEmailVerification && !user.emailVerified) {\n // Mailed again, but not more than once a minute however often\n // the holder tries.\n const lastSent = user.emailVerificationSentAt?.getTime() ?? 0;\n if (Date.now() - lastSent > 60_000) sendVerificationMailInBackground(user);\n // Not `EMAIL_NOT_VERIFIED`: that is the OAuth sign-in's refusal to\n // link onto an unverified account, and a login screen says\n // something else for it.\n throw ApiError.forbidden(\n \"Confirm your email address first: follow the link we sent to it, then sign in.\",\n \"EMAIL_NOT_CONFIRMED\"\n );\n }\n\n const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(\n user.id,\n c.req.header(\"user-agent\") || \"unknown\",\n requestClientAddress(c),\n { method: \"password\" }\n );\n\n // Fire onAuthenticated hook (fire-and-forget)\n if (ops.onAuthenticated) {\n ops.onAuthenticated(user, \"login\").catch(err => {\n logger.error(\"[AuthHooks] onAuthenticated error\", { error: err instanceof Error ? err.message : err });\n });\n }\n\n logger.info(\"[Security Audit] Auth login success\", {\n eventType: \"auth.login.success\",\n uid: user.id,\n email\n });\n\n const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken, \"password\");\n const transformedResponse = await applyTransformHook(authResponse, \"login\", c.req.raw, user.id);\n const finalResponse = redactRefreshToken(transformedResponse, c, refreshToken, config.cookieAuth);\n return c.json(finalResponse);\n });\n\n /**\n * Turn an OAuth request body into the provider's profile of the caller.\n *\n * Both OAuth doors come through here — sign-in and `/link/<provider>` —\n * because both end with that provider identity resolving to an account,\n * so both need the same two controls. The link door used to spend the\n * code without the first and echo the provider's error without the second:\n * a victim's code leaked through a stale registered URI could be linked to\n * the attacker's account, after which the victim's own \"Sign in with …\"\n * landed there.\n *\n * The redirect URI is allowlisted before the code is spent. The provider's\n * own registered-URI match authorises *every* URI on that OAuth client — a\n * leftover localhost entry, a staging host, a second product sharing the\n * client id — and any of them can mint a code this backend would otherwise\n * accept.\n *\n * A provider's error message is logged, never returned: its token-endpoint\n * error body routinely echoes the client_id, the redirect URI and\n * diagnostics about the credential state.\n */\n async function verifyProviderPayload(provider: OAuthProvider<unknown>, payload: unknown): Promise<OAuthProviderProfile> {\n const requestedRedirectUri = typeof payload === \"object\" && payload !== null && \"redirectUri\" in payload\n ? payload.redirectUri\n : undefined;\n if (typeof requestedRedirectUri === \"string\"\n && !isRedirectUriAllowed(requestedRedirectUri, config.allowedRedirectUris)) {\n throw ApiError.badRequest(\n \"redirectUri is not allowed for this backend\",\n \"REDIRECT_URI_NOT_ALLOWED\"\n );\n }\n\n let externalUser: OAuthProviderProfile | null;\n try {\n externalUser = await provider.verify(payload);\n } catch (err: unknown) {\n logger.error(`[OAuth] ${provider.id} verification threw`, {\n error: err instanceof Error ? err.message : String(err)\n });\n throw ApiError.unauthorized(`Invalid ${provider.id} credentials`, \"OAUTH_ERROR\");\n }\n if (!externalUser) {\n throw ApiError.unauthorized(`Invalid ${provider.id} credentials`, \"INVALID_TOKEN\");\n }\n return externalUser;\n }\n\n /**\n * Dynamically mount OAuth provider routes\n */\n if (config.oauthProviders && config.oauthProviders.length > 0) {\n for (const provider of config.oauthProviders) {\n router.post(`/${provider.id}`, limiters.default, async (c) => {\n const payload = parseBody(provider.schema, await c.req.json());\n const externalUser = await verifyProviderPayload(provider, payload);\n\n // A sign-in like any other, so the hook a deployment uses to\n // refuse one hears about it — it fired for passwords alone.\n if (ops.beforeLogin) {\n await ops.beforeLogin(normalizeEmail(externalUser.email), \"oauth\");\n }\n\n // Find or create user\n let user = await authRepo.getUserByIdentity(provider.id, externalUser.providerId);\n\n if (!user) {\n // Check if email exists (link accounts)\n user = await authRepo.getUserByEmail(externalUser.email);\n\n if (user) {\n const decision = decideOAuthAutoLink({\n providerEmailVerified: externalUser.emailVerified,\n existingUser: user\n });\n if (!decision.allowed) {\n // The message is for whoever reads the API response,\n // and names the endpoint. A login screen shows its\n // own words instead, chosen by `details.reason`.\n throw new ApiError(\n 403,\n \"EMAIL_NOT_VERIFIED\",\n `An account with this email already exists with a different sign-in method. ${provider.id} has not verified this email address, so it cannot be linked automatically. Sign in with your existing method, then POST to /auth/link/${provider.id} to link ${provider.id} to your account.`,\n {\n reason: decision.reason,\n provider: provider.id\n }\n );\n }\n if (decision.provesAddress) {\n // The provider vouched for an address nobody had\n // proven on this account. That is the owner proving\n // it, so it settles the account as an email code\n // does — the password and every identity whose\n // provider did not vouch go, every session ends —\n // before this identity is linked and signed in.\n // A store that cannot detach refuses with 409 here.\n await confirmAddressOwnership(authRepo, user, null);\n user = { ...user, emailVerified: true, passwordHash: null };\n }\n // Link Provider to existing account\n await authRepo.linkUserIdentity(user.id, provider.id, externalUser.providerId, identityProfileData(externalUser));\n\n // Optional: Update profile info from external provider if empty\n await authRepo.updateUser(user.id, {\n displayName: user.displayName || externalUser.displayName || undefined,\n photoUrl: user.photoUrl || externalUser.photoUrl || undefined\n });\n } else {\n // Creating an account through an OAuth button is still\n // registration, and used to be the one account-creating\n // path that consulted no policy at all — kill switch\n // included, first-user-becomes-admin included.\n //\n // The bare \"Registration is disabled\" that POST\n // /auth/register returns is a non-sequitur here: nobody\n // pressed a Create-account button, they pressed Sign in\n // with Google and there is no account behind that\n // identity. Say both halves, since the visitor can see\n // neither.\n const noSignupsMessage = `No account exists for this ${provider.id} identity, and new sign-ups are disabled on this backend. Ask an administrator to create your account.`;\n if (config.disableSelfRegistration) {\n throw ApiError.forbidden(noSignupsMessage, \"REGISTRATION_DISABLED\");\n }\n let bootstrapRegistration = false;\n if (!isRegistrationAllowed()) {\n bootstrapRegistration = await isUserTableEmpty(authRepo);\n if (!bootstrapRegistration) {\n throw ApiError.forbidden(noSignupsMessage, \"REGISTRATION_DISABLED\");\n }\n // Same rule as POST /auth/register: the empty-table\n // exception exists for a laptop, not a hostname.\n if (!isBootstrapWindowOpen()) {\n throw ApiError.forbidden(SETUP_REQUIRED_MESSAGE, \"SETUP_REQUIRED\");\n }\n }\n\n // Create new user. `emailVerified` was computed for the\n // link decision above and used to be discarded here, so\n // every OAuth account sat unverified forever — and was\n // then exactly the unverified local account the link\n // decision refuses to trust.\n // `beforeUserCreate` runs here too: a deployment that\n // limits sign-ups to its own domain wrote it for every\n // account, and an OAuth button created them unasked.\n let createData: CreateUserData = {\n email: normalizeEmail(externalUser.email),\n displayName: externalUser.displayName || undefined,\n photoUrl: externalUser.photoUrl || undefined,\n emailVerified: externalUser.emailVerified === true\n };\n if (ops.beforeUserCreate) {\n createData = await ops.beforeUserCreate(createData);\n }\n user = await authRepo.createUser(createData);\n\n await authRepo.linkUserIdentity(user.id, provider.id, externalUser.providerId, identityProfileData(externalUser));\n\n // Fire afterUserCreate hook\n if (ops.afterUserCreate) {\n try {\n await ops.afterUserCreate(user);\n } catch (err) {\n logger.error(\"[AuthHooks] afterUserCreate error\", { error: err instanceof Error ? err.message : err });\n }\n }\n\n // Auto-bootstrap: first user in the system gets admin\n const isFirstUser = await isOnlyUser(authRepo, user.id);\n\n if (bootstrapRegistration && !isFirstUser) {\n // Two sign-ups raced through the empty-table check;\n // same undo as POST /auth/register.\n await authRepo.deleteUser(user.id);\n throw ApiError.forbidden(noSignupsMessage, \"REGISTRATION_DISABLED\");\n }\n\n if (isFirstUser && isBootstrapWindowOpen()) {\n await authRepo.setUserRoles(user.id, [\"admin\"]);\n } else if (config.defaultRole) {\n // Assign configured default role (never auto-assign admin via registration)\n await authRepo.assignDefaultRole(user.id, config.defaultRole);\n }\n\n // Send welcome email for new OAuth users (fire-and-forget)\n sendWelcomeEmail({ email: user.email,\ndisplayName: user.displayName });\n }\n } else {\n // Update profile info from external provider\n await authRepo.updateUser(user.id, {\n displayName: externalUser.displayName || user.displayName || undefined,\n photoUrl: externalUser.photoUrl || user.photoUrl || undefined\n });\n }\n\n const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(\n user.id,\n c.req.header(\"user-agent\") || \"unknown\",\n requestClientAddress(c),\n { method: provider.id }\n );\n\n if (ops.onAuthenticated) {\n const signedIn = user;\n ops.onAuthenticated(signedIn, \"oauth\").catch(err => {\n logger.error(\"[AuthHooks] onAuthenticated error\", { error: err instanceof Error ? err.message : err });\n });\n }\n\n const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken, provider.id);\n const transformedResponse = await applyTransformHook(authResponse, \"oauth\", c.req.raw, user.id);\n const finalResponse = redactRefreshToken(transformedResponse, c, refreshToken, config.cookieAuth);\n return c.json(finalResponse);\n });\n\n /**\n * POST /auth/link/:provider\n * Attach an OAuth identity to the *already authenticated* account.\n *\n * This is the escape hatch from the `EMAIL_NOT_VERIFIED` rejection\n * on the sign-in route above, and the way to attach a provider\n * whose email differs from the account's.\n *\n * Note the deliberate asymmetry with sign-in: linking here does\n * NOT require the provider to have verified the email, and does\n * not require the emails to match at all. On the sign-in route the\n * provider's email is the *only* evidence tying the incoming\n * identity to an existing account, so an unverified address would\n * let an attacker claim someone else's account. Here the caller\n * has already proven ownership by holding a valid session, and the\n * OAuth credential proves control of the provider identity — the\n * email plays no part in the decision, so its verification status\n * is irrelevant.\n */\n router.post(`/link/${provider.id}`, limiters.default, requireLiveSession, async (c) => {\n const userCtx = c.get(\"user\") as { uid: string } | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n\n const payload = parseBody(provider.schema, await c.req.json());\n const externalUser = await verifyProviderPayload(provider, payload);\n\n // Refuse to attach an identity that already belongs to someone\n // else — one provider identity must resolve to exactly one\n // user, or the sign-in lookup above becomes ambiguous.\n const identityOwner = await authRepo.getUserByIdentity(provider.id, externalUser.providerId);\n if (identityOwner && identityOwner.id !== userCtx.uid) {\n throw ApiError.conflict(\n `That ${provider.id} account is already linked to a different user.`,\n \"IDENTITY_ALREADY_LINKED\"\n );\n }\n if (identityOwner) {\n // Already linked to this same user — idempotent success.\n return c.json({\n success: true,\n provider: provider.id,\n alreadyLinked: true,\n photoURL: externalUser.photoUrl ?? null\n });\n }\n\n // A guest that links a provider is becoming an account — the\n // same step `/anonymous/link` takes with a password — so it\n // answers to registration's controls and comes out of it an\n // account: not a guest, with the provider's address when the\n // provider vouches for it, and a session that says so. Linked\n // and left a guest, every later sign-in with that provider\n // returned the guest, and `rebase.is_anonymous()` kept refusing it.\n const linking = await authRepo.getUserById(userCtx.uid);\n if (linking?.isAnonymous) {\n if (!isRegistrationAllowed()) {\n throw ApiError.forbidden(\"A guest cannot become an account here: registration is disabled on this backend.\", \"REGISTRATION_DISABLED\");\n }\n const vouchedEmail = externalUser.emailVerified === true ? normalizeEmail(externalUser.email) : undefined;\n if (vouchedEmail && await authRepo.getUserByEmail(vouchedEmail)) {\n throw ApiError.conflict(\n `An account with this ${provider.id} address already exists. Sign in to it instead.`,\n \"EMAIL_EXISTS\"\n );\n }\n let upgrade: CreateUserData = {\n email: vouchedEmail ?? linking.email,\n ...(vouchedEmail ? { emailVerified: true } : {})\n };\n if (ops.beforeUserCreate) upgrade = await ops.beforeUserCreate(upgrade);\n await authRepo.linkUserIdentity(userCtx.uid, provider.id, externalUser.providerId, identityProfileData(externalUser));\n const upgraded = await authRepo.updateUser(userCtx.uid, {\n email: normalizeEmail(upgrade.email),\n ...(upgrade.emailVerified !== undefined ? { emailVerified: upgrade.emailVerified } : {}),\n isAnonymous: false\n });\n if (!upgraded) throw ApiError.notFound(\"User not found\");\n const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(\n upgraded.id,\n c.req.header(\"user-agent\") || \"unknown\",\n requestClientAddress(c),\n { method: provider.id }\n );\n const authResponse = buildAuthResponse(upgraded, roleIds, accessToken, refreshToken, provider.id);\n return c.json({\n ...redactRefreshToken(authResponse, c, refreshToken, config.cookieAuth),\n success: true,\n provider: provider.id,\n alreadyLinked: false,\n photoURL: externalUser.photoUrl ?? null\n });\n }\n\n await authRepo.linkUserIdentity(\n userCtx.uid,\n provider.id,\n externalUser.providerId,\n identityProfileData(externalUser)\n );\n\n return c.json({\n success: true,\n provider: provider.id,\n alreadyLinked: false,\n // The profile picture the provider just handed us, passed\n // back rather than dropped.\n //\n // `verify` fetches it on every provider that has one, and\n // sign-in uses it: both branches above call `updateUser`\n // with `externalUser.photoUrl`. This route asked for the\n // same profile, got the same answer and threw it away, so\n // an app whose users link an account *after* signing up had\n // no route to a picture at all — which is not a smaller\n // case than sign-in, it is the normal one for any product\n // where the account came first.\n //\n // Returned rather than stored, deliberately. These URLs are\n // signed and expire (LinkedIn's `media.licdn.com` links\n // within weeks), so a column holding one is a column that\n // silently turns into a broken image later. The caller gets\n // it while it is still good and decides what to do — copy\n // the bytes somewhere it owns, or nothing.\n photoURL: externalUser.photoUrl ?? null\n });\n });\n }\n }\n\n /**\n * POST /auth/forgot-password\n * Request password reset email\n */\n router.post(\n \"/forgot-password\",\n limiters.strict,\n // Per-address as well as per-IP. `strictAuthLimiter` bounds a machine;\n // an IP is the attacker's to rotate and the mailbox being filled is\n // not, so without this a distributed run puts an unbounded number of\n // reset messages into one person's inbox, from this deployment's\n // domain. See `recipientEmailLimiter`.\n captureRecipientEmail,\n limiters.recipientEmail,\n ...(captcha.forgotPassword ? [captcha.forgotPassword] : []),\n async (c) => {\n const startedAt = Date.now();\n const { email } = parseBody(forgotPasswordSchema, await c.req.json());\n\n // Check if email service is configured\n if (!isEmailConfigured()) {\n throw ApiError.serviceUnavailable(\"Email service not configured. Password reset is not available.\", \"EMAIL_NOT_CONFIGURED\");\n }\n\n // Always return success (security: don't reveal if email exists)\n // But only send email if user exists\n const user = await authRepo.getUserByEmail(email);\n\n if (user) {\n // Generate reset token\n const token = generateSecureToken();\n const tokenHash = hashToken(token);\n const expiresAt = getPasswordResetExpiry();\n\n await authRepo.createPasswordResetToken(user.id, tokenHash, expiresAt);\n\n // Build reset URL\n const baseUrl = resolveEmailLinkBase(emailConfig, \"resetPassword\");\n const resetUrl = `${baseUrl}/reset-password?token=${token}`;\n\n // Get email template\n const { appName, logoUrl } = resolveEmailBranding(emailConfig);\n const templateFn = emailConfig?.templates?.passwordReset;\n const emailContent = templateFn\n ? templateFn(resetUrl, { email: user.email,\ndisplayName: user.displayName })\n : getPasswordResetTemplate(resetUrl, { email: user.email,\ndisplayName: user.displayName }, appName, logoUrl);\n\n // Deliberately NOT awaited.\n //\n // An SMTP round trip is the largest and most variable thing this\n // handler does, and it only happens on the branch where the address\n // exists — so awaiting it made the response TIME the answer to the\n // question the response TEXT refuses to answer. A failure was\n // already withheld from the caller for the same reason; the only\n // change is that the log line arrives after the response.\n void emailService!.send({\n to: user.email,\n subject: emailContent.subject,\n html: emailContent.html,\n text: emailContent.text\n }).catch((emailError: unknown) => {\n logger.error(\"Failed to send password reset email\", { error: emailError instanceof Error ? emailError.message : emailError });\n });\n }\n\n // Held to a floor, so a known address and an unknown one take the same\n // time to answer. See `RECIPIENT_ROUTE_FLOOR_MS`.\n return notBefore(startedAt, RECIPIENT_ROUTE_FLOOR_MS, c.json({\n success: true,\n message: \"If an account with that email exists, a password reset link has been sent.\"\n }));\n });\n\n /**\n * POST /auth/reset-password\n * Reset password using token\n */\n router.post(\"/reset-password\", limiters.strict, async (c) => {\n const { token, password } = parseBody(resetPasswordSchema, await c.req.json());\n\n // Validate password strength\n const passwordValidation = ops.validatePasswordStrength(password);\n if (!passwordValidation.valid) {\n throw ApiError.badRequest(passwordValidation.errors.join(\". \"), \"WEAK_PASSWORD\");\n }\n\n // Find valid token\n const tokenHash = hashToken(token);\n const storedToken = await authRepo.findValidPasswordResetToken(tokenHash);\n\n if (!storedToken) {\n throw ApiError.badRequest(\"Invalid or expired reset token\", \"INVALID_TOKEN\");\n }\n\n // Update the password and log out every session. The whole point of a\n // reset is that someone else may be holding a token and actively\n // refreshing it; see `replaceUserPassword` for why deleting the rows\n // alone does not catch them.\n //\n // The link was read out of the inbox, so on an account whose address\n // nobody had proven this is the owner proving it: the identities\n // somebody else may have signed up with go too, and the account is\n // verified — see `confirmAddressOwnership`. It is also how an invited\n // user accepts the invitation.\n const passwordHash = await ops.hashPassword(password);\n const account = await authRepo.getUserById(storedToken.uid);\n if (account && !account.emailVerified) {\n await confirmAddressOwnership(authRepo, account, passwordHash);\n } else {\n await replaceUserPassword(authRepo, storedToken.uid, passwordHash);\n }\n\n // Mark token as used\n await authRepo.markPasswordResetTokenUsed(tokenHash);\n\n if (ops.onAuthenticated && account) {\n ops.onAuthenticated(account, \"password-reset\").catch(err => {\n logger.error(\"[AuthHooks] onAuthenticated error\", { error: err instanceof Error ? err.message : err });\n });\n }\n\n // Fire onPasswordReset hook (fire-and-forget)\n if (ops.onPasswordReset) {\n ops.onPasswordReset(storedToken.uid).catch(err => {\n logger.error(\"[AuthHooks] onPasswordReset error\", { error: err instanceof Error ? err.message : err });\n });\n }\n\n return c.json({ success: true,\nmessage: \"Password has been reset successfully\" });\n });\n\n /**\n * POST /auth/change-password\n * Change password for authenticated user\n */\n router.post(\"/change-password\", requireLiveSession, async (c) => {\n const userCtx = c.get(\"user\") as { uid: string; roles?: string[]; aal?: \"aal1\" | \"aal2\" } | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n\n const { oldPassword, newPassword } = parseBody(changePasswordSchema, await c.req.json());\n\n // Get user\n const user = await authRepo.getUserById(userCtx.uid);\n if (!user || !user.passwordHash) {\n throw ApiError.badRequest(\"Cannot change password for this account\", \"INVALID_ACCOUNT\");\n }\n\n // Verify old password\n const isValidOldPassword = await ops.verifyPassword(oldPassword, user.passwordHash);\n if (!isValidOldPassword) {\n throw ApiError.unauthorized(\"Current password is incorrect\", \"INVALID_CREDENTIALS\");\n }\n\n // Validate new password strength\n const passwordValidation = ops.validatePasswordStrength(newPassword);\n if (!passwordValidation.valid) {\n throw ApiError.badRequest(passwordValidation.errors.join(\". \"), \"WEAK_PASSWORD\");\n }\n\n // Update the password and log out every session, this one included —\n // then sign this device back in. Every other device has to sign in\n // with the new password, which is why the password was changed; this\n // one just proved the old password, so it gets a fresh session at the\n // level it already had, rather than a success answer and a session the\n // server had just revoked (which signed the app out on its next call).\n const passwordHash = await ops.hashPassword(newPassword);\n await replaceUserPassword(authRepo, user.id, passwordHash);\n\n const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(\n user.id,\n c.req.header(\"user-agent\") || \"unknown\",\n requestClientAddress(c),\n { skipMfaGate: true, aal: userCtx.aal === \"aal2\" ? \"aal2\" : \"aal1\", method: \"password\" }\n );\n const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken, \"password\");\n return c.json({\n ...redactRefreshToken(authResponse, c, refreshToken, config.cookieAuth),\n success: true,\n message: \"Password has been changed successfully\"\n });\n });\n\n /**\n * POST /auth/send-verification\n * Send email verification link (authenticated)\n *\n * Two limiters, because they bound different things: `strictAuthLimiter`\n * bounds the caller by IP as it does on every other email-sending route, and\n * `verificationEmailLimiter` bounds how much mail one account — i.e. one\n * recipient address — can be made to receive. Authentication is not a bound\n * here: the address is unverified by construction, so an attacker can hold a\n * session for a mailbox that is not theirs.\n */\n router.post(\"/send-verification\", limiters.strict, requireLiveSession, limiters.verificationEmail, async (c) => {\n const userCtx = c.get(\"user\") as { uid: string; roles?: string[] } | undefined;\n if (!userCtx) {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n\n // Check if email service is configured\n if (!isEmailConfigured()) {\n throw ApiError.serviceUnavailable(\"Email service not configured. Email verification is not available.\", \"EMAIL_NOT_CONFIGURED\");\n }\n\n const user = await authRepo.getUserById(userCtx.uid);\n if (!user) {\n throw ApiError.notFound(\"User not found\");\n }\n\n if (user.emailVerified) {\n throw ApiError.badRequest(\"Email is already verified\", \"ALREADY_VERIFIED\");\n }\n if (!isDeliverableAddress(user.email)) {\n throw ApiError.badRequest(\"This account has no address mail can reach. Set a real email address first.\", \"UNDELIVERABLE_ADDRESS\");\n }\n\n await sendVerificationMail(user);\n\n return c.json({ success: true,\nmessage: \"Verification email sent\" });\n });\n\n /**\n * Who proved what, when a verification link is followed.\n *\n * The link proves the inbox. It does not prove whoever registered the\n * address is that inbox's owner: anyone can register someone else's\n * address, leave a password on it, and ask for the link to be mailed. So a\n * first proof of address keeps only what the person following the link\n * also proved — and takes the rest off, as a magic link, an email code or\n * a reset does (`confirmAddressOwnership`):\n *\n * - `session`: the request carries a live session of this very account.\n * Only whoever put the credentials on it can hold one, and they now hold\n * the inbox too — nothing on the account is a stranger's.\n * - `password`: the request carries the account's password.\n * - `none`: the password and every identity whose provider did not vouch\n * for the address are removed, and every session ends.\n */\n async function verificationProof(c: Context<HonoEnv>, user: { id: string; passwordHash?: string | null }, password: string | undefined): Promise<\"session\" | \"password\" | \"none\"> {\n const bearer = extractBearerToken(c.req.header(\"authorization\"));\n if (bearer !== undefined) {\n const payload = await jwt.verifyAccessToken(bearer);\n if (payload?.uid === user.id && (await judgeAccessToken(authRepo, payload)).live) return \"session\";\n }\n if (password !== undefined) {\n if (!user.passwordHash || !(await ops.verifyPassword(password, user.passwordHash))) {\n throw ApiError.unauthorized(\"That is not this account's password.\", \"INVALID_CREDENTIALS\");\n }\n return \"password\";\n }\n return \"none\";\n }\n\n /** The account a verification token names, if the token is live. */\n async function accountForVerificationToken(token: string) {\n const user = await authRepo.getUserByVerificationToken(hashToken(token));\n const sentAt = user?.emailVerificationSentAt?.getTime();\n if (!user || (sentAt !== undefined && Date.now() - sentAt > EMAIL_VERIFICATION_TTL_MS)) {\n throw ApiError.badRequest(\"Invalid or expired verification token\", \"INVALID_TOKEN\");\n }\n return user;\n }\n\n /**\n * GET /auth/verify-email?token=\n * Verify an address by its link. Keeps what the request proves — a live\n * session of the account — and removes what nobody proved. See\n * `verificationProof`; `POST` is the same with a password and a choice.\n */\n router.get(\"/verify-email\", limiters.strict, async (c) => {\n const token = c.req.query(\"token\");\n if (!token) {\n throw ApiError.badRequest(\"Verification token is required\", \"INVALID_INPUT\");\n }\n const user = await accountForVerificationToken(token);\n const proof = await verificationProof(c, user, undefined);\n const outcome = await confirmAddressOwnership(authRepo, user, null, { everythingProven: proof === \"session\" });\n return c.json({\n success: true,\n message: \"Email verified successfully\",\n passwordRemoved: outcome.removedPassword,\n removedProviders: outcome.removedProviders\n });\n });\n\n /**\n * POST /auth/verify-email { token, password?, removeUnproven? }\n *\n * The same proof as `GET`, with the two things a link alone cannot say:\n *\n * - `password` proves the account's password, which is then kept, and\n * signs the caller in — they have just proven both the address and the\n * password. This is how a confirm-first registration completes.\n * - With neither a session nor a password, an account carrying a password\n * or an identity nobody proved is answered `409 PROOF_REQUIRED` and the\n * token is left unspent, so the person holding the link can choose:\n * prove the password, or `removeUnproven: true` to verify without it.\n */\n router.post(\"/verify-email\", limiters.strict, async (c) => {\n const { token, password, removeUnproven } = parseBody(verifyEmailSchema, await c.req.json());\n const user = await accountForVerificationToken(token);\n const proof = await verificationProof(c, user, password);\n\n if (proof === \"none\" && !removeUnproven) {\n const unprovenProviders = (await authRepo.getUserIdentities(user.id))\n .filter(identity => !identityVouchesForAddress(identity, user.email))\n .map(identity => identity.provider);\n if (user.passwordHash || unprovenProviders.length > 0) {\n throw new ApiError(409, \"PROOF_REQUIRED\",\n \"This account has a way to sign in that was set before its address was verified. \" +\n \"Enter its password to keep it, or verify without it and it is removed.\",\n { password: Boolean(user.passwordHash), providers: unprovenProviders });\n }\n }\n\n const outcome = await confirmAddressOwnership(\n authRepo,\n user,\n proof === \"password\" ? user.passwordHash ?? null : null,\n { everythingProven: proof === \"session\" }\n );\n\n if (proof !== \"password\") {\n return c.json({\n success: true,\n message: \"Email verified successfully\",\n passwordRemoved: outcome.removedPassword,\n removedProviders: outcome.removedProviders\n });\n }\n\n // Address and password both proven: a sign-in. Through the same\n // session minting every sign-in uses, the MFA gate included.\n const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(\n user.id,\n c.req.header(\"user-agent\") || \"unknown\",\n requestClientAddress(c),\n { method: \"password\" }\n );\n const verified = { ...user, emailVerified: true };\n const authResponse = buildAuthResponse(verified, roleIds, accessToken, refreshToken, \"password\");\n const transformedResponse = await applyTransformHook(authResponse, \"login\", c.req.raw, user.id);\n return c.json({\n ...redactRefreshToken(transformedResponse, c, refreshToken, config.cookieAuth),\n success: true,\n passwordRemoved: false,\n removedProviders: outcome.removedProviders\n });\n });\n\n /**\n * POST /auth/refresh\n * Refresh access token using refresh token\n */\n router.post(\"/refresh\", async (c) => {\n // Cookie mode sends NO body: the refresh token lives in the httpOnly\n // cookie, which is the entire point of it. Hono's `json()` throws\n // \"Unexpected end of JSON input\" on an empty body, so parsing it\n // unguarded turned every cookie-mode refresh into a 500 — and clients\n // refresh on page load, so sessions never restored and users were\n // signed out on every reload.\n //\n // `refreshToken` is already optional under cookieAuth and\n // `readRefreshToken` already falls back to the cookie; this is only\n // what makes that reachable.\n const body = await c.req.json().catch(() => ({}));\n const parsed = parseBody(refreshSchema, body);\n const refreshToken = readRefreshToken(c, parsed, config.cookieAuth);\n\n if (!refreshToken) {\n // Presenting no token at all is not a malformed request — it is an\n // unauthenticated one, and the overwhelmingly common case is a\n // first-time visitor, because clients refresh on page load before\n // they know whether a session exists. Answering 400 INVALID_INPUT\n // told them their request was wrong and logged a warning for every\n // anonymous page view. The invalid- and expired-token branches below\n // already answer 401; having no token is the same class of thing.\n throw ApiError.unauthenticated(\"No refresh token presented\", \"NO_SESSION\");\n }\n\n const tokenHash = await hashRefreshToken(refreshToken);\n const storedToken = await authRepo.findRefreshTokenByHash(tokenHash);\n\n if (!storedToken) {\n // Deliberately NOT clearing the cookie. A token we do not recognise\n // means we cannot authenticate THIS request; it does not mean the\n // credential in the browser should be destroyed. Clearing here made\n // any single stale request — a duplicate from a second tab, a retry\n // after a rolled pod — permanently sign out a user whose session was\n // otherwise perfectly alive, because the good cookie went with it.\n throw ApiError.unauthorized(\"Invalid refresh token\", \"INVALID_TOKEN\");\n }\n\n if (storedToken.revoked) {\n // A hard kill: logout, a remotely revoked device, an admin action.\n // Unlike the case above this session really is over, so clearing is\n // correct — there is nothing left for the cookie to authenticate.\n clearRefreshCookie(c, config.cookieAuth);\n throw ApiError.unauthorized(\"Session has been revoked\", \"SESSION_REVOKED\");\n }\n\n if (new Date() > storedToken.expiresAt) {\n await authRepo.deleteRefreshToken(tokenHash);\n clearRefreshCookie(c, config.cookieAuth);\n throw ApiError.unauthorized(\"Refresh token expired\", \"TOKEN_EXPIRED\");\n }\n\n // A session that began before the user's revocation mark is void, even\n // if its row survived. This closes the race in which a refresh already\n // in flight inserts a rotated token microseconds after a password reset\n // has deleted every row it could see.\n const sessionStartedAt = storedToken.sessionStartedAt ?? storedToken.createdAt;\n const tokensValidAfter = await authRepo.getTokensValidAfter?.(storedToken.uid).catch(() => null);\n if (tokensValidAfter && sessionStartedAt < tokensValidAfter) {\n await authRepo.deleteRefreshToken(tokenHash);\n clearRefreshCookie(c, config.cookieAuth);\n throw ApiError.unauthorized(\"Session has been revoked\", \"SESSION_REVOKED\");\n }\n\n // A token that was already rotated away is the normal signature of a\n // client that never received the answer — a response lost to a deploy,\n // a suspended laptop, two tabs booting together. Inside the reuse window\n // it earns a fresh token of the same session rather than a 401.\n const reuseWindowMs = Math.max(0, config.refreshTokenReuseIntervalSeconds ?? 10) * 1000;\n const supersededAt = storedToken.rotatedAt ? new Date(storedToken.rotatedAt) : null;\n if (supersededAt && Date.now() - supersededAt.getTime() > reuseWindowMs) {\n // A token replayed long after it was superseded is either a\n // straggler — a client that lost an answer and slept — or a copy\n // someone else is using. Which risk to take is the deployment's\n // call (`refreshTokenReuse`):\n //\n // - \"reject\" (default): decline the request, leave the session\n // standing. Its live token is still good, and punishing its\n // holder for a late straggler is how a legitimate user gets\n // signed out. This is not reuse *detection*: a thief who\n // refreshes first keeps the session and the owner is the one\n // refused.\n // - \"revoke-session\": end the whole sign-in, both holders included,\n // as GoTrue does — the owner signs in again, the thief is out.\n //\n // Logged either way: it is what a stolen token looks like.\n const revoke = config.refreshTokenReuse === \"revoke-session\";\n logger.warn(\"[Security Audit] Refresh token replayed after the reuse window\", {\n eventType: \"auth.refresh.reuse\",\n uid: storedToken.uid,\n sessionId: storedToken.sessionId,\n supersededSecondsAgo: Math.round((Date.now() - supersededAt.getTime()) / 1000),\n userAgent: c.req.header(\"user-agent\") || \"unknown\",\n sessionRevoked: revoke\n });\n if (revoke) {\n if (storedToken.sessionId && authRepo.revokeRefreshTokenSession) {\n await authRepo.revokeRefreshTokenSession(storedToken.sessionId);\n } else {\n await authRepo.deleteRefreshToken(tokenHash);\n }\n clearRefreshCookie(c, config.cookieAuth);\n throw ApiError.unauthorized(\"Refresh token already used; this session has been ended\", \"SESSION_REVOKED\");\n }\n throw ApiError.unauthorized(\"Refresh token already used\", \"TOKEN_ALREADY_USED\");\n }\n\n // Generate new tokens\n const roleIds = await authRepo.getUserRoleIds(storedToken.uid);\n\n // Best-effort: load the user so we can return it in the response, which\n // lets the client restore a session from an httpOnly cookie alone (cold\n // start in cookie mode). This enrichment must NEVER break refresh — on\n // any failure we fall back to a tokens-only response (the pre-existing\n // behavior), and the client restores the user via GET /me.\n const user = await authRepo.getUserById(storedToken.uid).catch((err: unknown) => {\n logger.warn(\"[Auth] Could not load user during token refresh; returning tokens only\", {\n uid: storedToken.uid,\n error: err instanceof Error ? err.message : String(err)\n });\n return null;\n });\n\n // A disabled account keeps no session: refresh is how a session lives\n // past its access token, so it ends here too.\n if (user?.disabled) {\n await authRepo.deleteRefreshToken(tokenHash);\n clearRefreshCookie(c, config.cookieAuth);\n throw ApiError.unauthorized(\"This account has been disabled\", \"ACCOUNT_DISABLED\");\n }\n\n // The assurance level is a property of the sign-in, and rotation is not\n // a new sign-in — so it is read off the presented token's row and\n // carried forward. Hardcoding `aal1` here meant an MFA-verified session\n // silently dropped back to password-grade within one access-token\n // lifetime, which both defeats the step-up and locks the user out of\n // the operations that require it. A row with no stored value (written\n // before the column existed, or by a repository that does not keep it)\n // reads as `aal1`, the restrictive value.\n const sessionAal: \"aal1\" | \"aal2\" = storedToken.aal === \"aal2\" ? \"aal2\" : \"aal1\";\n // How the session was signed in, carried the same way and for the same\n // reason: a refresh is not a sign-in. A row from before the column\n // existed says nothing, and reads as the password sign-in it most\n // likely was — which is what every refresh used to answer.\n const sessionMethod = storedToken.method ?? \"password\";\n\n // Allow customization of access token claims via hook\n let customClaims: Record<string, unknown> | undefined;\n if (ops.customizeAccessToken && user) {\n const defaultClaims: Record<string, unknown> = { uid: storedToken.uid,\nroles: roleIds,\naal: sessionAal };\n customClaims = await ops.customizeAccessToken(defaultClaims, user);\n }\n\n // The guest flag is carried across a rotation, from the user row\n // rather than from the old token: a session that WAS anonymous and has\n // since been upgraded to a real account should stop being a guest at\n // its next refresh, not at its next sign-in.\n const sessionId = storedToken.sessionId ?? storedToken.id;\n const newAccessToken = await jwt.generateAccessToken(\n storedToken.uid, roleIds, sessionAal, customClaims, user?.isAnonymous === true, sessionId\n );\n const newRefreshToken = generateRefreshToken();\n\n // Rotate: mark the presented token superseded and mint its successor\n // into the same session. Note the ORDER and the absence of a delete —\n // the old row has to survive, because it is the only thing that can\n // tell a client replaying it apart from a stranger.\n //\n // If the presented token was ALREADY superseded (a replay inside the\n // reuse window, allowed above), leave the existing rotation stamp alone\n // and simply add a sibling: two tabs then hold two live tokens of one\n // session, which is exactly what we want them to have.\n const userAgent = c.req.header(\"user-agent\") || \"unknown\";\n const ipAddress = requestClientAddress(c);\n const session = {\n id: sessionId,\n startedAt: sessionStartedAt,\n aal: sessionAal,\n method: sessionMethod\n };\n\n if (!supersededAt) {\n if (authRepo.markRefreshTokenRotated) {\n await authRepo.markRefreshTokenRotated(tokenHash);\n } else {\n // Repository from an older release: no way to record the\n // supersession, so fall back to the lossy delete.\n await authRepo.deleteRefreshToken(tokenHash);\n }\n }\n\n // Written only while the presented token is still live: a sign-out\n // that landed after the checks above has revoked this session, and a\n // token written into it anyway would bring it back — in cookie mode by\n // re-setting the cookie. The repository decides that in the database\n // (see `RefreshTokenSession.rotatedFrom`). Not asked when the presented\n // row was deleted above for want of `markRefreshTokenRotated`: there is\n // no row left to hold the write to.\n const presentedKept = Boolean(supersededAt) || typeof authRepo.markRefreshTokenRotated === \"function\";\n const minted = await authRepo.createRefreshToken(\n storedToken.uid,\n await hashRefreshToken(newRefreshToken),\n jwt.getRefreshTokenExpiry(),\n userAgent,\n ipAddress,\n presentedKept ? { ...session, rotatedFrom: tokenHash } : session\n );\n if (minted === false) {\n clearRefreshCookie(c, config.cookieAuth);\n throw ApiError.unauthorized(\"Session has been revoked\", \"SESSION_REVOKED\");\n }\n\n // Housekeeping, deliberately after the new token exists and never\n // allowed to fail the request: rotation adds a row per refresh, and\n // nothing else would ever remove them.\n authRepo.pruneRefreshTokens?.(\n storedToken.uid,\n session.id,\n new Date(Date.now() - reuseWindowMs)\n ).catch((err: unknown) => {\n logger.warn(\"[Auth] Refresh token prune failed\", {\n error: err instanceof Error ? err.message : String(err)\n });\n });\n\n const tokensOnlyResponse: AuthResponsePayload = {\n tokens: {\n accessToken: newAccessToken,\n refreshToken: newRefreshToken,\n accessTokenExpiresAt: jwt.getAccessTokenExpiry()\n }\n };\n let refreshResponse: AuthResponsePayload = tokensOnlyResponse;\n if (user) {\n try {\n refreshResponse = buildAuthResponse(user, roleIds, newAccessToken, newRefreshToken, sessionMethod);\n } catch (err: unknown) {\n logger.warn(\"[Auth] Could not build enriched refresh response; returning tokens only\", {\n error: err instanceof Error ? err.message : String(err)\n });\n refreshResponse = tokensOnlyResponse;\n }\n }\n if (ops.onAuthenticated && user) {\n ops.onAuthenticated(user, \"refresh\").catch(err => {\n logger.error(\"[AuthHooks] onAuthenticated error\", { error: err instanceof Error ? err.message : err });\n });\n }\n const transformedResponse = await applyTransformHook(refreshResponse, \"refresh\", c.req.raw, storedToken.uid);\n const finalResponse = redactRefreshToken(transformedResponse, c, newRefreshToken, config.cookieAuth);\n return c.json(finalResponse);\n });\n\n // ═══════════════════════════════════════════════════════════════════════\n // Self-service email change\n // ═══════════════════════════════════════════════════════════════════════\n mountEmailChangeRoutes({ router, config, ops, parseBody, requireLiveSession, limiters });\n\n mountSessionRoutes({\n router,\n config,\n jwt,\n limiters,\n ops,\n parseBody,\n buildAuthResponse,\n createSessionAndTokens,\n applyTransformHook,\n requireLiveSession,\n registerCaptcha: captcha.register,\n sendVerificationMail: sendVerificationMailInBackground\n });\n\n // ═══════════════════════════════════════════════════════════════════════\n // MFA / TOTP\n // ═══════════════════════════════════════════════════════════════════════\n mountMfaRoutes({\n router,\n config,\n jwt,\n limiters,\n ops,\n parseBody,\n buildAuthResponse,\n createSessionAndTokens,\n applyTransformHook,\n requireLiveSession\n });\n\n // ═══════════════════════════════════════════════════════════════════════\n // Magic Link (passwordless email login)\n // ═══════════════════════════════════════════════════════════════════════\n if (config.enableMagicLink) {\n mountMagicLinkRoutes({\n router,\n config,\n limiters,\n ops,\n parseBody,\n buildAuthResponse,\n createSessionAndTokens,\n applyTransformHook,\n captchaMiddleware: captcha.magicLink\n });\n }\n\n // ═══════════════════════════════════════════════════════════════════════\n // Email one-time codes (six digits, for devices a link cannot reach)\n // ═══════════════════════════════════════════════════════════════════════\n if (config.enableEmailOtp) {\n mountOtpRoutes({\n router,\n config,\n limiters,\n ops,\n parseBody,\n buildAuthResponse,\n createSessionAndTokens,\n applyTransformHook,\n captchaMiddleware: captcha.emailOtp\n });\n }\n\n return router;\n}\n","/**\n * The `GET /auth/config` payload, assembled in exactly one place.\n *\n * Two handlers used to claim that path — `init.ts` registers it directly and\n * only afterwards mounts the auth router, so the router's copy never ran — and\n * they returned *different shapes*: one reported `emailServiceEnabled` and\n * `magicLinkEnabled`, the other `passwordReset` and `magicLink`. A fix applied\n * to the shadowed copy therefore changed nothing, twice.\n *\n * The router's copy is gone. This module is what the surviving handler calls,\n * so the payload has one definition and one set of rules — the same move\n * `registration-policy.ts` makes for the predicate itself.\n */\n\nimport type { AuthAdapterCapabilities } from \"@rebasepro/types\";\nimport { isAnonymousAuthOpen, isRegistrationOpen } from \"./registration-policy\";\n\n/** Everything the built-in payload is derived from. */\nexport interface BuiltinAuthCapabilityInputs {\n /**\n * True when the user table is empty — the first-user bootstrap window.\n *\n * The caller resolves this, because how you count users is the adapter's\n * business and it is also reported verbatim as `needsSetup`.\n */\n needsSetup: boolean;\n /** Steady-state self-registration. */\n allowRegistration?: boolean;\n /** The hard kill switch: blocks registration and anonymous sign-in alike. */\n disableSelfRegistration?: boolean;\n /** Opt-in `POST /auth/anonymous`. */\n allowAnonymous?: boolean;\n /** Magic-link login. Needs an email service to be of any use. */\n enableMagicLink?: boolean;\n /** Email one-time codes. Needs an email service, as magic link does. */\n enableEmailOtp?: boolean;\n /**\n * Whether an email service is configured.\n *\n * Every email-borne flow hangs off this — password reset, verification and\n * magic link are advertised only when a message can actually be sent.\n */\n emailConfigured: boolean;\n /** OAuth provider ids the backend has wired, e.g. `[\"google\", \"github\"]`. */\n enabledProviders: string[];\n}\n\n/**\n * Build the capability document for the built-in auth adapter.\n *\n * Every flag is a *runtime* answer, not a compile-time feature list: what this\n * advertises, the matching route must accept. `registrationEnabled` and\n * `anonymousLogin` go through the shared predicates for exactly that reason.\n */\nexport function buildBuiltinAuthCapabilities(\n inputs: BuiltinAuthCapabilityInputs\n): AuthAdapterCapabilities {\n const {\n needsSetup,\n allowRegistration,\n disableSelfRegistration,\n allowAnonymous,\n enableMagicLink,\n enableEmailOtp,\n emailConfigured,\n enabledProviders\n } = inputs;\n\n return {\n hasBuiltInAuthRoutes: true,\n emailPasswordLogin: true,\n registrationEnabled: isRegistrationOpen({\n disableSelfRegistration,\n allowRegistration,\n needsSetup\n }),\n passwordReset: emailConfigured,\n // Always available: createAdminRoutes() unconditionally mounts the\n // reset-password route, which falls back to returning a one-time\n // temporary password when no email service is configured.\n adminPasswordReset: true,\n sessionManagement: true,\n profileUpdate: true,\n emailVerification: emailConfigured,\n magicLink: !!enableMagicLink && emailConfigured,\n emailOtp: !!enableEmailOtp && emailConfigured,\n // A capability that exists and is not in the capability surface is how a\n // client ends up calling a route it cannot discover is off.\n anonymousLogin: isAnonymousAuthOpen({ allowAnonymous, disableSelfRegistration }),\n enabledProviders,\n needsSetup\n };\n}\n","/**\n * Standalone admin endpoint for resetting a user's password.\n *\n * Hook resolution order:\n * 1. Collection-level hook (`auth.onResetPassword` on the collection)\n * 2. Backend-level hook (`AuthHooks.onAdminResetPassword`)\n * 3. Built-in default (send reset email, or generate temp password)\n *\n * Whichever of them runs, the account's existing sessions end, and a\n * `temporaryPassword` in its result becomes the account's password before the\n * response shows it to the admin.\n */\n\nimport { Hono } from \"hono\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport type { AuthRepository } from \"./interfaces\";\nimport { createRequireAuth } from \"./middleware\";\nimport { assertMayManageAccount, requireScope } from \"./access\";\nimport type { AuthHooks } from \"./auth-hooks\";\nimport { resolveAuthHooks } from \"./auth-hooks\";\nimport { generateSecurePassword, generateSecureToken, hashToken } from \"./admin-user-ops\";\nimport { replaceUserPassword, revokeAllSessions } from \"./token-revocation\";\nimport { getPasswordResetTemplate, resolveEmailBranding } from \"../email/templates\";\nimport { resolveEmailLinkBase } from \"../email/link-base\";\nimport type { EmailService, EmailConfig } from \"../email\";\nimport type { HonoEnv } from \"../api/types\";\nimport type { AuthCollectionConfig } from \"@rebasepro/types\";\nimport { logger } from \"../utils/logger\";\n\nexport interface ResetPasswordRouteConfig {\n authRepo: AuthRepository;\n emailService?: EmailService;\n emailConfig?: EmailConfig;\n serviceKey?: string;\n authHooks?: AuthHooks;\n /** The parsed auth config from the collection, if available. */\n collectionAuthConfig?: AuthCollectionConfig;\n}\n\n/**\n * Create a standalone admin route for resetting user passwords.\n *\n * Mounts: POST /users/:uid/reset-password\n */\nexport function createResetPasswordRoute(config: ResetPasswordRouteConfig): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n const authRepo = config.authRepo;\n const { emailService, emailConfig, collectionAuthConfig } = config;\n const ops = resolveAuthHooks(config.authHooks);\n\n router.onError(errorHandler);\n router.use(\"/*\", createRequireAuth({\n serviceKey: config.serviceKey,\n // A demoted admin must not still be able to reset anyone's password.\n resolveRoles: uid => authRepo.getUserRoleIds(uid),\n revocationRepo: authRepo\n }));\n\n router.post(\"/users/:uid/reset-password\", requireScope(\"users:write\"), async (c) => {\n const uid = c.req.param(\"uid\");\n const existing = await authRepo.getUserById(uid);\n if (!existing) {\n throw ApiError.notFound(\"User not found\");\n }\n // A reset hands the caller the account. Not one that outranks them.\n assertMayManageAccount(c, await authRepo.getUserRoleIds(uid));\n\n let invitationSent = false;\n let temporaryPassword: string | undefined;\n // Distinguishes \"email was never configured\" from \"email is configured but\n // the send failed\" — both fall back to a temporary password.\n let emailDeliveryFailed = false;\n\n // Parse optional body — if a password is provided, set it directly\n const body = await c.req.json().catch(() => ({}));\n const chosenPassword = body.password ? body.password as string : undefined;\n\n if (chosenPassword) {\n const validation = ops.validatePasswordStrength(chosenPassword);\n if (!validation.valid) {\n throw ApiError.badRequest(\n `Password too weak: ${validation.errors.join(\", \")}`,\n \"WEAK_PASSWORD\"\n );\n }\n }\n // 1. Collection-level hook (closest to the data)\n else if (collectionAuthConfig?.onResetPassword) {\n const isEmailConfigured = !!(emailService && emailService.isConfigured());\n const hookResult = await collectionAuthConfig.onResetPassword(existing.id, {\n hashPassword: (password: string) => ops.hashPassword(password),\n sendEmail: isEmailConfigured\n ? (options) => emailService!.send(options)\n : undefined,\n emailConfigured: isEmailConfigured,\n appName: emailConfig?.appName || \"Rebase\",\n resetPasswordUrl: emailConfig?.resetPasswordUrl || \"\"\n });\n temporaryPassword = hookResult.temporaryPassword;\n invitationSent = hookResult.invitationSent ?? false;\n }\n // 2. Backend-level hook (global override)\n else if (ops.onAdminResetPassword) {\n const hookResult = await ops.onAdminResetPassword(existing.id, {\n authRepo,\n emailService,\n emailConfig\n });\n temporaryPassword = hookResult.temporaryPassword;\n invitationSent = hookResult.invitationSent ?? false;\n }\n // 3. Built-in default\n else {\n const isEmailConfigured = !!(emailService && emailService.isConfigured());\n\n if (isEmailConfigured) {\n const token = generateSecureToken();\n const tokenHash = hashToken(token);\n const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour\n\n // Outside the `try` below, deliberately. Its `catch` answers\n // \"the email did not go out\" with a temporary password, which is\n // right for a failed send and wrong for a failed database write:\n // when minting the token fails, the honest answer is a 500 and\n // the password left as it was, not a new one nobody asked for.\n await authRepo.createPasswordResetToken(existing.id, tokenHash, expiresAt);\n\n try {\n // The resolver the forgot-password email uses — see the\n // same line in `finalizeAdminUserCreation`.\n const baseUrl = resolveEmailLinkBase(emailConfig, \"resetPassword\");\n const setPasswordUrl = `${baseUrl}/reset-password?token=${token}`;\n\n const { appName, logoUrl } = resolveEmailBranding(emailConfig);\n const templateFn = emailConfig?.templates?.passwordReset;\n const emailContent = templateFn\n ? templateFn(setPasswordUrl, { email: existing.email,\ndisplayName: existing.displayName })\n : getPasswordResetTemplate(setPasswordUrl, { email: existing.email,\ndisplayName: existing.displayName }, appName, logoUrl);\n\n await emailService!.send({\n to: existing.email,\n subject: emailContent.subject,\n html: emailContent.html,\n text: emailContent.text\n });\n invitationSent = true;\n } catch (emailError: unknown) {\n logger.error(\"Failed to send reset email\", { error: emailError instanceof Error ? emailError.message : emailError });\n // Fall back to returning the temporary password\n temporaryPassword = generateSecurePassword();\n emailDeliveryFailed = true;\n }\n } else {\n // No email service — generate password, set it, and return one-time\n temporaryPassword = generateSecurePassword();\n }\n }\n\n // The password this reset leaves the account with: the one the admin\n // typed, or the temporary one the response is about to show them —\n // whichever branch produced it, a hook's included. It is written here,\n // once. A hook's used to be shown and never written, and the collection\n // hook has no way to write one, so the admin handed over a password\n // that did not work. `undefined` means the password does not change\n // yet: a link went out, or a hook sent its own.\n const newPassword = chosenPassword ?? temporaryPassword;\n\n // An admin reset is what an administrator reaches for when an account\n // is compromised, so it ends every session the account holds, on every\n // branch — including the ones that set no password: an emailed link\n // changes nothing until the user opens it, and a stolen refresh token\n // would otherwise go on minting access tokens in the meantime.\n if (newPassword) {\n await replaceUserPassword(authRepo, existing.id, await ops.hashPassword(newPassword));\n } else {\n await revokeAllSessions(authRepo, existing.id);\n }\n\n const userRoles = await authRepo.getUserRoleIds(existing.id);\n\n return c.json({\n user: {\n uid: existing.id,\n email: existing.email,\n displayName: existing.displayName,\n roles: userRoles\n },\n invitationSent,\n ...(temporaryPassword ? { temporaryPassword } : {}),\n ...(emailDeliveryFailed ? { emailDeliveryFailed } : {})\n }, 200);\n });\n\n return router;\n}\n","/**\n * Admin endpoint for listing roles.\n *\n * Mounts: GET /roles — the built-in `admin` role and the roles the app\n * declares under `auth.roles` on the users collection, with the scopes each\n * holds. Requires `users:read`.\n */\n\nimport { Hono } from \"hono\";\nimport { summarizeRoles } from \"@rebasepro/types\";\nimport { errorHandler } from \"../api/errors\";\nimport type { AuthRepository } from \"./interfaces\";\nimport { createRequireAuth } from \"./middleware\";\nimport { accessOf, requireScope } from \"./access\";\nimport type { HonoEnv } from \"../api/types\";\n\nexport interface AdminRolesRouteConfig {\n authRepo: AuthRepository;\n serviceKey?: string;\n}\n\n/**\n * Create a standalone admin route for listing roles.\n *\n * Mounts: GET /roles\n */\nexport function createAdminRolesRoute(config: AdminRolesRouteConfig): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n const authRepo = config.authRepo;\n\n router.onError(errorHandler);\n router.use(\"/*\", createRequireAuth({\n serviceKey: config.serviceKey,\n resolveRoles: uid => authRepo.getUserRoleIds(uid),\n revocationRepo: authRepo\n }));\n\n router.get(\"/roles\", requireScope(\"users:read\"), (c) => {\n return c.json({ roles: summarizeRoles(accessOf(c).model) });\n });\n\n return router;\n}\n","/**\n * Who may read the user picker (`GET /api/admin/users/pick`).\n *\n * A `userSelect` field stores a user's id, and the person filling it in picks\n * that user from a list. Listing the users is an admin-plane read\n * (`users:read`), which an editor does not hold — so the picker answers a\n * narrower question for a wider audience: id, name, email and avatar, to\n * anyone who may write a collection that has such a field.\n *\n * \"May write\" is the collection's `securityRules` for an insert or an update,\n * evaluated the way the admin panel decides whether to offer the form, with\n * one difference. A rule that depends only on the row (an owner field) counts\n * as allowing, since the person may write some row. A rule written as SQL the\n * policy model cannot read cannot be judged ahead, and here it counts as\n * refusing: read optimistically, it would let any signed-in person list every\n * user's name and email. A key or token narrowed by scopes must also hold\n * `data:write` on that collection.\n *\n * The collections are the backend's own (`c.var.services`), read when the\n * question is asked, so a schema edited live is what is judged.\n *\n * @module\n */\nimport type { Context } from \"hono\";\nimport { checkOperation, securityRuleToConditions } from \"@rebasepro/common\";\nimport {\n isAnonymousUid,\n policy,\n type CollectionConfig,\n type PolicyExpression,\n type Properties,\n type Property,\n type SecurityRule,\n type User\n} from \"@rebasepro/types\";\nimport type { HonoEnv } from \"../api/types\";\nimport { hasScope } from \"./access\";\n\n/**\n * The collections the picker judges for a route mounted outside any backend:\n * the process default's. None until a backend is installed as the default, or\n * a test configures some.\n */\nlet collectionsForPicking: () => readonly CollectionConfig[] = () => [];\n\n/** Tell the process default's picker which collections it serves. */\nexport function configureUserPicking(collections: () => readonly CollectionConfig[]): void {\n collectionsForPicking = collections;\n}\n\n/** @internal The process default's collections getter, to snapshot or restore it. */\nexport function defaultUserPicking(): () => readonly CollectionConfig[] {\n return collectionsForPicking;\n}\n\nfunction propertyReferencesUsers(property: Property | undefined): boolean {\n if (!property) return false;\n if (property.type === \"string\" && property.userSelect === true) return true;\n if (property.type === \"map\") return propertiesReferenceUsers(property.properties);\n if (property.type === \"array\") {\n const of = property.of;\n if (Array.isArray(of)) return of.some(propertyReferencesUsers);\n if (of) return propertyReferencesUsers(of);\n return propertiesReferenceUsers(property.oneOf?.properties);\n }\n return false;\n}\n\n/** Whether any property, at any depth, is a `userSelect` field. */\nexport function propertiesReferenceUsers(properties: Properties | undefined): boolean {\n return !!properties && Object.values(properties).some(propertyReferencesUsers);\n}\n\n/** Whether any part of a policy is raw SQL the evaluator cannot decide. */\nfunction containsRawSql(expr: PolicyExpression | null): boolean {\n if (!expr) return false;\n switch (expr.kind) {\n case \"raw\":\n return true;\n case \"and\":\n case \"or\":\n return expr.operands.some(containsRawSql);\n case \"not\":\n return containsRawSql(expr.operand);\n case \"existsIn\":\n return containsRawSql(expr.where);\n default:\n return false;\n }\n}\n\n/**\n * The rule with each clause that holds raw SQL read as `false`, so an\n * undecidable rule never grants (permissive) and always refuses\n * (restrictive). The clauses are carried already desugared — roles folded\n * in — so nothing else in the rule is read again.\n */\nfunction failClosedOnRawSql(rule: SecurityRule): SecurityRule {\n const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);\n if (!containsRawSql(usingExpr) && !containsRawSql(withCheckExpr)) return rule;\n // A missing clause refuses (Postgres writes `false` for it), so `false`\n // stands in for one as well as for a raw clause.\n const clause = (expr: PolicyExpression | null): PolicyExpression =>\n expr === null || containsRawSql(expr) ? policy.false() : expr;\n return {\n operation: rule.operation,\n operations: rule.operations,\n mode: rule.mode,\n name: rule.name,\n condition: clause(usingExpr),\n check: clause(withCheckExpr)\n };\n}\n\n/** The signed-in person behind a request, as the policy evaluator reads one. */\nfunction personOf(c: Context<HonoEnv>): User | null {\n const user = c.get(\"user\");\n if (typeof user !== \"object\" || user === null) return null;\n const uid = typeof user.uid === \"string\" ? user.uid : undefined;\n if (!uid || isAnonymousUid(uid)) return null;\n if (\"isAnonymous\" in user && user.isAnonymous === true) return null;\n const roles = Array.isArray(user.roles) ? user.roles.filter((role): role is string => typeof role === \"string\") : [];\n return { uid, roles, isAnonymous: false, displayName: null, email: null, photoURL: null, providerId: \"custom\" };\n}\n\n/**\n * Whether this request's caller may read the user picker: they hold\n * `users:read`, or they may write a collection with a `userSelect` field.\n */\nexport function mayPickUsers(c: Context<HonoEnv>): boolean {\n if (hasScope(c, \"users:read\")) return true;\n const person = personOf(c);\n if (!person) return false;\n const collections = c.get(\"services\")?.collections ?? collectionsForPicking;\n return collections().some(collection => {\n if (!propertiesReferenceUsers(collection.properties) || !hasScope(c, \"data:write\", collection.slug)) return false;\n const judged = collection.securityRules\n ? { ...collection, securityRules: collection.securityRules.map(failClosedOnRawSql) }\n : collection;\n return checkOperation(judged, { user: person }, null, \"insert\")\n || checkOperation(judged, { user: person }, null, \"update\");\n });\n}\n","/**\n * Standalone admin endpoint for user management.\n *\n * Mounts:\n * GET /users\n * GET /users/pick\n * GET /users/:uid\n * POST /users\n * PUT /users/:uid\n * DELETE /users/:uid\n * DELETE /users/:uid/mfa\n * POST /bootstrap\n */\n\nimport { Hono } from \"hono\";\nimport { isAnonymousUid } from \"@rebasepro/types\";\nimport { isBootstrapWindowOpen, SETUP_REQUIRED_MESSAGE } from \"./registration-policy\";\nimport { normalizeEmail } from \"@rebasepro/common\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport type { AuthRepository } from \"./interfaces\";\nimport { createRequireAuth } from \"./middleware\";\nimport { assertMayGrantRoles, assertMayManageAccount, callerIsAdmin, readRoleNames, requireScope } from \"./access\";\nimport { mayPickUsers } from \"./user-picking\";\nimport type { AuthHooks } from \"./auth-hooks\";\nimport { resolveAuthHooks } from \"./auth-hooks\";\nimport {\n prepareAdminUserValues,\n finalizeAdminUserCreation,\n completeUserCreation,\n assertRoleChangesAllowed,\n assertUserDeletionsAllowed,\n normalizeUserEmailChange,\n runAfterUserDelete\n} from \"./admin-user-ops\";\nimport { replaceUserPassword, revokeAllSessions } from \"./token-revocation\";\nimport type { EmailService, EmailConfig } from \"../email\";\nimport type { HonoEnv } from \"../api/types\";\nimport type { AdminUser, AuthCollectionConfig, PickedUser } from \"@rebasepro/types\";\nimport { resolveListLimitParam } from \"../api/rest/query-parser\";\nimport { logger } from \"../utils/logger\";\n\nexport interface AdminUsersRouteConfig {\n authRepo: AuthRepository;\n emailService?: EmailService;\n emailConfig?: EmailConfig;\n serviceKey?: string;\n authHooks?: AuthHooks;\n collectionAuthConfig?: AuthCollectionConfig;\n}\n\n/** Upper bound for `GET /users?ids=…`, so one request can't fan out unbounded. */\nconst MAX_USER_IDS_PER_LOOKUP = 100;\n\nexport function createAdminUsersRoute(config: AdminUsersRouteConfig): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n const authRepo = config.authRepo;\n const { emailService, emailConfig, collectionAuthConfig } = config;\n const ops = resolveAuthHooks(config.authHooks);\n\n function toAdminUser(\n u: {\n id: string;\n email: string;\n displayName?: string | null;\n photoUrl?: string | null;\n createdAt?: Date | string;\n updatedAt?: Date | string;\n disabled?: boolean;\n },\n roles: string[]\n ): AdminUser {\n return {\n uid: u.id,\n email: u.email,\n displayName: u.displayName ?? null,\n photoURL: u.photoUrl ?? null,\n providerId: \"custom\",\n roles,\n disabled: u.disabled === true,\n createdAt: u.createdAt instanceof Date ? u.createdAt.toISOString() : (u.createdAt ?? new Date().toISOString()),\n updatedAt: u.updatedAt instanceof Date ? u.updatedAt.toISOString() : (u.updatedAt ?? new Date().toISOString())\n };\n }\n\n router.onError(errorHandler);\n router.use(\"/*\", createRequireAuth({\n serviceKey: config.serviceKey,\n // These routes can grant roles, so they must not trust a role claim.\n resolveRoles: uid => authRepo.getUserRoleIds(uid),\n revocationRepo: authRepo\n }));\n\n router.post(\"/bootstrap\", async (c) => {\n const user = c.get(\"user\");\n if (!user || typeof user !== \"object\") {\n throw ApiError.unauthorized(\"Not authenticated\");\n }\n\n // The gate is \"does this backend already have an admin\", asked of the\n // database below. There used to be a pair of `isBootstrapCompleted` /\n // `setBootstrapCompleted` hooks in front of it that no caller ever\n // supplied — a flag-shaped answer to a question the rows already answer.\n const users = await authRepo.listUsers();\n let hasAdmin = false;\n\n for (const u of users) {\n const roles = await authRepo.getUserRoleIds(u.id);\n if (roles.includes(\"admin\")) {\n hasAdmin = true;\n break;\n }\n }\n\n if (hasAdmin) {\n throw ApiError.forbidden(\"Admin users already exist. Bootstrap not allowed.\", \"BOOTSTRAP_COMPLETED\");\n }\n\n // \"Earliest registered user\" is the same race as \"first to register\",\n // one request later: on a public hostname with registration open, the\n // stranger who signs up first can claim this too. The window that\n // makes an empty laptop database usable is shut in production — see\n // `isBootstrapWindowOpen` — and the ways in there are the named admin\n // seed and the service key, neither of which anyone can race for.\n if (!isBootstrapWindowOpen()) {\n logger.warn(\"[Security Audit] Bootstrap denied: the first-admin window is closed in production\", {\n eventType: \"auth.bootstrap.denied.production\"\n });\n throw ApiError.forbidden(SETUP_REQUIRED_MESSAGE, \"SETUP_REQUIRED\");\n }\n\n const uid = \"uid\" in user ? (user as { uid: string }).uid : undefined;\n if (!uid) {\n throw ApiError.unauthorized(\"User ID not found in auth context\");\n }\n\n // An anonymous session may not claim the initial admin role.\n //\n // `POST /auth/anonymous` mints a real session for anyone who asks, and\n // an anonymous principal is a row in the users table like any other —\n // so on an empty backend it was also the *earliest* one, which is the\n // only thing the land-grab gate below checks. Two unauthenticated\n // requests therefore took a fresh deployment: anonymous session, then\n // bootstrap. It worked with `disableSelfRegistration: true` as well,\n // the flag whose docblock promises \"an empty backend has no\n // self-service path in at all\".\n //\n // A guest is recognised by its row's `isAnonymous` flag. The route\n // mints guests with random ids, so the reserved uids `isAnonymousUid`\n // knows never match one; checking only those left this guard matching\n // nothing a real guest could present.\n const refuseAnonymousCaller = (): never => {\n logger.warn(\"[Security Audit] Bootstrap denied: anonymous caller\", {\n eventType: \"auth.bootstrap.denied.anonymous\",\n callerId: uid\n });\n throw ApiError.forbidden(\n \"An anonymous session cannot claim the initial admin role. Register a real \" +\n \"account and bootstrap from it, or assign the admin role using the service key.\",\n \"BOOTSTRAP_ANONYMOUS\"\n );\n };\n if (isAnonymousUid(uid)) refuseAnonymousCaller();\n const caller = await authRepo.getUserById(uid);\n if (!caller) {\n throw ApiError.notFound(\"Authenticated user does not exist in the database.\", \"USER_NOT_FOUND\");\n }\n if (caller.isAnonymous === true) refuseAnonymousCaller();\n\n // Even while no admin exists, only the earliest-registered user may claim\n // the initial admin role. The common paths already auto-promote the first\n // user, so this endpoint only matters once a system has reached a \"users\n // exist but no admin\" state — e.g. concurrent first-registrations, or the\n // first user being deleted. Without this gate, any authenticated user\n // could then seize admin: a land-grab. The genuine first user is\n // deterministic; tie-break by id so identical timestamps still resolve to\n // a single winner.\n // Anonymous principals are excluded from \"earliest registered\" for the\n // same reason they cannot bootstrap: they are sessions, not\n // registrations. Leaving them in would also let an anonymous row that\n // happens to predate the real first user block that user forever.\n const registeredUsers = users.filter(u => u.isAnonymous !== true && !isAnonymousUid(u.id));\n if (registeredUsers.length > 0) {\n const earliest = registeredUsers.reduce((a, b) => {\n const at = new Date(a.createdAt).getTime();\n const bt = new Date(b.createdAt).getTime();\n if (at !== bt) return at < bt ? a : b;\n return a.id < b.id ? a : b;\n });\n if (earliest.id !== uid) {\n logger.warn(\"[Security Audit] Bootstrap denied: caller is not the earliest-registered user\", {\n eventType: \"auth.bootstrap.denied\",\n callerId: uid,\n earliestUserId: earliest.id\n });\n throw ApiError.forbidden(\n \"Only the first registered user may claim the initial admin role. \" +\n \"Ask that user to bootstrap, or assign the admin role using the service key.\",\n \"BOOTSTRAP_NOT_FIRST_USER\"\n );\n }\n }\n\n await authRepo.setUserRoles(uid, [\"admin\"]);\n logger.info(\"[Security Audit] Initial admin bootstrapped\", {\n eventType: \"auth.bootstrap.success\",\n uid\n });\n\n return c.json({\n success: true,\n message: \"You are now an admin\",\n user: {\n uid: uid,\n roles: [\"admin\"]\n }\n });\n });\n\n router.get(\"/users\", requireScope(\"users:read\"), async (c) => {\n // `?ids=a,b,c` resolves a known set of users in one round trip. The admin\n // UI needs it to turn the ids stored in `userSelect` columns into names\n // without firing one request per row.\n const idsParam = c.req.query(\"ids\");\n if (idsParam !== undefined) {\n const ids = [...new Set(idsParam.split(\",\").map(id => id.trim()).filter(Boolean))]\n .slice(0, MAX_USER_IDS_PER_LOOKUP);\n const resolved = await Promise.all(ids.map(async (id) => {\n const result = await authRepo.getUserWithRoles(id);\n return result ? toAdminUser(result.user, result.roles) : undefined;\n }));\n const users = resolved.filter((u): u is AdminUser => u !== undefined);\n return c.json({ users,\ntotal: users.length,\nlimit: ids.length,\noffset: 0 });\n }\n\n const limitParam = c.req.query(\"limit\");\n const offsetParam = c.req.query(\"offset\");\n const search = c.req.query(\"search\");\n const orderBy = c.req.query(\"orderBy\");\n const orderDir = c.req.query(\"orderDir\") as \"asc\" | \"desc\" | undefined;\n\n // Same bounded guarantee as every other list ingress: an absent limit\n // gets this endpoint's own 25 default, and one above the ceiling is a\n // 400 naming it rather than a shorter page that reads like the end of\n // the user list.\n const limit = resolveListLimitParam(limitParam, { defaultLimit: 25 });\n const offset = offsetParam ? Math.max(0, parseInt(offsetParam, 10) || 0) : 0;\n\n const result = await authRepo.listUsersPaginated({\n limit,\n offset,\n search: search || undefined,\n orderBy: orderBy || undefined,\n orderDir: orderDir || undefined,\n roleId: c.req.query(\"role\") || undefined\n });\n\n const usersWithRoles = await Promise.all(\n result.users.map(async (u) => {\n const roles = await authRepo.getUserRoleIds(u.id);\n return toAdminUser(u, roles);\n })\n );\n\n return c.json({\n users: usersWithRoles,\n total: result.total,\n limit: result.limit,\n offset: result.offset\n });\n });\n\n /**\n * The user picker's read: who a `userSelect` field can name, as id, name,\n * email and avatar — nothing an administrator's list carries beyond that.\n * Open to anyone who may write a collection with such a field, as well as\n * to `users:read` ({@link mayPickUsers}). Registered before `/users/:uid`,\n * which would otherwise take `pick` for an id.\n *\n * `?ids=a,b` resolves the users a stored value names; otherwise a page of\n * `?search=`, `?limit=` (25 by default) and `?offset=`.\n */\n router.get(\"/users/pick\", async (c) => {\n if (!mayPickUsers(c)) {\n throw new ApiError(403, \"SCOPE_MISSING\",\n \"Picking a user needs write access to a collection with a user field, or the \\\"users:read\\\" scope.\",\n { requiredScope: \"users:read\" });\n }\n const toPickedUser = (u: { id: string; email: string; displayName?: string | null; photoUrl?: string | null }): PickedUser => ({\n uid: u.id,\n email: u.email,\n displayName: u.displayName ?? null,\n photoURL: u.photoUrl ?? null\n });\n\n const idsParam = c.req.query(\"ids\");\n if (idsParam !== undefined) {\n const ids = [...new Set(idsParam.split(\",\").map(id => id.trim()).filter(Boolean))]\n .slice(0, MAX_USER_IDS_PER_LOOKUP);\n const found = await Promise.all(ids.map(id => authRepo.getUserById(id)));\n const users = found.filter((u): u is NonNullable<typeof u> => !!u).map(toPickedUser);\n return c.json({ users, limit: ids.length, offset: 0 });\n }\n\n const limit = resolveListLimitParam(c.req.query(\"limit\"), { defaultLimit: 25 });\n const offsetParam = c.req.query(\"offset\");\n const offset = offsetParam ? Math.max(0, parseInt(offsetParam, 10) || 0) : 0;\n const result = await authRepo.listUsersPaginated({\n limit,\n offset,\n search: c.req.query(\"search\") || undefined\n });\n return c.json({ users: result.users.map(toPickedUser), limit: result.limit, offset: result.offset });\n });\n\n router.get(\"/users/:uid\", requireScope(\"users:read\"), async (c) => {\n const uid = c.req.param(\"uid\");\n const result = await authRepo.getUserWithRoles(uid);\n\n if (!result) {\n throw ApiError.notFound(\"User not found\");\n }\n\n const adminUser = toAdminUser(result.user, result.roles);\n return c.json({ user: adminUser });\n });\n\n router.post(\"/users\", requireScope(\"users:write\"), async (c) => {\n const body = await c.req.json();\n const { email } = body;\n if (!email) {\n throw ApiError.badRequest(\"Email is required\");\n }\n\n // One list: the roles judged here are the roles written below.\n const roles = body.roles === undefined || body.roles === null ? undefined : readRoleNames(body.roles);\n if (roles !== undefined) {\n assertMayGrantRoles(c, roles);\n }\n\n const existing = await authRepo.getUserByEmail(normalizeEmail(email));\n if (existing) {\n throw ApiError.conflict(\"A user with this email already exists\");\n }\n\n // A chosen password is held to the password policy; an administrator\n // importing accounts may waive it with `skipPasswordPolicy: true`.\n const prepResult = await prepareAdminUserValues(body, {\n authRepo,\n emailService,\n emailConfig,\n resolvedHooks: ops,\n collectionAuthConfig\n }, { mayWaivePasswordPolicy: callerIsAdmin(c) });\n\n // `beforeUserCreate` runs here too.\n //\n // It fired on self-service registration and nowhere else, while the\n // boot warning and `authentication.md` name admin user management as\n // one of the paths that bypasses collection callbacks and tell\n // developers to use this hook instead. A hook that skips the path its\n // own documentation sends people to is worse than no hook: they wrote\n // the validation, and it silently does not run here.\n // `emailVerified` is kept as well: an administrator names the address,\n // and the built-in prepare step marks it verified, as the collection\n // REST route already stored it. Dropped here, the invited account sat\n // unverified, and an OAuth sign-in onto an unverified account is\n // refused — so the invitee could not use \"Sign in with Google\".\n let createData = {\n email: prepResult.values.email as string,\n passwordHash: prepResult.values.passwordHash as string | undefined,\n displayName: prepResult.values.displayName as string | undefined,\n photoUrl: prepResult.values.photoUrl as string | undefined,\n metadata: prepResult.values.metadata as Record<string, unknown> | undefined,\n emailVerified: prepResult.values.emailVerified === true\n };\n\n if (ops.beforeUserCreate) {\n createData = await ops.beforeUserCreate(createData) as typeof createData;\n }\n\n const user = await authRepo.createUser(createData);\n\n if (roles !== undefined) {\n await authRepo.setUserRoles(user.id, roles);\n }\n\n // Through the same step as the collection REST route, which is what\n // decides whether a create hook already delivered the credentials. This\n // route used to call `finalizeAdminUserCreation` itself, and so sent its\n // own invitation after a hook's and dropped the hook's password from\n // the response.\n const delivery = await completeUserCreation(prepResult, clearPassword => finalizeAdminUserCreation(\n { id: user.id,\nvalues: prepResult.values },\n clearPassword,\n {\n authRepo,\n emailService,\n emailConfig,\n resolvedHooks: ops,\n collectionAuthConfig\n }\n ));\n\n const userRoles = await authRepo.getUserRoleIds(user.id);\n const adminUser = toAdminUser(user, userRoles);\n\n return c.json({ user: adminUser, ...delivery }, 201);\n });\n\n router.put(\"/users/:uid\", requireScope(\"users:write\"), async (c) => {\n const uid = c.req.param(\"uid\");\n const body = await c.req.json();\n const { password, email, displayName, disabled } = body;\n const roles = body.roles === undefined || body.roles === null ? undefined : readRoleNames(body.roles);\n\n const existing = await authRepo.getUserById(uid);\n if (!existing) {\n throw ApiError.notFound(\"User not found\");\n }\n if (disabled !== undefined) {\n if (typeof disabled !== \"boolean\") {\n throw ApiError.badRequest(\"`disabled` must be true or false\", \"INVALID_INPUT\");\n }\n const caller = c.get(\"user\") as { uid?: string } | undefined;\n if (disabled && caller?.uid === uid) {\n throw ApiError.badRequest(\"Cannot disable your own account\", \"SELF_DISABLE\");\n }\n if (typeof authRepo.setUserDisabled !== \"function\") {\n throw new ApiError(501, \"NOT_SUPPORTED\", \"This backend's auth repository cannot disable accounts.\");\n }\n }\n\n // Refused before anything is written, so a refused change does not\n // leave the email or password it arrived with applied. Nobody edits an\n // account holding more than they do, nor grants more than they hold.\n assertMayManageAccount(c, await authRepo.getUserRoleIds(uid));\n if (roles !== undefined) {\n assertMayGrantRoles(c, roles);\n await assertRoleChangesAllowed(authRepo, [{ uid, roles }]);\n }\n\n const updates: Record<string, unknown> = {};\n if (email !== undefined) {\n updates.email = await normalizeUserEmailChange(authRepo, uid, email);\n }\n if (displayName !== undefined) updates.displayName = displayName;\n\n let passwordHash: string | undefined;\n if (password) {\n const validation = ops.validatePasswordStrength(password);\n if (!validation.valid) {\n throw ApiError.badRequest(`Password too weak: ${validation.errors.join(\". \")}`, \"WEAK_PASSWORD\");\n }\n passwordHash = await ops.hashPassword(password);\n }\n\n if (Object.keys(updates).length > 0) {\n await authRepo.updateUser(uid, updates);\n }\n // Not folded into `updates`: an administrator setting someone's password\n // is a reset, and a reset ends that account's sessions. Writing the hash\n // through `updateUser` is how this route used to skip that.\n if (passwordHash) {\n await replaceUserPassword(authRepo, uid, passwordHash);\n }\n\n if (roles !== undefined) {\n await authRepo.setUserRoles(uid, roles);\n }\n\n // Disabling is how an administrator stops an account without deleting\n // it: it signs in nowhere, and every session and token it holds ends\n // now, on every door — see `judgeAccessToken`.\n if (typeof disabled === \"boolean\" && authRepo.setUserDisabled) {\n await authRepo.setUserDisabled(uid, disabled);\n if (disabled) await revokeAllSessions(authRepo, uid);\n logger.info(\"[Security Audit] Account \" + (disabled ? \"disabled\" : \"re-enabled\") + \" by an administrator\", {\n eventType: disabled ? \"auth.account.disabled\" : \"auth.account.enabled\",\n uid,\n by: (c.get(\"user\") as { uid?: string } | undefined)?.uid\n });\n }\n\n const result = await authRepo.getUserWithRoles(uid);\n const adminUser = toAdminUser(result!.user, result!.roles);\n\n return c.json({ user: adminUser });\n });\n\n /**\n * DELETE /users/:uid/mfa — remove every second factor and recovery code\n * an account has, and end its sessions.\n *\n * The way back in for someone who lost their authenticator and their\n * codes, which used to be SQL. It lowers the account's protection, so it\n * is held to what a password reset is held to: `users:write`, never on an\n * account that outranks the caller, and every session ends — a session\n * that passed the old factor must not outlive its removal.\n */\n router.delete(\"/users/:uid/mfa\", requireScope(\"users:write\"), async (c) => {\n const uid = c.req.param(\"uid\");\n const existing = await authRepo.getUserById(uid);\n if (!existing) {\n throw ApiError.notFound(\"User not found\");\n }\n assertMayManageAccount(c, await authRepo.getUserRoleIds(uid));\n\n const factors = await authRepo.getMfaFactors(uid);\n for (const factor of factors) {\n await authRepo.deleteMfaFactor(factor.id, uid);\n }\n await authRepo.deleteAllRecoveryCodes(uid);\n await revokeAllSessions(authRepo, uid);\n\n const caller = c.get(\"user\") as { uid?: string } | undefined;\n logger.info(\"[Security Audit] Second factors reset by an administrator\", {\n eventType: \"auth.mfa.admin_reset\",\n uid,\n by: caller?.uid,\n removedFactors: factors.length\n });\n return c.json({ success: true, removedFactors: factors.length });\n });\n\n router.delete(\"/users/:uid\", requireScope(\"users:write\"), async (c) => {\n const uid = c.req.param(\"uid\");\n const authUser = c.get(\"user\") as { uid?: string } | undefined;\n\n if (authUser?.uid === uid) {\n throw ApiError.badRequest(\"Cannot delete your own account\", \"SELF_DELETE\");\n }\n\n const existing = await authRepo.getUserById(uid);\n if (!existing) {\n throw ApiError.notFound(\"User not found\");\n }\n\n assertMayManageAccount(c, await authRepo.getUserRoleIds(uid));\n await assertUserDeletionsAllowed(authRepo, [uid]);\n\n // The delete hooks fire here, because this is where users are deleted.\n //\n // They had a single call site — `UserManagementAdapter.deleteUser` — and\n // no route invokes it, so `beforeUserDelete` and `afterUserDelete` fired\n // on no reachable path at all. Both the boot warning and\n // `authentication.md` tell developers to use exactly these hooks for the\n // side effects their collection callbacks will not see on this path, so\n // anyone who followed that advice had a veto that vetoed nothing and a\n // cleanup that never ran.\n //\n // `beforeUserDelete` throws to prevent the deletion, so it is awaited\n // before the write; `afterUserDelete` is fire-and-forget, so a failing\n // listener cannot fail a deletion that already happened.\n if (ops.beforeUserDelete) {\n await ops.beforeUserDelete(uid);\n }\n\n // Deleting an account is how an administrator bans one, so its\n // sessions end here rather than by whatever the engine cascades:\n // Postgres drops the refresh tokens with the user row, Mongo and a\n // custom repository need not, and a deleted user's refresh token then\n // kept minting access tokens.\n await authRepo.deleteAllRefreshTokensForUser(uid);\n await authRepo.deleteUser(uid);\n\n runAfterUserDelete(ops, uid);\n\n return c.json({ success: true });\n });\n\n return router;\n}\n","/**\n * RebaseBuiltinAuthAdapter\n *\n * Wraps Rebase's existing built-in JWT auth system (routes, middleware, user/role\n * management) into the `AuthAdapter` interface. This is the default adapter used\n * when the user passes a plain `RebaseAuthConfig` object.\n *\n * This is NOT a rewrite — it delegates to the existing `createAuthRoutes()`,\n * `createResetPasswordRoute()`, and `verifyAccessToken()` functions. The goal is to\n * present the same functionality through the pluggable `AuthAdapter` contract.\n */\n\nimport type {\n AuthAdapter,\n AuthenticatedUser,\n AuthAdapterCapabilities,\n UserManagementAdapter,\n AuthUserListOptions,\n AuthUserListResult,\n AuthUserData,\n AuthCreateUserData,\n BootstrappedAuth\n} from \"@rebasepro/types\";\n\nimport { Hono } from \"hono\";\nimport { hasAdminRole } from \"@rebasepro/types\";\nimport { accessTokenRefusalError, judgeAccessToken, replaceUserPassword, type AccessTokenRefusal, type AccessTokenVerdict } from \"./token-revocation\";\nimport { customClaimsOf, processJwtService, type JwtService } from \"./jwt\";\nimport type { AccessTokenPayload } from \"./jwt\";\nimport type { AuthLimiters } from \"./rate-limiter\";\nimport type { AccessControl } from \"./access\";\nimport { createAuthRoutes } from \"./routes\";\nimport type { CaptchaConfig } from \"./captcha\";\nimport { buildBuiltinAuthCapabilities } from \"./capabilities\";\nimport { isBootstrapWindowOpen, isUserTableEmpty } from \"./registration-policy\";\nimport { createResetPasswordRoute } from \"./reset-password-admin\";\nimport { createAdminRolesRoute } from \"./admin-roles-route\";\nimport { createAdminUsersRoute } from \"./admin-users-route\";\nimport { prepareAdminUserValues, finalizeAdminUserCreation, authCollectionUserAdmin, SKIP_PASSWORD_POLICY_FIELD } from \"./admin-user-ops\";\nimport type { AuthRepository, OAuthProvider } from \"./interfaces\";\nimport type { AuthHooks, ResolvedAuthHooks } from \"./auth-hooks\";\nimport { resolveAuthHooks } from \"./auth-hooks\";\nimport type { EmailService, EmailConfig } from \"../email\";\nimport type { HonoEnv } from \"../api/types\";\nimport { safeCompare } from \"./crypto-utils\";\nimport { extractBearerToken } from \"./bearer-token\";\nimport { logger } from \"../utils/logger\";\nimport { ApiError, isDataException, isDatabaseUnreachable } from \"../api/errors\";\nimport { warnIfDatabaseClosed } from \"../api/closed-database\";\n\n/**\n * Configuration for the built-in Rebase auth adapter.\n *\n * This mirrors the existing `RebaseAuthConfig` — users pass this and\n * server auto-wraps it in a `RebaseBuiltinAuthAdapter`.\n */\nexport interface BuiltinAuthAdapterConfig {\n /** The bootstrapper-provided auth repository (users, roles, tokens). */\n authRepository: AuthRepository;\n /**\n * The backend this adapter signs and verifies tokens for —\n * `backend.services.jwt`, or a service of your own from `createJwtService`.\n * The process default (what `configureJwt` sets) when omitted.\n */\n jwt?: JwtService;\n /** The backend's auth limiters, counting in its own store. The process default's set when omitted. */\n limiters?: AuthLimiters;\n /** The backend's access model, which the default role is checked against. The process default when omitted. */\n access?: AccessControl;\n /** Email service for password resets, verification, etc. */\n emailService?: EmailService;\n /** Email configuration. */\n emailConfig?: EmailConfig;\n /** Whether to allow new user registration. */\n allowRegistration?: boolean;\n /**\n * Hard kill switch: block self-registration outright, including the\n * first-user bootstrap window that an empty database would otherwise open.\n *\n * Was declared on the route module and read by both config endpoints, but\n * never plumbed through here — so nothing a user of the framework could\n * write ever reached it, and the tests that covered it passed only because\n * they built `createAuthRoutes` directly, bypassing this adapter (the sole\n * wiring path a real backend uses).\n */\n disableSelfRegistration?: boolean;\n /**\n * Opt-in: allow `POST /auth/anonymous` to mint a user without credentials.\n *\n * Off by default, and overridden by `disableSelfRegistration`. Anonymous\n * sign-in inserts a `users` row and assigns `defaultRole` exactly as\n * registration does, so before this key existed a closed backend still\n * handed out permanent accounts via `/auth/anonymous` + `/auth/anonymous/link`.\n */\n allowAnonymous?: boolean;\n /** Whether to expose the authenticated email→minimal-profile lookup route. */\n allowUserLookup?: boolean;\n /** Bot protection for the auth routes. See `captcha.ts`. */\n captcha?: CaptchaConfig;\n /** Default role to assign to new users. */\n defaultRole?: string;\n /** OAuth providers to register. */\n oauthProviders?: OAuthProvider<unknown>[];\n /** Redirect URIs the OAuth routes accept, for every provider. */\n allowedRedirectUris?: string[];\n /** Static service key for server-to-server auth. */\n serviceKey?: string;\n /** Auth hooks for customizing password, credentials, lifecycle, etc. */\n authHooks?: AuthHooks;\n /** The parsed auth config from the collection (if `auth` is an object, not just `true`). */\n collectionAuthConfig?: import(\"@rebasepro/types\").AuthCollectionConfig;\n /** Enable magic link (passwordless email) login. Requires email service. */\n enableMagicLink?: boolean;\n enableEmailOtp?: boolean;\n /** Opt-in httpOnly cookie mode for refresh tokens. */\n cookieAuth?: import(\"./routes\").CookieAuthConfig;\n /** Refuse password sign-in until the address is verified; confirm-first registration. */\n requireEmailVerification?: boolean;\n /** Seconds a rotated-away refresh token still mints a sibling. Default 10. */\n refreshTokenReuseIntervalSeconds?: number;\n /** What a refresh token replayed after that window does to its session. Default `\"reject\"`. */\n refreshTokenReuse?: import(\"./routes\").RefreshTokenReusePolicy;\n /** A magic-link or email-code request for an unknown address creates its account, while registration is open. */\n magicLinkCreatesUsers?: boolean;\n}\n\n/**\n * Create the built-in Rebase auth adapter.\n *\n * This wraps the existing auth infrastructure (JWT, OAuth, user/role management)\n * into the `AuthAdapter` interface. It's used internally by `initializeRebaseBackend()`\n * when the user passes a plain `RebaseAuthConfig` object.\n */\nexport function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): AuthAdapter {\n const {\n authRepository,\n emailService,\n emailConfig,\n allowRegistration = false,\n disableSelfRegistration = false,\n allowAnonymous = false,\n allowUserLookup = false,\n captcha,\n defaultRole,\n oauthProviders = [],\n allowedRedirectUris,\n serviceKey,\n authHooks,\n collectionAuthConfig,\n enableMagicLink = false,\n enableEmailOtp = false,\n cookieAuth,\n requireEmailVerification = false,\n refreshTokenReuseIntervalSeconds,\n refreshTokenReuse,\n magicLinkCreatesUsers = false\n } = config;\n const jwt = config.jwt ?? processJwtService;\n\n const resolvedOps = resolveAuthHooks(authHooks);\n\n /**\n * The caller's roles as the database has them now.\n *\n * Read per request because the token's `roles` claim can be a whole\n * access-token lifetime stale — a demoted admin still holds a token that\n * says `admin`. So when the read fails, falling back to the claim hands\n * that token exactly the access the lookup exists to take away; during a\n * database blip it scoped a demoted admin with admin RLS. This refuses\n * instead, with the 503 `createRequireAuth` already gives the admin\n * routes: the adapter middleware answers with it, and so does the socket.\n */\n async function resolveLiveRoles(uid: string): Promise<string[]> {\n try {\n return await authRepository.getUserRoleIds(uid);\n } catch (error: unknown) {\n throw lookupRefusal(\"[Auth] Could not resolve roles for a request; refusing it\", uid, error);\n }\n }\n\n /**\n * The 503 a request is refused with when its account cannot be read.\n *\n * A database that cannot be reached at all is said as that —\n * `DATABASE_UNAVAILABLE`, the answer every other door gives then — and\n * not as a doubt about the caller's permissions, which nobody was in a\n * position to have. Anything else the store throws is the lookup failing,\n * `ROLE_LOOKUP_FAILED`.\n */\n function lookupRefusal(line: string, uid: string, error: unknown): ApiError {\n if (isDatabaseUnreachable(error)) {\n // A database closed to new connections is named, with what opens\n // it, once a minute: every request fails the same way.\n if (!warnIfDatabaseClosed(error)) logger.warn(`${line}: the database is unreachable`, { uid });\n return ApiError.databaseUnavailable();\n }\n logger.warn(line, { uid, error });\n return new ApiError(503, \"ROLE_LOOKUP_FAILED\", \"Could not verify your permissions. Please try again.\");\n }\n\n /**\n * The roles a verified access token runs with now, or why it must not be\n * honoured at all: revoked, or its account deleted or disabled. See\n * `judgeAccessToken`. A repository that cannot answer refuses the\n * request (503), as `resolveLiveRoles` does.\n */\n async function liveRoles(payload: AccessTokenPayload): Promise<string[] | AccessTokenRefusal> {\n let verdict: AccessTokenVerdict;\n try {\n verdict = await judgeAccessToken(authRepository, payload);\n } catch (error: unknown) {\n throw lookupRefusal(\"[Auth] Could not judge an access token; refusing it\", payload.uid, error);\n }\n if (!verdict.live) {\n logger.warn(\"[Security Audit] Refused an access token\", {\n eventType: verdict.refusal === \"revoked\" ? \"auth.token.revoked\" : \"auth.token.account_gone\",\n refusal: verdict.refusal,\n uid: payload.uid\n });\n return verdict.refusal;\n }\n return verdict.roles ?? await resolveLiveRoles(payload.uid);\n }\n\n /**\n * Who a bearer token names, now — the one judgement both verify paths\n * make, so the data plane, the realtime socket and\n * `backend.verifyCredential` cannot disagree about a token.\n *\n * `null` when it does not verify: not one of this backend's, malformed or\n * expired. The refusal when it verifies and must not be honoured. Otherwise\n * the account, with the roles the database holds for it now — never the\n * ones the token claims, which can be a whole token lifetime stale.\n */\n async function judgeBearer(token: string): Promise<AuthenticatedUser | AccessTokenRefusal | null> {\n // The service key first, compared in constant time.\n if (serviceKey && safeCompare(token, serviceKey)) {\n return {\n uid: \"service\",\n email: \"service@rebase.internal\",\n roles: [\"admin\"],\n isAdmin: true,\n rawToken: token\n };\n }\n\n const payload = await jwt.verifyAccessToken(token);\n if (!payload) return null;\n\n // A token issued before the user's revocation watermark is void,\n // however well it verifies, and so is one whose account is gone.\n // This is the read that makes `logout`, `change-password`,\n // `reset-password`, `DELETE /auth/sessions` and deleting the account\n // reach the access token instead of only the refresh row.\n const roles = await liveRoles(payload);\n if (!Array.isArray(roles)) return roles;\n\n return {\n uid: payload.uid,\n email: payload.email ?? \"\",\n displayName: payload.displayName ?? null,\n roles,\n isAdmin: hasAdminRole(roles),\n // From the token, where the server wrote it at sign-in and a\n // custom-claims hook cannot reach it. Reaches the database as\n // `rebase.is_anonymous()` — see the claim's own note.\n isAnonymous: payload.isAnonymous === true,\n // What `customizeAccessToken` put on the token. Rebuilt without\n // it, a claim-tenanted collection had no tenant to read —\n // `rebase.jwt() ->> 'org_id'` was NULL on every backend with a\n // `config.auth` object, which is every one that runs this. The\n // socket's tenancy reads a claim too.\n ...(payload.claims ? { claims: payload.claims } : {}),\n rawToken: token\n };\n }\n\n const adapter: AuthAdapter = {\n id: \"rebase-builtin\",\n\n serviceKey,\n\n async verifyRequest(request: Request): Promise<AuthenticatedUser | null> {\n // Tokens are accepted ONLY via the Authorization header. A `?token=`\n // query param must NOT authenticate here: URLs leak into access\n // logs, proxies, Referer headers, and browser history, and the\n // non-adapter middleware already refuses query tokens for exactly\n // that reason. Routes that legitimately need query-string tokens\n // (storage file serving for `<img src>`) use scoped download\n // tokens via `fileTokenAuth`, which never reach this adapter.\n const token = extractBearerToken(request.headers.get(\"authorization\"));\n if (token === undefined) return null;\n\n // A refused token is thrown as the 401 that says why —\n // `SESSION_REVOKED`, `ACCOUNT_DISABLED` — which the adapter\n // middleware answers with, rather than read as one that does not\n // verify (\"Invalid or expired token\"), which it is not.\n const verdict = await judgeBearer(token);\n if (typeof verdict === \"string\") throw accessTokenRefusalError(verdict);\n return verdict;\n },\n\n async verifyToken(token: string): Promise<AuthenticatedUser | null> {\n // The same judgement the request path makes above. This is what\n // the WebSocket handler calls — at AUTHENTICATE, and again on the\n // frames that follow — and what `backend.verifyCredential` asks,\n // so a token voided for HTTP is voided there too. A refusal is\n // `null` here: the socket's contract, where a throw means the\n // store could not answer.\n const verdict = await judgeBearer(token);\n return typeof verdict === \"string\" ? null : verdict;\n },\n\n async resolveUser(uid: string): Promise<AuthenticatedUser | null> {\n let user: Awaited<ReturnType<typeof authRepository.getUserById>>;\n try {\n user = await authRepository.getUserById(uid);\n } catch (error: unknown) {\n // A uid the id column cannot hold (`abc` against a UUID key)\n // names no account. Any other failure is the store's, and is\n // not an answer about anyone.\n if (isDataException(error)) return null;\n throw error;\n }\n // A disabled account signs in nowhere, so nothing runs as it.\n if (!user || user.disabled) return null;\n\n const roles = await resolveLiveRoles(uid);\n const isAnonymous = user.isAnonymous === true;\n\n // What a token minted for this account now would carry, through\n // the same hook and the same filter as a real one: the hook adds\n // facts about the session, and the identity claims it was handed\n // are dropped, exactly as `verifyAccessToken` drops them.\n let claims: Record<string, unknown> | undefined;\n if (resolvedOps.customizeAccessToken) {\n claims = customClaimsOf(await resolvedOps.customizeAccessToken({ uid, roles, aal: \"aal1\" }, user));\n }\n\n return {\n uid,\n email: user.email,\n displayName: user.displayName ?? null,\n photoUrl: user.photoUrl ?? null,\n roles,\n isAdmin: hasAdminRole(roles),\n isAnonymous,\n ...(claims ? { claims } : {})\n };\n },\n\n userManagement: createUserManagementFromRepo(authRepository, resolvedOps),\n\n\n createAuthRoutes(): Hono<HonoEnv> | undefined {\n return createAuthRoutes({\n authRepo: authRepository,\n jwt,\n limiters: config.limiters,\n access: config.access,\n emailService,\n emailConfig,\n allowRegistration,\n disableSelfRegistration,\n allowAnonymous,\n allowUserLookup,\n defaultRole,\n oauthProviders,\n allowedRedirectUris,\n authHooks,\n enableMagicLink,\n enableEmailOtp,\n cookieAuth,\n captcha,\n requireEmailVerification,\n refreshTokenReuseIntervalSeconds,\n refreshTokenReuse,\n magicLinkCreatesUsers\n });\n },\n\n createAdminRoutes(): Hono<HonoEnv> | undefined {\n const router = new Hono<HonoEnv>();\n const resetPasswordRoute = createResetPasswordRoute({\n authRepo: authRepository,\n emailService,\n emailConfig,\n serviceKey,\n authHooks,\n collectionAuthConfig\n });\n const rolesRoute = createAdminRolesRoute({\n authRepo: authRepository,\n serviceKey\n });\n const adminUsersRoute = createAdminUsersRoute({\n authRepo: authRepository,\n emailService,\n emailConfig,\n serviceKey,\n authHooks,\n collectionAuthConfig\n });\n router.route(\"/\", resetPasswordRoute);\n router.route(\"/\", rolesRoute);\n router.route(\"/\", adminUsersRoute);\n return router;\n },\n\n async prepareUserCreation(values, collectionAuth, options) {\n const parsedCollectionAuth = collectionAuth as import(\"@rebasepro/types\").AuthCollectionConfig | undefined;\n return prepareAdminUserValues(values, {\n authRepo: authRepository,\n emailService,\n emailConfig,\n resolvedHooks: resolvedOps,\n collectionAuthConfig: parsedCollectionAuth ?? collectionAuthConfig\n }, options);\n },\n\n describeUserCreationContract(collectionAuth) {\n const parsedCollectionAuth = collectionAuth as import(\"@rebasepro/types\").AuthCollectionConfig | undefined;\n const authConfig = parsedCollectionAuth ?? collectionAuthConfig;\n\n // A custom hook owns the body's shape — `prepareAdminUserValues`\n // hands it the raw body and returns whatever the hook built, so the\n // fields it accepts are the hook's business and unknowable here.\n // Checking against the collection would reject bodies the hook is\n // designed to take.\n if (authConfig?.onCreateUser || resolvedOps.onAdminCreateUser) {\n return { validate: false, extraFields: [] };\n }\n\n // The built-in path consumes exactly two fields the users table\n // does not declare: `password`, which it hashes into `passwordHash`\n // and deletes, and `skipPasswordPolicy`, which it reads and drops.\n // Everything else it spreads through untouched, so everything else\n // has to be a real column.\n // No hook runs, so creations may be prepared at once — unless the\n // hash is the application's, which may not be reentrant.\n return { validate: true, extraFields: [\"password\", SKIP_PASSWORD_POLICY_FIELD], independent: !authHooks?.hashPassword };\n },\n\n async finalizeUserCreation(entity, clearPassword) {\n return finalizeAdminUserCreation(entity, clearPassword, {\n authRepo: authRepository,\n emailService,\n emailConfig,\n resolvedHooks: resolvedOps,\n collectionAuthConfig\n });\n },\n\n // A write to the auth collection through the data API is user\n // administration, and holds to what `/admin/users` holds to — the\n // same rules and hooks, from the same functions.\n ...authCollectionUserAdmin({ authRepo: authRepository, resolvedHooks: resolvedOps }),\n\n async getCapabilities(): Promise<AuthAdapterCapabilities> {\n // Detect bootstrap mode: are there any users, and may the first one\n // still claim the deployment? In production the answer to the second\n // is no (`isBootstrapWindowOpen`), and advertising `needsSetup` there\n // would put a \"create the first admin\" form in front of a route that\n // refuses it — and tell a scanner which hosts are unclaimed.\n //\n // So the table is only asked where the window exists: in production\n // `needsSetup` is false whatever it holds, and this endpoint is\n // anonymous and fetched on every admin-panel load and login screen.\n let needsSetup = false;\n try {\n needsSetup = isBootstrapWindowOpen() && await isUserTableEmpty(authRepository);\n } catch (error) {\n // Fail closed — an unreadable users table must not open the\n // first-admin window — but say so. Silently answering \"not in\n // setup mode\" makes a database that is merely unreachable look\n // like a backend that has been set up already, which is the\n // login screen a fresh deployment least wants to see.\n logger.warn(\"[Auth] Could not count users for `needsSetup`; reporting the backend as already set up.\", {\n error: error instanceof Error ? error.message : String(error)\n });\n }\n\n // `buildBuiltinAuthCapabilities` is the only place this payload is\n // assembled, so `GET /auth/config` cannot drift from what the auth\n // routes enforce — see `capabilities.ts`.\n return buildBuiltinAuthCapabilities({\n needsSetup,\n allowRegistration,\n disableSelfRegistration,\n allowAnonymous,\n enableMagicLink,\n enableEmailOtp,\n emailConfigured: !!emailService?.isConfigured(),\n enabledProviders: oauthProviders.map((p) => p.id)\n });\n }\n };\n\n return adapter;\n}\n\n// ─── Internal Helpers ────────────────────────────────────────────────────────\n\nfunction createUserManagementFromRepo(repo: AuthRepository, resolvedOps: ResolvedAuthHooks): UserManagementAdapter {\n return {\n async listUsers(options?: AuthUserListOptions): Promise<AuthUserListResult> {\n const result = await repo.listUsersPaginated({\n limit: options?.limit,\n offset: options?.offset,\n search: options?.search,\n orderBy: options?.orderBy,\n orderDir: options?.orderDir,\n roleId: options?.roleId\n });\n return {\n users: result.users.map(toAuthUserData),\n total: result.total,\n limit: result.limit,\n offset: result.offset\n };\n },\n\n async getUserById(id: string): Promise<AuthUserData | null> {\n const user = await repo.getUserById(id);\n return user ? toAuthUserData(user) : null;\n },\n\n async createUser(data: AuthCreateUserData): Promise<AuthUserData> {\n const passwordHash = data.password ? await resolvedOps.hashPassword(data.password) : undefined;\n let createData: import(\"./interfaces\").CreateUserData = {\n email: data.email,\n passwordHash,\n displayName: data.displayName,\n photoUrl: data.photoUrl,\n metadata: data.metadata,\n ...(data.emailVerified !== undefined ? { emailVerified: data.emailVerified } : {})\n };\n if (resolvedOps.beforeUserCreate) {\n createData = await resolvedOps.beforeUserCreate(createData);\n }\n const user = await repo.createUser(createData);\n if (resolvedOps.afterUserCreate) {\n try {\n await resolvedOps.afterUserCreate(user);\n } catch (err) {\n logger.error(\"[AuthHooks] afterUserCreate error\", { error: err instanceof Error ? err.message : err });\n }\n }\n return toAuthUserData(user);\n },\n\n async updateUser(id: string, data: Partial<AuthCreateUserData>): Promise<AuthUserData | null> {\n const updateData: Record<string, unknown> = {};\n if (data.email !== undefined) updateData.email = data.email;\n if (data.displayName !== undefined) updateData.displayName = data.displayName;\n if (data.photoUrl !== undefined) updateData.photoUrl = data.photoUrl;\n if (data.metadata !== undefined) updateData.metadata = data.metadata;\n const passwordHash = data.password ? await resolvedOps.hashPassword(data.password) : undefined;\n const user = await repo.updateUser(id, updateData);\n // A new password ends the account's sessions, whichever door set\n // it — so it goes through the same helper as every route does,\n // rather than riding along in `updateData`.\n if (user && passwordHash) {\n await replaceUserPassword(repo, id, passwordHash);\n }\n return user ? toAuthUserData(user) : null;\n },\n\n async deleteUser(id: string): Promise<void> {\n // Call beforeUserDelete hook (throw to prevent deletion)\n if (resolvedOps.beforeUserDelete) {\n await resolvedOps.beforeUserDelete(id);\n }\n\n // The sessions end with the account on every engine — see the\n // same line in `DELETE /admin/users/:uid`.\n await repo.deleteAllRefreshTokensForUser(id);\n await repo.deleteUser(id);\n\n // Fire afterUserDelete hook (fire-and-forget)\n if (resolvedOps.afterUserDelete) {\n resolvedOps.afterUserDelete(id).catch(err => {\n logger.error(\"[AuthHooks] afterUserDelete error\", { error: err instanceof Error ? err.message : err });\n });\n }\n },\n\n async getUserRoles(uid: string): Promise<string[]> {\n return repo.getUserRoleIds(uid);\n },\n\n async setUserRoles(uid: string, roleIds: string[]): Promise<void> {\n await repo.setUserRoles(uid, roleIds);\n }\n };\n}\n\nfunction toAuthUserData(user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; emailVerified?: boolean; metadata?: Record<string, unknown>; createdAt?: Date; updatedAt?: Date }): AuthUserData {\n return {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n photoUrl: user.photoUrl,\n emailVerified: user.emailVerified,\n metadata: user.metadata,\n createdAt: user.createdAt,\n updatedAt: user.updatedAt\n };\n}\n","import { Hono } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { processJwtService, type JwtService } from \"./jwt\";\n\n/**\n * `GET /.well-known/jwks.json` — the public keys that verify this issuer's\n * access tokens.\n *\n * Deliberately unauthenticated and world-readable: the whole point is that a\n * gateway, an edge function or a neighbouring service can check a Rebase token\n * without being trusted with anything. Public keys are not a secret, and\n * `jwt-keys.ts` derives what is served here from the public half of each pair.\n *\n * Mounted at the root rather than under `basePath`, because `/.well-known/` is\n * where every verifier looks — an issuer of `https://api.example.com` implies\n * `https://api.example.com/.well-known/jwks.json`, whatever the API happens to\n * be prefixed with.\n *\n * `jwt` is the issuer whose keys these are — a backend's own\n * (`backend.services.jwt`), or the process default when omitted.\n */\nexport function createJwksRoutes(jwt: JwtService = processJwtService): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n\n router.get(\"/jwks.json\", (c) => {\n const jwks = jwt.getJwks();\n\n // Verifiers cache JWKS and re-fetch on an unknown `kid`, so a long TTL\n // is safe and a short one is wasteful — adding a key is not a reason to\n // invalidate anyone's cache, because nothing signs with it until the\n // deploy that makes it active, and a verifier that meets an unfamiliar\n // `kid` refetches anyway. Five minutes is the interval a rotation is\n // measured against, not the tokens' lifetime.\n c.header(\"Cache-Control\", \"public, max-age=300\");\n return c.json(jwks);\n });\n\n return router;\n}\n","/**\n * The scopes a key stored before scopes existed is read as.\n *\n * Such a row carries `permissions` — `[{ collection, operations }]`, with\n * `\"*\"`, `\"storage\"`, `\"functions\"` and `\"functions/<name>\"` overloading the\n * collection field — and an `admin` flag. This turns it into the scopes and\n * RLS roles it now holds, once, when the store backfills the row.\n *\n * The rule is that nothing widens. Where a stored grant has no exact\n * equivalent, it narrows:\n *\n * - A function grant without `write` becomes nothing. Functions were checked\n * by HTTP method, so a read-only grant let a key call GET functions; a\n * function is code, and `functions:invoke` is not a read.\n * - `admin: true` becomes the `admin` RLS role and the admin surfaces such a\n * key could reach (users, schema, backups, cron, logs) — not `database:*`,\n * which lived on the realtime socket where no key could authenticate, and\n * not `keys:*`, which no key may hold.\n *\n * @module\n */\n\nimport type { AdminScope } from \"@rebasepro/types\";\n\n/** One stored permission entry, as the old column held it. */\nexport interface StoredPermission {\n collection: string;\n operations: string[];\n}\n\nconst OPERATIONS = [\"read\", \"write\", \"delete\"] as const;\ntype Operation = (typeof OPERATIONS)[number];\n\nfunction isOperation(value: string): value is Operation {\n return (OPERATIONS as readonly string[]).includes(value);\n}\n\n/** The admin surfaces an `admin: true` key reached. */\nconst ADMIN_KEY_SCOPES: readonly AdminScope[] = [\n \"users:read\",\n \"users:write\",\n \"schema:read\",\n \"schema:write\",\n \"backups:read\",\n \"cron:read\",\n \"cron:write\",\n \"logs:read\"\n];\n\n/** Read a stored `permissions` value — JSON text or already-parsed — into entries. */\nexport function parseStoredPermissions(value: unknown): StoredPermission[] {\n let raw = value;\n if (typeof raw === \"string\") {\n try {\n raw = JSON.parse(raw);\n } catch {\n return [];\n }\n }\n if (!Array.isArray(raw)) return [];\n const entries: StoredPermission[] = [];\n for (const item of raw) {\n if (typeof item !== \"object\" || item === null) continue;\n if (!(\"collection\" in item) || typeof item.collection !== \"string\") continue;\n const operations = \"operations\" in item && Array.isArray(item.operations)\n ? item.operations.filter((op: unknown): op is string => typeof op === \"string\")\n : [];\n entries.push({ collection: item.collection, operations });\n }\n return entries;\n}\n\n/** The scopes and RLS roles a stored key now holds. Never more than it did. */\nexport function scopesFromStoredPermissions(\n permissions: readonly StoredPermission[],\n admin: boolean\n): { scopes: string[]; roles: string[] } {\n const scopes = new Set<string>();\n for (const { collection, operations } of permissions) {\n const ops = operations.filter(isOperation);\n if (collection === \"*\") {\n for (const op of ops) {\n scopes.add(`data:${op}`);\n scopes.add(`storage:${op}`);\n }\n if (ops.includes(\"write\")) scopes.add(\"functions:invoke\");\n } else if (collection === \"storage\") {\n for (const op of ops) scopes.add(`storage:${op}`);\n } else if (collection === \"functions\") {\n if (ops.includes(\"write\")) scopes.add(\"functions:invoke\");\n } else if (collection.startsWith(\"functions/\")) {\n const name = collection.slice(\"functions/\".length);\n if (name && ops.includes(\"write\")) scopes.add(`functions:invoke:${name}`);\n } else if (collection) {\n for (const op of ops) scopes.add(`data:${op}:${collection}`);\n }\n }\n if (admin) {\n for (const scope of ADMIN_KEY_SCOPES) scopes.add(scope);\n }\n return { scopes: [...scopes], roles: admin ? [\"admin\"] : [] };\n}\n","/**\n * Database operations for API keys.\n *\n * Uses the DataDriver's `admin.executeSql` capability (same pattern as\n * the cron-store and ensure-tables modules). All data lives in the\n * `rebase.api_keys` table.\n *\n * @module\n */\n\nimport { randomBytes, createHash } from \"crypto\";\nimport type { DataDriver } from \"@rebasepro/types\";\nimport { isSQLAdmin } from \"@rebasepro/types\";\nimport { REBASE_USER_ROLE, revokeInternalTableSql } from \"@rebasepro/common\";\nimport { logger } from \"../../utils/logger\";\nimport { createDdlBootstrapper, provisioningExec, SchemaChangeRefusedError } from \"../../boot/ddl-bootstrap\";\nimport type {\n ApiKey,\n ApiKeyKind,\n ApiKeyMasked,\n ApiKeyWithSecret,\n UpdateApiKeyRequest\n} from \"./api-key-types\";\nimport { parseStoredPermissions, scopesFromStoredPermissions } from \"./legacy-permissions\";\n\nconst TABLE = \"rebase.api_keys\";\n\n/**\n * Drops `rebase.idx_api_keys_hash` — a plain btree on `key_hash`, next to the\n * UNIQUE (key_hash) constraint's own index — but only while a valid, unique,\n * non-partial btree on the table leads with the same column, so a lookup is\n * never left without one. Under a one-second `lock_timeout`, local to this\n * statement's transaction: `DROP INDEX` takes the table's ACCESS EXCLUSIVE lock.\n * A no-op read once the index is gone.\n */\nconst DROP_DUPLICATE_KEY_HASH_INDEX_SQL = `\nDO $rebase_drop_hash_index$\nBEGIN\n IF EXISTS (\n SELECT 1\n FROM pg_index d\n JOIN pg_class ic ON ic.oid = d.indexrelid\n JOIN pg_namespace n ON n.oid = ic.relnamespace\n WHERE n.nspname = 'rebase' AND ic.relname = 'idx_api_keys_hash' AND d.indnatts = 1\n AND EXISTS (\n SELECT 1 FROM pg_index o\n JOIN pg_class oc ON oc.oid = o.indexrelid\n JOIN pg_am am ON am.oid = oc.relam\n WHERE o.indrelid = d.indrelid AND o.indexrelid <> d.indexrelid\n AND o.indisvalid AND o.indisunique AND o.indpred IS NULL AND o.indexprs IS NULL\n AND am.amname = 'btree' AND o.indkey[0] = d.indkey[0])\n ) THEN\n PERFORM set_config('lock_timeout', '1000ms', true);\n DROP INDEX IF EXISTS rebase.idx_api_keys_hash;\n END IF;\nEND\n$rebase_drop_hash_index$`.trim();\n\n/** The relations `ensureTable` creates in `rebase`: the table and its two indexes. */\nconst BOOT_RELATIONS = [\"api_keys\", \"idx_api_keys_prefix\", \"idx_api_keys_owner\"];\n\n/** The columns `ensureTable` adds to a table created before scopes. */\nconst BOOT_COLUMNS = [\"kind\", \"scopes\", \"roles\", \"owner_uid\"];\n\n/**\n * One catalogue read answering whether `ensureTable` has anything to do. It\n * returns one row: whether the table, its indexes and the scope columns are\n * there, whether the duplicate hash index and the pre-scopes `permissions`\n * column are gone, and whether the end-user role still holds a privilege (the\n * same test `revokeInternalTableSql` makes before it revokes). `readable` is\n * the connection's own access, the question `isReadable` asks.\n */\nconst BOOT_STATE_SQL = (() => {\n const list = (names: string[]) => names.map(name => `'${name}'`).join(\", \");\n const target = `(SELECT oid FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')`;\n const columnCount = (names: string[]) =>\n `(SELECT count(*) FROM pg_attribute a WHERE a.attrelid = to_regclass('${TABLE}') AND a.attnum > 0 ` +\n `AND NOT a.attisdropped AND a.attname IN (${list(names)}))::int`;\n return `\nSELECT\n (SELECT count(*) FROM pg_class c WHERE c.relnamespace = to_regnamespace('rebase')\n AND c.relname IN (${list(BOOT_RELATIONS)}))::int AS present,\n (SELECT count(*) FROM pg_class c WHERE c.relnamespace = to_regnamespace('rebase')\n AND c.relname = 'idx_api_keys_hash')::int AS stale,\n ${columnCount(BOOT_COLUMNS)} AS columns,\n ${columnCount([\"permissions\"])} AS legacy,\n (SELECT count(*) FROM pg_class c\n WHERE c.oid = to_regclass('${TABLE}') AND NOT c.relrowsecurity AND ${target} IS NOT NULL\n AND (c.relacl IS NULL\n OR EXISTS (SELECT 1 FROM aclexplode(c.relacl) a WHERE a.grantee = ${target})\n OR EXISTS (SELECT 1 FROM pg_attribute att, aclexplode(att.attacl) a\n WHERE att.attrelid = c.oid AND att.attacl IS NOT NULL AND a.grantee = ${target})))::int AS granted,\n (has_schema_privilege('rebase', 'USAGE') AND has_table_privilege(to_regclass('${TABLE}'), 'SELECT')) AS readable\nWHERE to_regnamespace('rebase') IS NOT NULL`.trim();\n})();\n\n/**\n * Generate a plaintext API key with the `rk_live_` prefix.\n *\n * Format: `rk_live_` + 32 random hex characters.\n */\nfunction generateApiKey(): string {\n const random = randomBytes(16).toString(\"hex\"); // 32 hex chars\n return `rk_live_${random}`;\n}\n\n/**\n * SHA-256 hash a plaintext API key for database storage.\n */\nfunction hashKey(plaintext: string): string {\n return createHash(\"sha256\").update(plaintext).digest(\"hex\");\n}\n\n/**\n * Extract the display prefix from a plaintext key (first 12 chars).\n */\nfunction keyPrefix(plaintext: string): string {\n return plaintext.substring(0, 12);\n}\n\n/**\n * Strip the `key_hash` field and return a safe-to-expose masked key.\n */\nfunction toMasked(row: ApiKey): ApiKeyMasked {\n return {\n id: row.id,\n name: row.name,\n kind: row.kind,\n key_prefix: row.key_prefix,\n scopes: row.scopes,\n roles: row.roles,\n owner_uid: row.owner_uid,\n rate_limit: row.rate_limit,\n created_by: row.created_by,\n created_at: row.created_at,\n updated_at: row.updated_at,\n last_used_at: row.last_used_at,\n expires_at: row.expires_at,\n revoked_at: row.revoked_at\n };\n}\n\n/** A JSONB string array as the driver hands it back: parsed, or JSON text. */\nfunction stringList(value: unknown): string[] {\n let raw = value;\n if (typeof raw === \"string\") {\n try {\n raw = JSON.parse(raw);\n } catch {\n return [];\n }\n }\n return Array.isArray(raw) ? raw.filter((entry): entry is string => typeof entry === \"string\") : [];\n}\n\n/**\n * Parse a raw DB row into the typed `ApiKey` shape.\n */\nfunction rowToApiKey(row: Record<string, unknown>): ApiKey {\n // A row an older runtime wrote after this one backfilled the table has\n // no scopes yet; it is read the way the backfill would write it.\n const grant = row.scopes === null || row.scopes === undefined\n ? scopesFromStoredPermissions(parseStoredPermissions(row.permissions), row.admin === true)\n : { scopes: stringList(row.scopes), roles: stringList(row.roles) };\n return {\n id: String(row.id),\n name: String(row.name),\n kind: row.kind === \"personal\" ? \"personal\" : \"service\",\n key_prefix: String(row.key_prefix),\n key_hash: String(row.key_hash),\n scopes: grant.scopes,\n roles: grant.roles,\n owner_uid: typeof row.owner_uid === \"string\" ? row.owner_uid : null,\n rate_limit: row.rate_limit !== null && row.rate_limit !== undefined\n ? Number(row.rate_limit)\n : null,\n created_by: String(row.created_by),\n created_at: new Date(String(row.created_at)).toISOString(),\n updated_at: new Date(String(row.updated_at)).toISOString(),\n last_used_at: row.last_used_at ? new Date(String(row.last_used_at)).toISOString() : null,\n expires_at: row.expires_at ? new Date(String(row.expires_at)).toISOString() : null,\n revoked_at: row.revoked_at ? new Date(String(row.revoked_at)).toISOString() : null\n };\n}\n\n// ─── Public API ──────────────────────────────────────────────────────\n\n/** What a new key row is made of, already validated by the route that asked. */\nexport interface NewApiKey {\n name: string;\n kind: ApiKeyKind;\n scopes: string[];\n /** RLS roles beside `service`. Always empty for a personal key. */\n roles: string[];\n /** The account a personal key acts as. */\n owner_uid: string | null;\n rate_limit: number | null;\n expires_at: string | null;\n}\n\n/** Which keys a listing returns. */\nexport type ApiKeyFilter =\n | { kind: \"service\" }\n | { kind: \"personal\"; owner_uid: string };\n\nexport interface ApiKeyStore {\n /** Ensure the `rebase.api_keys` table exists. Called once on startup. */\n ensureTable(): Promise<void>;\n\n /** Create a new API key. Returns the full plaintext key exactly once. */\n createApiKey(key: NewApiKey, createdBy: string): Promise<ApiKeyWithSecret>;\n\n /** Look up an API key by its SHA-256 hash. Returns `null` if not found. */\n findByKeyHash(hash: string): Promise<ApiKey | null>;\n\n /** List keys (masked, never includes hash), newest first. */\n listApiKeys(filter: ApiKeyFilter): Promise<ApiKeyMasked[]>;\n\n /** Get a single API key by ID (masked). */\n getApiKeyById(id: string): Promise<ApiKeyMasked | null>;\n\n /** Update name, scopes, roles, rate_limit, or expires_at. */\n updateApiKey(id: string, updates: UpdateApiKeyRequest): Promise<ApiKeyMasked | null>;\n\n /**\n * Soft-delete: set `revoked_at` to now. With `owner_uid`, only that\n * account's personal key is revoked — the answer is false for anyone\n * else's, so a route can 404 without saying whether the id exists.\n */\n revokeApiKey(id: string, owner_uid?: string): Promise<boolean>;\n\n /** Touch `last_used_at` to the current timestamp. */\n updateLastUsed(id: string): Promise<void>;\n}\n\n/**\n * Create an `ApiKeyStore` backed by the driver's SQL admin capability.\n *\n * Returns `undefined` if the driver does not support `executeSql`.\n */\nexport function createApiKeyStore(\n driver: DataDriver,\n options: {\n /**\n * Default `true`. `false` is a backend that changes nothing in its\n * database: `ensureTable` reads the table, creates, alters and revokes\n * nothing, and leaves a pre-scopes table's rows as they are.\n */\n provision?: boolean;\n } = {}\n): ApiKeyStore | undefined {\n const admin = driver.admin;\n if (!isSQLAdmin(admin)) {\n logger.warn(\"⚠️ [api-key-store] DataDriver does not support SQL admin — API keys will not be available.\");\n return undefined;\n }\n\n const exec = provisioningExec(\n (sqlText, execOptions) =>\n admin.executeSql(sqlText, execOptions?.params ? { params: execOptions.params } : undefined),\n options.provision\n );\n\n const ddl = createDdlBootstrapper(exec, \"api-key-store\");\n\n /**\n * Give every row stored before scopes existed the scopes it now holds.\n *\n * Idempotent and race-safe: a row is only written while its `scopes` is\n * still NULL, so two instances backfilling together write the same value\n * once. The `permissions` and `admin` columns are left as they are, so a\n * runtime that predates scopes still reads its own keys after a rollback.\n */\n async function backfillScopes(): Promise<void> {\n const rows = await exec(`SELECT id, permissions, admin FROM ${TABLE} WHERE scopes IS NULL`);\n for (const row of rows) {\n const { scopes, roles } = scopesFromStoredPermissions(\n parseStoredPermissions(row.permissions),\n row.admin === true\n );\n await exec(\n `UPDATE ${TABLE} SET scopes = $1::jsonb, roles = $2::jsonb WHERE id = $3 AND scopes IS NULL`,\n { params: [JSON.stringify(scopes), JSON.stringify(roles), row.id] }\n );\n }\n if (rows.length > 0) {\n logger.info(`[api-key-store] Gave ${rows.length} stored API key(s) their scopes.`);\n }\n }\n\n /**\n * Whether a boot has nothing to do, from {@link BOOT_STATE_SQL}. Every\n * statement below is a no-op on such a database, but not a free one: each\n * `ADD COLUMN IF NOT EXISTS` queues for the table's ACCESS EXCLUSIVE lock\n * before it finds the column there, each `CREATE INDEX IF NOT EXISTS` takes\n * a SHARE lock, and the `REVOKE` rewrites the table's catalogue row. Anything\n * missing, granted or legacy — or a read that fails, as it does on a driver\n * that is not Postgres — runs the full bootstrap, exactly as before.\n */\n async function bootIsCurrent(): Promise<boolean> {\n try {\n const row = (await exec(BOOT_STATE_SQL))[0];\n return row !== undefined\n && Number(row.present) === BOOT_RELATIONS.length\n && Number(row.stale) === 0\n && Number(row.columns) === BOOT_COLUMNS.length\n && Number(row.legacy) === 0\n && Number(row.granted) === 0\n && row.readable === true;\n } catch {\n return false;\n }\n }\n\n return {\n // ── Schema bootstrap ────────────────────────────────────────\n async ensureTable(): Promise<void> {\n // A warm boot: the table is complete and the end-user role holds\n // nothing on it. One read, no lock, no catalogue write.\n if (await bootIsCurrent()) {\n logger.debug(\"✅ API keys table ready\");\n return;\n }\n\n // Every statement here is idempotent, so losing the race to a peer\n // that booted at the same moment is survivable — but only if the\n // loser retries instead of abandoning everything below it. One\n // contained step each, so that a hard failure on any one of them\n // does not take the others with it. See `boot/ddl-bootstrap.ts`.\n await ddl.ensureObject(\"Creating schema rebase\", \"CREATE SCHEMA IF NOT EXISTS rebase\");\n\n await ddl.ensureObject(`Creating ${TABLE}`, `\n CREATE TABLE IF NOT EXISTS ${TABLE} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n name TEXT NOT NULL,\n kind TEXT NOT NULL DEFAULT 'service',\n key_prefix TEXT NOT NULL,\n key_hash TEXT NOT NULL UNIQUE,\n scopes JSONB,\n roles JSONB NOT NULL DEFAULT '[]'::jsonb,\n owner_uid TEXT,\n rate_limit INTEGER,\n created_by TEXT NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n last_used_at TIMESTAMPTZ,\n expires_at TIMESTAMPTZ,\n revoked_at TIMESTAMPTZ\n )\n `);\n\n // Every key lookup is `key_hash = $1`, which the UNIQUE (key_hash)\n // constraint's index answers. `idx_api_keys_hash` was a second btree\n // on the same column that every key creation paid to maintain; it is\n // no longer created, and dropped where an older boot left it — only\n // while the unique index is there to serve the lookups, and under a\n // one-second lock budget, since DROP INDEX locks the whole table.\n try {\n await exec(DROP_DUPLICATE_KEY_HASH_INDEX_SQL);\n } catch (err) {\n // Redundant, not wrong: a later boot drops it.\n if (!(err instanceof SchemaChangeRefusedError)) {\n logger.warn(`[api-key-store] Left the duplicate idx_api_keys_hash in place for now: ${\n err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n await ddl.ensureObject(\"Creating idx_api_keys_prefix\", `\n CREATE INDEX IF NOT EXISTS idx_api_keys_prefix\n ON ${TABLE}(key_prefix)\n `);\n\n // The columns a table created before scopes lacks. Idempotent and\n // raced like the rest — two instances running them together can\n // deadlock on the table's catalog lock, which `ensureObject` retries.\n await ddl.ensureObject(`Adding ${TABLE}.kind`, `\n ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS kind TEXT NOT NULL DEFAULT 'service'\n `);\n await ddl.ensureObject(`Adding ${TABLE}.scopes`, `\n ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS scopes JSONB\n `);\n await ddl.ensureObject(`Adding ${TABLE}.roles`, `\n ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS roles JSONB NOT NULL DEFAULT '[]'::jsonb\n `);\n await ddl.ensureObject(`Adding ${TABLE}.owner_uid`, `\n ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS owner_uid TEXT\n `);\n await ddl.ensureObject(\"Creating idx_api_keys_owner\", `\n CREATE INDEX IF NOT EXISTS idx_api_keys_owner\n ON ${TABLE}(owner_uid) WHERE owner_uid IS NOT NULL\n `);\n\n // Keyed on what exists, not on who created it, so an instance that\n // lost any race above still takes the table off the end-user role.\n if (!await ddl.isReadable(TABLE)) {\n logger.error(options.provision === false\n ? `❌ [api-key-store] ${TABLE} does not exist, and this backend changes nothing in its ` +\n \"database — every API-key authenticated request will be rejected on this instance \" +\n \"until a provisioning run creates it.\"\n : `❌ [api-key-store] ${TABLE} is unavailable — every API-key authenticated ` +\n \"request will be rejected on this instance, and the table could not be \" +\n \"taken back off the end-user role.\");\n return;\n }\n\n // This table holds key hashes and the scopes each grants, and it\n // has no RLS — it is not a collection. The Postgres driver grants\n // the authenticated role DML on everything in `rebase`, including\n // tables (like this one) created after that grant ran, so the\n // privilege has to come back off. Without it a user-context query\n // that reached this table could mint itself a key. A security\n // control, so every boot checks it: a boot that finds the role\n // holding anything on the table comes here and revokes it.\n await ddl.step(\"Revoking end-user access to api_keys\", () =>\n exec(revokeInternalTableSql(\"rebase\", \"api_keys\")));\n\n // Only a table that has the old columns has rows to backfill.\n const legacyColumns = await exec(\n `SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'rebase' AND table_name = 'api_keys' AND column_name = 'permissions'`\n );\n // A table an older release made reads as present and then fails the\n // first query that names a column it never got. Said at boot, by\n // name, since nothing here is allowed to add them.\n if (options.provision === false) {\n try {\n await exec(`SELECT ${BOOT_COLUMNS.join(\", \")} FROM ${TABLE} WHERE false`);\n } catch {\n logger.error(\n `❌ [api-key-store] ${TABLE} is out of date — it lacks one of ${BOOT_COLUMNS.join(\", \")}, ` +\n \"which this release reads. This backend changes nothing in its database; a provisioning \" +\n \"run adds them.\"\n );\n }\n }\n\n if (legacyColumns.length > 0 && options.provision === false) {\n // A backfill is a migration of rows, and the run that migrates\n // the table is the one that gives them their scopes.\n logger.warn(\n `⚠️ [api-key-store] ${TABLE} still has the pre-scopes \\`permissions\\` column. Keys stored ` +\n \"before scopes are given theirs by a provisioning run; this backend changes nothing in its database.\"\n );\n } else if (legacyColumns.length > 0) {\n await ddl.step(\"Backfilling API key scopes\", backfillScopes);\n }\n\n logger.debug(\"✅ API keys table ready\");\n },\n\n // ── Create ──────────────────────────────────────────────────\n async createApiKey(key: NewApiKey, createdBy: string): Promise<ApiKeyWithSecret> {\n const plaintext = generateApiKey();\n const hash = hashKey(plaintext);\n const prefix = keyPrefix(plaintext);\n\n const rows = await exec(\n `INSERT INTO ${TABLE} (name, kind, key_prefix, key_hash, scopes, roles, owner_uid, rate_limit, created_by, expires_at)\n VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7, $8, $9, $10)\n RETURNING *`,\n { params: [\n key.name,\n key.kind,\n prefix,\n hash,\n JSON.stringify(key.scopes),\n JSON.stringify(key.kind === \"personal\" ? [] : key.roles),\n key.owner_uid,\n key.rate_limit,\n createdBy,\n key.expires_at\n ] }\n );\n\n const apiKey = rowToApiKey(rows[0]);\n return {\n ...toMasked(apiKey),\n key: plaintext\n };\n },\n\n // ── Lookup by hash ──────────────────────────────────────────\n async findByKeyHash(hash: string): Promise<ApiKey | null> {\n const rows = await exec(\n `SELECT * FROM ${TABLE}\n WHERE key_hash = $1\n LIMIT 1`,\n { params: [hash] }\n );\n if (rows.length === 0) return null;\n return rowToApiKey(rows[0]);\n },\n\n // ── List (masked) ───────────────────────────────────────────\n async listApiKeys(filter: ApiKeyFilter): Promise<ApiKeyMasked[]> {\n const rows = filter.kind === \"personal\"\n ? await exec(\n `SELECT * FROM ${TABLE}\n WHERE kind = 'personal' AND owner_uid = $1\n ORDER BY created_at DESC`,\n { params: [filter.owner_uid] }\n )\n : await exec(`\n SELECT * FROM ${TABLE}\n WHERE kind = 'service'\n ORDER BY created_at DESC\n `);\n return rows.map(r => toMasked(rowToApiKey(r)));\n },\n\n // ── Get by ID (masked) ──────────────────────────────────────\n async getApiKeyById(id: string): Promise<ApiKeyMasked | null> {\n const rows = await exec(\n `SELECT * FROM ${TABLE}\n WHERE id = $1\n LIMIT 1`,\n { params: [id] }\n );\n if (rows.length === 0) return null;\n return toMasked(rowToApiKey(rows[0]));\n },\n\n // ── Update ──────────────────────────────────────────────────\n async updateApiKey(id: string, updates: UpdateApiKeyRequest): Promise<ApiKeyMasked | null> {\n const setClauses: string[] = [];\n const params: unknown[] = [];\n let paramIdx = 1;\n\n if (updates.name !== undefined) {\n setClauses.push(`name = $${paramIdx++}`);\n params.push(updates.name);\n }\n if (updates.scopes !== undefined) {\n setClauses.push(`scopes = $${paramIdx++}::jsonb`);\n params.push(JSON.stringify(updates.scopes));\n }\n if (updates.roles !== undefined) {\n setClauses.push(`roles = $${paramIdx++}::jsonb`);\n params.push(JSON.stringify(updates.roles));\n }\n if (updates.rate_limit !== undefined) {\n if (updates.rate_limit !== null) {\n setClauses.push(`rate_limit = $${paramIdx++}`);\n params.push(updates.rate_limit);\n } else {\n setClauses.push(\"rate_limit = NULL\");\n }\n }\n if (updates.expires_at !== undefined) {\n if (updates.expires_at !== null) {\n setClauses.push(`expires_at = $${paramIdx++}`);\n params.push(updates.expires_at);\n } else {\n setClauses.push(\"expires_at = NULL\");\n }\n }\n\n if (setClauses.length === 0) {\n return this.getApiKeyById(id);\n }\n\n setClauses.push(\"updated_at = NOW()\");\n\n // The WHERE id = $N uses the next available param index\n params.push(id);\n\n const rows = await exec(\n `UPDATE ${TABLE}\n SET ${setClauses.join(\", \")}\n WHERE id = $${paramIdx} AND kind = 'service'\n RETURNING *`,\n { params }\n );\n\n if (rows.length === 0) return null;\n return toMasked(rowToApiKey(rows[0]));\n },\n\n // ── Revoke (soft-delete) ────────────────────────────────────\n async revokeApiKey(id: string, owner_uid?: string): Promise<boolean> {\n const rows = owner_uid === undefined\n ? await exec(\n `UPDATE ${TABLE}\n SET revoked_at = NOW(), updated_at = NOW()\n WHERE id = $1 AND kind = 'service' AND revoked_at IS NULL\n RETURNING id`,\n { params: [id] }\n )\n : await exec(\n `UPDATE ${TABLE}\n SET revoked_at = NOW(), updated_at = NOW()\n WHERE id = $1 AND kind = 'personal' AND owner_uid = $2 AND revoked_at IS NULL\n RETURNING id`,\n { params: [id, owner_uid] }\n );\n return rows.length > 0;\n },\n\n // ── Touch last_used_at ──────────────────────────────────────\n async updateLastUsed(id: string): Promise<void> {\n try {\n await exec(\n `UPDATE ${TABLE}\n SET last_used_at = NOW()\n WHERE id = $1`,\n { params: [id] }\n );\n } catch (err) {\n // Non-blocking — don't fail requests because of a usage timestamp update\n logger.error(\"[api-key-store] Failed to update last_used_at\", { error: err });\n }\n }\n };\n}\n","/**\n * What a new or changed key may hold, decided against whoever asked for it.\n *\n * One rule for every door that mints a credential: **nothing is minted with\n * more than its minter holds.** A key's scopes must be within the creator's\n * own; a service key's RLS roles must be roles the creator holds, unless the\n * creator is an admin; and `keys:*` never goes on a key at all.\n *\n * @module\n */\n\nimport {\n hasAdminRole,\n KEY_MANAGEMENT_SCOPES,\n knownScopes,\n parseScope,\n roleNameProblem,\n scopesBeyond,\n SCOPE_TARGETS,\n isBuiltInScope,\n validateScopes,\n type AccessModel,\n type ScopeTargetKind\n} from \"@rebasepro/types\";\nimport { ApiError } from \"../../api/errors\";\n\n/**\n * The targets this backend serves, for refusing a key narrowed to something\n * that does not exist — a typo there is a key that silently reaches nothing.\n * Each list is read when a key is minted, so it reflects what is loaded then.\n */\nexport interface KeyTargets {\n collections(): readonly string[];\n functions(): readonly string[];\n buckets(): readonly string[];\n}\n\nconst TARGET_LISTS: Record<ScopeTargetKind, keyof KeyTargets> = {\n collection: \"collections\",\n bucket: \"buckets\",\n function: \"functions\"\n};\n\n/** Read a `scopes` body field: a non-empty array of strings, deduplicated. */\nexport function readScopesField(value: unknown): string[] {\n if (!Array.isArray(value) || value.length === 0 || value.some(entry => typeof entry !== \"string\")) {\n throw ApiError.badRequest(\n \"scopes must be a non-empty array of scope strings, e.g. [\\\"data:read\\\", \\\"data:write:posts\\\"]\",\n \"INVALID_INPUT\"\n );\n }\n return [...new Set(value.map(entry => entry.trim()))];\n}\n\n/**\n * Read a `roles` body field: an array of role names, trimmed, deduplicated,\n * without `service`. A key's roles reach RLS joined by `,` in `app.user_roles`,\n * so a name `roleNameProblem` refuses — `\"editor,admin\"` — is refused here too.\n */\nexport function readRolesField(value: unknown): string[] {\n if (!Array.isArray(value) || value.some(entry => typeof entry !== \"string\" || entry.trim() === \"\")) {\n throw ApiError.badRequest(\"roles must be an array of role names\", \"INVALID_INPUT\");\n }\n const trimmed = value.map(entry => String(entry).trim());\n const problems = trimmed.flatMap(name => {\n const problem = roleNameProblem(name);\n return problem ? [`${JSON.stringify(name)} ${problem}`] : [];\n });\n if (problems.length > 0) {\n throw new ApiError(400, \"INVALID_ROLE\",\n `Not valid role names: ${problems.join(\"; \")}. A role name is a non-empty string without , { } \" \\\\ or control characters.`,\n { problems });\n }\n // `service` is every service key's identity already; listing it adds nothing.\n return [...new Set(trimmed)].filter(role => role !== \"service\");\n}\n\n/**\n * Refuse scopes that are malformed, unknown, aimed at a target that does not\n * exist, for key management, or beyond what `minterScopes` covers.\n */\nexport function assertScopesGrantable(\n requested: readonly string[],\n minterScopes: readonly string[],\n model: AccessModel,\n targets?: KeyTargets\n): void {\n // `keys:*` are scopes, and no key may hold one, so the list a refusal\n // offers leaves them out: offering them sent the next attempt to\n // KEY_MANAGEMENT_SCOPE.\n const keyManagement: ReadonlySet<string> = new Set(KEY_MANAGEMENT_SCOPES);\n const problems = validateScopes(requested, model);\n if (problems.length > 0) {\n const grantable = knownScopes(model).filter(scope => !keyManagement.has(scope));\n throw new ApiError(400, \"INVALID_SCOPES\",\n `Not valid scopes: ${problems.map(p => `\"${p.scope}\" (${p.reason})`).join(\", \")}. ` +\n `Valid scopes are ${grantable.join(\", \")}; data, storage and function scopes take a ` +\n \"target after a second colon, e.g. \\\"data:read:posts\\\".\",\n { problems, validScopes: grantable });\n }\n\n const management = requested.filter(entry => keyManagement.has(parseScope(entry)?.scope ?? \"\"));\n if (management.length > 0) {\n throw new ApiError(400, \"KEY_MANAGEMENT_SCOPE\",\n `${management.join(\", \")} cannot go on a key: a key that manages keys could mint its own successor. ` +\n \"Manage keys as a person, or with the service key.\",\n { scopes: management });\n }\n\n if (targets) {\n const unknown: string[] = [];\n for (const entry of requested) {\n const parsed = parseScope(entry);\n if (!parsed?.target || !isBuiltInScope(parsed.scope)) continue;\n const kind = SCOPE_TARGETS[parsed.scope];\n if (!kind) continue;\n if (!targets[TARGET_LISTS[kind]]().includes(parsed.target)) unknown.push(entry);\n }\n if (unknown.length > 0) {\n throw new ApiError(400, \"UNKNOWN_SCOPE_TARGET\",\n `These scopes name something this backend does not serve: ${unknown.join(\", \")}. ` +\n \"A target is a collection slug, a storage source id or a function name.\",\n {\n scopes: unknown,\n collections: targets.collections(),\n buckets: targets.buckets(),\n functions: targets.functions()\n });\n }\n }\n\n const beyond = scopesBeyond(requested, minterScopes);\n if (beyond.length > 0) {\n throw new ApiError(403, \"SCOPE_EXCEEDS_CREATOR\",\n `A key cannot hold more than the account creating it, and you do not hold ${beyond.join(\", \")}.`,\n { scopes: beyond });\n }\n}\n\n/**\n * Refuse RLS roles the minter does not hold. An admin may give any role — they\n * already read every row — and anyone else only their own.\n */\nexport function assertRolesGrantable(requested: readonly string[], minterRoles: readonly string[]): void {\n if (hasAdminRole(minterRoles)) return;\n const beyond = requested.filter(role => !minterRoles.includes(role));\n if (beyond.length > 0) {\n throw new ApiError(403, \"ROLE_EXCEEDS_CREATOR\",\n `A key cannot run as a role the account creating it does not hold: ${beyond.join(\", \")}.`,\n { roles: beyond });\n }\n}\n\n/**\n * Refuse changing or revoking a key that holds more than the caller — a role\n * they do not hold, or a scope beyond theirs. An admin manages any key.\n *\n * The same ceiling as minting, applied to the key as it already is: a\n * `keys:write` role could otherwise widen an admin-role key it could never\n * have minted to every data scope, or revoke the key an admin's integration\n * runs on. `assertMayManageAccount` is the same rule for accounts.\n */\nexport function assertMayManageKey(\n existing: { roles: readonly string[]; scopes: readonly string[] },\n managerRoles: readonly string[],\n managerScopes: readonly string[]\n): void {\n if (hasAdminRole(managerRoles)) return;\n const roles = existing.roles.filter(role => !managerRoles.includes(role));\n const scopes = scopesBeyond(existing.scopes, managerScopes);\n if (roles.length > 0 || scopes.length > 0) {\n throw new ApiError(403, \"KEY_OUTRANKS_CALLER\",\n `This key holds ${[...roles.map(role => `the role ${role}`), ...scopes].join(\", \")}, which you do not, ` +\n \"so you cannot change or revoke it.\",\n { roles, scopes });\n }\n}\n\n/** Read an optional `expires_at`: absent, null, or a future ISO-8601 instant. */\nexport function readExpiresAt(value: unknown, requireFuture: boolean): string | null | undefined {\n if (value === undefined) return undefined;\n if (value === null) return null;\n const parsed = typeof value === \"string\" ? new Date(value) : new Date(NaN);\n if (isNaN(parsed.getTime())) {\n throw ApiError.badRequest(\"expires_at must be a valid ISO-8601 date\", \"INVALID_INPUT\");\n }\n if (requireFuture && parsed <= new Date()) {\n throw ApiError.badRequest(\"expires_at must be in the future\", \"INVALID_INPUT\");\n }\n return parsed.toISOString();\n}\n\n/** Read an optional `rate_limit`: absent, null, or a positive integer. */\nexport function readRateLimit(value: unknown): number | null | undefined {\n if (value === undefined) return undefined;\n if (value === null) return null;\n if (typeof value !== \"number\" || value < 1 || !Number.isInteger(value)) {\n throw ApiError.badRequest(\"rate_limit must be a positive integer or null\", \"INVALID_INPUT\");\n }\n return value;\n}\n\n/** Read a key name: a non-empty string, trimmed. */\nexport function readName(value: unknown): string {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw ApiError.badRequest(\"Name is required\", \"INVALID_INPUT\");\n }\n return value.trim();\n}\n","/**\n * Routes for managing API keys.\n *\n * - {@link createApiKeyRoutes} — the project's service keys, under\n * `/api/admin/api-keys`, for holders of `keys:read` / `keys:write`.\n * - {@link createPersonalKeyRoutes} — the caller's own personal keys, under\n * `/api/auth/keys`, when the app enables `auth.personalKeys`.\n *\n * All routes return masked keys (never the hash). The full plaintext key is\n * returned exactly once, in the response that creates it. No API key may call\n * either router.\n *\n * @module\n */\n\nimport { Hono, type Context, type MiddlewareHandler } from \"hono\";\nimport type { AccessModel } from \"@rebasepro/types\";\nimport { ApiError, errorHandler } from \"../../api/errors\";\nimport { createRequireAuth } from \"../middleware\";\nimport { accessOf, callerScopes, requireScopeByMethod } from \"../access\";\nimport type { HonoEnv } from \"../../api/types\";\nimport type { ApiKeyStore } from \"./api-key-store\";\nimport type { UpdateApiKeyRequest } from \"./api-key-types\";\nimport {\n assertMayManageKey,\n assertRolesGrantable,\n assertScopesGrantable,\n readExpiresAt,\n readName,\n readRateLimit,\n readRolesField,\n readScopesField,\n type KeyTargets\n} from \"./key-grant\";\n\nexport interface ApiKeyRouteOptions {\n store: ApiKeyStore;\n serviceKey?: string;\n /**\n * Read the caller's roles from the database rather than from their token.\n *\n * A key minted here may outlive the session that minted it, so the roles\n * that bound what it may hold must be the caller's roles now — not the\n * ones a token issued before a demotion still claims. See\n * `createRequireAuth`.\n */\n resolveRoles?: (uid: string) => Promise<string[]>;\n /** Repository for the token-revocation watermark. See `createRequireAuth`. */\n revocationRepo?: import(\"../token-revocation\").AccessJudgeRepository;\n /** What a scope's target may name. Unset, targets are not checked for existence. */\n targets?: KeyTargets;\n /**\n * The access model to validate scopes against. Defaults to the one of the\n * backend answering the request.\n */\n accessModel?: () => AccessModel;\n}\n\n/**\n * Refuse API-key-authenticated requests to the key-management routes.\n *\n * A key that could manage keys could mint its own successor, widen itself, or\n * revoke the keys other integrations run on — and revoking it would undo none\n * of that, because the keys it minted are ordinary rows with no link back to\n * it. So key management is for a person, or the service key. Reads are\n * refused alongside writes: a listing of every key's name, prefix and scopes\n * is reconnaissance for exactly that.\n *\n * `keys:*` can never be granted to a key either (`key-grant.ts`); this guard is\n * what makes the rule hold for a key minted before that, and for personal\n * keys, whose routes need no scope.\n */\nconst rejectApiKeyAuth: MiddlewareHandler<HonoEnv> = async (c, next) => {\n if (c.get(\"apiKey\")) {\n // Names where the service key comes from, because \"use the service key\"\n // sent readers looking for something they had no way to obtain: it is\n // injected by the platform, so it is not in their config, and it is\n // reserved, so `env set` refuses it.\n throw ApiError.forbidden(\n \"API keys cannot manage API keys. Authenticate as a person, or use the service key — \" +\n \"REBASE_SERVICE_KEY in this server's environment, or `rebase cloud env reveal REBASE_SERVICE_KEY` \" +\n \"on Rebase Cloud.\",\n \"API_KEY_SELF_MANAGEMENT_FORBIDDEN\"\n );\n }\n return next();\n};\n\n/** The caller's uid and roles, as the auth gate resolved them. */\nfunction caller(c: Parameters<MiddlewareHandler<HonoEnv>>[0]): { uid: string; roles: string[]; isAnonymous: boolean } {\n const user = c.get(\"user\");\n if (!user || typeof user !== \"object\" || !(\"uid\" in user) || typeof user.uid !== \"string\") {\n throw ApiError.unauthenticated(\"Authentication required\");\n }\n const roles = \"roles\" in user && Array.isArray(user.roles)\n ? user.roles.filter((role): role is string => typeof role === \"string\")\n : [];\n const isAnonymous = \"isAnonymous\" in user && user.isAnonymous === true;\n return { uid: user.uid, roles, isAnonymous };\n}\n\nasync function readBody(c: Parameters<MiddlewareHandler<HonoEnv>>[0]): Promise<Record<string, unknown>> {\n let body: unknown;\n try {\n body = await c.req.json();\n } catch {\n throw ApiError.badRequest(\"Request body must be JSON\", \"INVALID_INPUT\");\n }\n if (typeof body !== \"object\" || body === null || Array.isArray(body)) {\n throw ApiError.badRequest(\"Request body must be a JSON object\", \"INVALID_INPUT\");\n }\n return Object.fromEntries(Object.entries(body));\n}\n\n/**\n * The project's service keys: `GET` needs `keys:read`, everything else\n * `keys:write`.\n */\nexport function createApiKeyRoutes(options: ApiKeyRouteOptions): Hono<HonoEnv> {\n const { store, serviceKey, resolveRoles, revocationRepo, targets } = options;\n const model = (c: Context<HonoEnv>): AccessModel => options.accessModel?.() ?? accessOf(c).model;\n const router = new Hono<HonoEnv>();\n\n router.onError(errorHandler);\n\n router.use(\"/*\", createRequireAuth({ serviceKey, resolveRoles, revocationRepo }));\n // Before the scope check, so a key gets the reason it was refused rather\n // than a missing scope that implies a wider key would work.\n router.use(\"/*\", rejectApiKeyAuth);\n router.use(\"/*\", requireScopeByMethod({ read: \"keys:read\", write: \"keys:write\" }));\n\n const existingServiceKey = async (id: string) => {\n const key = await store.getApiKeyById(id);\n if (!key || key.kind !== \"service\") {\n throw ApiError.notFound(\"API key not found\");\n }\n return key;\n };\n\n router.get(\"/\", async (c) => {\n const keys = await store.listApiKeys({ kind: \"service\" });\n return c.json({ keys });\n });\n\n router.post(\"/\", async (c) => {\n const body = await readBody(c);\n const name = readName(body.name);\n const scopes = readScopesField(body.scopes);\n const roles = body.roles === undefined ? [] : readRolesField(body.roles);\n const rate_limit = readRateLimit(body.rate_limit) ?? null;\n const expires_at = readExpiresAt(body.expires_at, true) ?? null;\n const minter = caller(c);\n\n assertScopesGrantable(scopes, callerScopes(c), model(c), targets);\n assertRolesGrantable(roles, minter.roles);\n\n const key = await store.createApiKey({\n name,\n kind: \"service\",\n scopes,\n roles,\n owner_uid: null,\n rate_limit,\n expires_at\n }, minter.uid);\n\n return c.json({ key }, 201);\n });\n\n router.get(\"/:id\", async (c) => {\n const key = await store.getApiKeyById(c.req.param(\"id\"));\n if (!key || key.kind !== \"service\") {\n throw ApiError.notFound(\"API key not found\");\n }\n return c.json({ key });\n });\n\n router.put(\"/:id\", async (c) => {\n const id = c.req.param(\"id\");\n const body = await readBody(c);\n const minter = caller(c);\n const updates: UpdateApiKeyRequest = {};\n\n if (body.name !== undefined) updates.name = readName(body.name);\n if (body.scopes !== undefined) {\n updates.scopes = readScopesField(body.scopes);\n assertScopesGrantable(updates.scopes, callerScopes(c), model(c), targets);\n }\n if (body.roles !== undefined) {\n updates.roles = readRolesField(body.roles);\n assertRolesGrantable(updates.roles, minter.roles);\n }\n const rateLimit = readRateLimit(body.rate_limit);\n if (rateLimit !== undefined) updates.rate_limit = rateLimit;\n const expiresAt = readExpiresAt(body.expires_at, false);\n if (expiresAt !== undefined) updates.expires_at = expiresAt;\n\n // The key as it is, too: nobody changes a key that holds more than they do.\n assertMayManageKey(await existingServiceKey(id), minter.roles, callerScopes(c));\n\n const key = await store.updateApiKey(id, updates);\n if (!key) {\n throw ApiError.notFound(\"API key not found\");\n }\n return c.json({ key });\n });\n\n router.delete(\"/:id\", async (c) => {\n // Nor revokes one: that is the integration an administrator runs on it.\n assertMayManageKey(await existingServiceKey(c.req.param(\"id\")), caller(c).roles, callerScopes(c));\n const revoked = await store.revokeApiKey(c.req.param(\"id\"));\n if (!revoked) {\n throw ApiError.notFound(\"API key not found or already revoked\");\n }\n return c.json({ success: true });\n });\n\n return router;\n}\n\nexport interface PersonalKeyRouteOptions {\n store: ApiKeyStore;\n /** Whether the app enabled `auth.personalKeys`. Off, every route explains how to turn it on. */\n enabled: boolean;\n /** Recognised only to be refused with a reason: the service key has no account. */\n serviceKey?: string;\n resolveRoles?: (uid: string) => Promise<string[]>;\n revocationRepo?: import(\"../token-revocation\").AccessJudgeRepository;\n targets?: KeyTargets;\n accessModel?: () => AccessModel;\n}\n\n/**\n * The caller's own keys. Each acts as the caller — their account, their roles\n * as they are when the key is used — and holds no scope the caller does not.\n *\n * For a signed-in account only: not a key, not the service key (it has no\n * account to act as) and not a guest, whose account is one sign-out from gone.\n */\nexport function createPersonalKeyRoutes(options: PersonalKeyRouteOptions): Hono<HonoEnv> {\n const { store, enabled, serviceKey, resolveRoles, revocationRepo, targets } = options;\n const model = (c: Context<HonoEnv>): AccessModel => options.accessModel?.() ?? accessOf(c).model;\n const router = new Hono<HonoEnv>();\n\n router.onError(errorHandler);\n\n if (!enabled) {\n router.all(\"/*\", () => {\n throw ApiError.forbidden(\n \"Personal API keys are switched off on this backend. Set `personalKeys: true` in the \" +\n \"auth block of the users collection to let accounts create them.\",\n \"PERSONAL_KEYS_DISABLED\"\n );\n });\n return router;\n }\n\n router.use(\"/*\", createRequireAuth({ serviceKey, resolveRoles, revocationRepo }));\n router.use(\"/*\", rejectApiKeyAuth);\n router.use(\"/*\", async (c, next) => {\n const me = caller(c);\n if (me.uid === \"service\") {\n throw ApiError.forbidden(\n \"The service key has no account for a personal key to act as. Create a service key \" +\n \"under /api/admin/api-keys instead.\",\n \"PERSONAL_KEY_NEEDS_ACCOUNT\"\n );\n }\n if (me.isAnonymous) {\n throw ApiError.forbidden(\n \"A guest session cannot create API keys. Sign in to an account first.\",\n \"PERSONAL_KEY_NEEDS_ACCOUNT\"\n );\n }\n return next();\n });\n\n router.get(\"/\", async (c) => {\n const keys = await store.listApiKeys({ kind: \"personal\", owner_uid: caller(c).uid });\n return c.json({ keys });\n });\n\n router.post(\"/\", async (c) => {\n const body = await readBody(c);\n if (body.roles !== undefined) {\n throw ApiError.badRequest(\n \"A personal key runs as your account's roles and takes none of its own.\",\n \"INVALID_INPUT\"\n );\n }\n if (body.rate_limit !== undefined) {\n throw ApiError.badRequest(\n \"A personal key uses the server's API-key rate limit and cannot set its own.\",\n \"INVALID_INPUT\"\n );\n }\n const name = readName(body.name);\n const scopes = readScopesField(body.scopes);\n const expires_at = readExpiresAt(body.expires_at, true) ?? null;\n const me = caller(c);\n\n assertScopesGrantable(scopes, callerScopes(c), model(c), targets);\n\n const key = await store.createApiKey({\n name,\n kind: \"personal\",\n scopes,\n roles: [],\n owner_uid: me.uid,\n rate_limit: null,\n expires_at\n }, me.uid);\n\n return c.json({ key }, 201);\n });\n\n router.delete(\"/:id\", async (c) => {\n const revoked = await store.revokeApiKey(c.req.param(\"id\"), caller(c).uid);\n if (!revoked) {\n throw ApiError.notFound(\"API key not found or already revoked\");\n }\n return c.json({ success: true });\n });\n\n return router;\n}\n","import { z } from \"zod\";\n\n/**\n * The authorization-code request shape every OAuth provider accepts, and the\n * two derivations of it that were previously copy-pasted twelve times.\n *\n * Twelve providers declared their own `z.object({ code, redirectUri })` and\n * each decided independently whether to accept a PKCE verifier (one did) and\n * how to read a verification signal off the provider's profile response (five\n * skipped the question and hardcoded `true`). One predicate, twelve\n * implementations — so the controls below live here and every provider calls\n * them, which is what makes it impossible for a thirteenth provider to ship\n * without them.\n */\n\n/** The request body every authorization-code provider consumes. */\nexport interface OAuthCodeFlowPayload {\n /** Authorization code returned to the client by the provider. */\n code: string;\n /**\n * Redirect URI the code was issued against. Echoed to the token endpoint,\n * which is what binds the code to the client that started the flow — and\n * why the route allowlists it before `verify` ever runs\n * (see `isRedirectUriAllowed`).\n */\n redirectUri: string;\n /**\n * PKCE verifier, when the client started the flow with a\n * `code_challenge`. Forwarded to the token endpoint verbatim.\n */\n codeVerifier?: string;\n}\n\n/**\n * Build the request schema for an authorization-code provider.\n *\n * `pkce` defaults to `\"optional\"`: the client decides whether it sent a\n * `code_challenge`, and providers that do not implement PKCE simply never see\n * a verifier because their clients never generate one. `\"required\"` is for\n * providers that mandate PKCE (Twitter/X).\n */\nexport function oauthCodeFlowSchema(opts: { pkce?: \"required\" | \"optional\" } = {}) {\n const verifier = opts.pkce === \"required\"\n ? z.string().min(1, \"PKCE code verifier is required\")\n : z.string().min(1).optional();\n\n return z.object({\n code: z.string().min(1, \"Auth code is required\"),\n redirectUri: z.string().url(\"Valid redirect URI is required\"),\n codeVerifier: verifier\n });\n}\n\n/**\n * The `code_verifier` token-endpoint parameter, or nothing.\n *\n * Returned as a spreadable object so a provider adds PKCE with one `...` in\n * its existing body literal, rather than branching.\n */\nexport function pkceTokenParams(codeVerifier?: string): Record<string, string> {\n return codeVerifier ? { code_verifier: codeVerifier } : {};\n}\n\n/**\n * Normalise a provider's email-verification signal into the boolean that\n * `OAuthProviderProfile.emailVerified` promises.\n *\n * The whole point is the default: anything that is not an affirmative\n * verification signal — `undefined`, `null`, a missing field, an empty string,\n * `false` — becomes `false`. A provider that reports nothing therefore cannot\n * accidentally assert that it verified the address, which is exactly the\n * mistake five providers had made.\n *\n * Accepts the string forms because OIDC issuers are inconsistent about whether\n * `email_verified` is a JSON boolean or a string.\n */\nexport function providerVerifiedEmail(signal: unknown): boolean {\n if (signal === true) return true;\n if (typeof signal === \"string\") {\n const normalized = signal.trim().toLowerCase();\n return normalized === \"true\" || normalized === \"1\";\n }\n return false;\n}\n","import type { OAuthProvider, OAuthProviderProfile } from \"./interfaces\";\nimport { z } from \"zod\";\nimport { logger } from \"../utils/logger\";\nimport { pkceTokenParams, providerVerifiedEmail } from \"./oauth-code-flow\";\n\nlet _googleAuth: typeof import(\"google-auth-library/build/src/index.js\") | undefined;\n\nasync function loadGoogleAuth() {\n if (!_googleAuth) {\n try {\n _googleAuth = await import(\"google-auth-library/build/src/index.js\");\n } catch {\n throw new Error(\n \"google-auth-library is required for Google OAuth. \" +\n \"Install it: pnpm add google-auth-library\"\n );\n }\n }\n return _googleAuth;\n}\n\n/**\n * Verify a Google **access token** and resolve it to a profile.\n *\n * SECURITY: an access token must be checked against Google's `tokeninfo`\n * endpoint so we can confirm its `aud` (the OAuth client the token was issued\n * to) matches *our* `clientId`. The `userinfo` endpoint does **not** verify\n * audience — it returns a profile for any valid Google access token — so\n * trusting it alone lets an attacker present a token they obtained for a\n * *different* client (e.g. their own app, via a phished consent) and log in as\n * whatever account that token belongs to. The `aud` check is what binds the\n * token to this application.\n *\n * Identity (`sub` / `email` / `email_verified`) is taken from the\n * audience-checked `tokeninfo` response. `userinfo` is used only, best-effort,\n * to enrich display fields (name, picture).\n */\nasync function verifyGoogleAccessToken(\n accessToken: string,\n expectedClientId: string\n): Promise<OAuthProviderProfile> {\n const tokenInfoRes = await fetch(\n `https://oauth2.googleapis.com/tokeninfo?access_token=${encodeURIComponent(accessToken)}`\n );\n if (!tokenInfoRes.ok) {\n throw new Error(`Google tokeninfo request failed with status ${tokenInfoRes.status}`);\n }\n const tokenInfo = await tokenInfoRes.json() as {\n aud?: string;\n sub?: string;\n email?: string;\n // The legacy tokeninfo endpoint returns this as the string \"true\"/\"false\".\n email_verified?: string | boolean;\n };\n\n if (tokenInfo.aud !== expectedClientId) {\n throw new Error(\n \"Google access token audience mismatch: the token was not issued for this application\"\n );\n }\n if (!tokenInfo.sub || !tokenInfo.email) {\n throw new Error(\"Google access token missing sub or email (the email scope is required)\");\n }\n\n const emailVerified = providerVerifiedEmail(tokenInfo.email_verified);\n\n let displayName: string | null = null;\n let photoUrl: string | null = null;\n try {\n const res = await fetch(\n \"https://www.googleapis.com/oauth2/v3/userinfo\",\n { headers: { Authorization: `Bearer ${accessToken}` } }\n );\n if (res.ok) {\n const info = await res.json() as { name?: string; picture?: string };\n displayName = info.name || null;\n photoUrl = info.picture || null;\n }\n } catch {\n // Non-fatal: identity is already established from tokeninfo.\n }\n\n return {\n providerId: tokenInfo.sub,\n email: tokenInfo.email,\n displayName,\n photoUrl,\n emailVerified\n };\n}\n\nexport interface GoogleUserInfo {\n googleId: string;\n email: string;\n displayName: string | null;\n photoUrl: string | null;\n emailVerified: boolean;\n}\n\nexport interface GoogleProviderConfig {\n clientId: string;\n /**\n * The OAuth 2.0 client secret from Google Cloud Console.\n *\n * Required for the **authorization code flow** (Path 3), where the\n * frontend sends an authorization `code` and the backend exchanges it\n * server-side for tokens. This is the most secure flow because tokens\n * never touch the browser.\n *\n * When omitted, only ID-token and access-token verification are available\n * (Paths 1 & 2), which rely on the frontend obtaining tokens directly.\n */\n clientSecret?: string;\n}\n\n/**\n * Creates a Google OAuth Provider integration.\n *\n * Supports three verification paths:\n *\n * **Path 1 – ID Token** (One Tap / Sign In With Google button):\n * Frontend sends `idToken`. Backend verifies cryptographically using\n * Google's public keys. No secret required.\n *\n * **Path 2 – Access Token** (popup via `initTokenClient`):\n * Frontend sends `accessToken`. Backend validates by calling Google's\n * userinfo endpoint. No secret required.\n *\n * **Path 3 – Authorization Code** (most secure, requires `clientSecret`):\n * Frontend sends `code` + `redirectUri`. Backend exchanges the code\n * server-side for an ID token using `clientId` + `clientSecret`, then\n * verifies the ID token. Tokens never touch the browser.\n */\nexport function createGoogleProvider(config: GoogleProviderConfig | string): OAuthProvider<{\n idToken?: string;\n accessToken?: string;\n code?: string;\n redirectUri?: string;\n codeVerifier?: string;\n}> {\n const clientId = typeof config === \"string\" ? config : config.clientId;\n const clientSecret = typeof config === \"string\" ? undefined : config.clientSecret;\n\n // The admin panel's login button uses the authorization-code flow, which\n // needs the secret. Without it the provider still advertises itself, the\n // button still renders, and the failure only surfaces when someone clicks\n // it — so say it at boot instead.\n if (!clientSecret) {\n console.warn(\n \"[Rebase] Google provider configured without a client secret. ID-token and access-token \"\n + \"sign-in still work, but the authorization-code flow used by the admin login button will \"\n + \"fail. Set GOOGLE_CLIENT_SECRET to enable it.\"\n );\n }\n let googleClient: InstanceType<typeof import(\"google-auth-library/build/src/index.js\").OAuth2Client> | undefined;\n\n async function getClient() {\n if (!googleClient) {\n const { OAuth2Client } = await loadGoogleAuth();\n googleClient = new OAuth2Client(clientId, clientSecret);\n }\n return googleClient;\n }\n\n return {\n id: \"google\",\n schema: z.object({\n idToken: z.string().min(1).optional(),\n accessToken: z.string().min(1).optional(),\n code: z.string().min(1).optional(),\n redirectUri: z.string().min(1).optional(),\n codeVerifier: z.string().min(1).optional()\n }).refine(\n (data) => data.idToken || data.accessToken || (data.code && data.redirectUri),\n { message: \"One of idToken, accessToken, or code+redirectUri is required\" }\n ),\n verify: async (payload: {\n idToken?: string;\n accessToken?: string;\n code?: string;\n redirectUri?: string;\n codeVerifier?: string;\n }): Promise<OAuthProviderProfile | null> => {\n try {\n // Path 1: verify an ID token (One Tap / renderButton)\n if (payload.idToken) {\n const client = await getClient();\n const ticket = await client.verifyIdToken({\n idToken: payload.idToken,\n audience: clientId\n });\n\n const content = ticket.getPayload();\n if (!content) {\n throw new Error(\"Google ID token payload was empty\");\n }\n\n return {\n providerId: content.sub,\n email: content.email || \"\",\n displayName: content.name || null,\n photoUrl: content.picture || null,\n emailVerified: providerVerifiedEmail(content.email_verified)\n };\n }\n\n // Path 2: verify a client-supplied access token. This validates\n // the token's audience against our clientId — see\n // verifyGoogleAccessToken — before trusting the identity.\n if (payload.accessToken) {\n return await verifyGoogleAccessToken(payload.accessToken, clientId);\n }\n\n // Path 3: authorization code exchange (most secure)\n // The frontend obtained a one-time authorization code via the\n // Google OAuth consent screen. We exchange it server-side for\n // tokens, so the access/id tokens never touch the browser.\n if (payload.code && payload.redirectUri) {\n if (!clientSecret) {\n throw new Error(\n \"Google authorization code flow requires clientSecret. \" +\n \"Configure GOOGLE_CLIENT_SECRET in your environment.\"\n );\n }\n\n // Exchange the authorization code for tokens\n const tokenResponse = await fetch(\"https://oauth2.googleapis.com/token\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n code: payload.code,\n client_id: clientId,\n client_secret: clientSecret,\n redirect_uri: payload.redirectUri,\n grant_type: \"authorization_code\",\n ...pkceTokenParams(payload.codeVerifier)\n })\n });\n\n if (!tokenResponse.ok) {\n const errorBody = await tokenResponse.text();\n throw new Error(`Google token exchange failed (${tokenResponse.status}): ${errorBody}`);\n }\n\n const tokenData = await tokenResponse.json() as {\n id_token?: string;\n access_token?: string;\n error?: string;\n error_description?: string;\n };\n\n if (tokenData.error) {\n throw new Error(`Google token exchange error: ${tokenData.error} – ${tokenData.error_description || \"no details\"}`);\n }\n\n // Prefer verifying the ID token (cryptographic verification)\n if (tokenData.id_token) {\n const client = await getClient();\n const ticket = await client.verifyIdToken({\n idToken: tokenData.id_token,\n audience: clientId\n });\n\n const content = ticket.getPayload();\n if (!content) {\n throw new Error(\"Google ID token payload was empty after code exchange\");\n }\n\n return {\n providerId: content.sub,\n email: content.email || \"\",\n displayName: content.name || null,\n photoUrl: content.picture || null,\n emailVerified: providerVerifiedEmail(content.email_verified)\n };\n }\n\n // Fallback: use the access token from our own confidential\n // exchange. Its audience is necessarily our clientId, so the\n // check in verifyGoogleAccessToken passes — we route through\n // it anyway for a single trusted identity path.\n if (tokenData.access_token) {\n return await verifyGoogleAccessToken(tokenData.access_token, clientId);\n }\n\n throw new Error(\"Google token exchange returned neither id_token nor access_token\");\n }\n\n throw new Error(\"No valid Google credential provided (expected idToken, accessToken, or code+redirectUri)\");\n } catch (error) {\n logger.error(\"Google OAuth verification failed\", { error: error });\n throw error;\n }\n }\n };\n}\n","import type { OAuthProvider, OAuthProviderProfile } from \"./interfaces\";\nimport { logger } from \"../utils/logger\";\nimport { oauthCodeFlowSchema, pkceTokenParams, providerVerifiedEmail, type OAuthCodeFlowPayload } from \"./oauth-code-flow\";\n\nexport interface LinkedinUserInfo {\n linkedinId: string;\n email: string;\n displayName: string | null;\n photoUrl: string | null;\n emailVerified: boolean;\n}\n\n/**\n * Creates a LinkedIn OAuth Provider integration\n */\nexport function createLinkedinProvider(config: { clientId: string, clientSecret: string }): OAuthProvider<OAuthCodeFlowPayload> {\n return {\n id: \"linkedin\",\n schema: oauthCodeFlowSchema(),\n verify: async (payload: OAuthCodeFlowPayload): Promise<OAuthProviderProfile | null> => {\n try {\n // Exchange code for access token\n const tokenResponse = await fetch(\"https://www.linkedin.com/oauth/v2/accessToken\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\"\n },\n body: new URLSearchParams({\n grant_type: \"authorization_code\",\n code: payload.code,\n redirect_uri: payload.redirectUri,\n client_id: config.clientId,\n client_secret: config.clientSecret,\n ...pkceTokenParams(payload.codeVerifier)\n })\n });\n\n if (!tokenResponse.ok) {\n const errBody = await tokenResponse.text();\n logger.error(\"Failed to get LinkedIn access token\", { detail: errBody });\n return null;\n }\n\n const tokenData = await tokenResponse.json() as { access_token: string; id_token?: string };\n const accessToken = tokenData.access_token;\n\n // Fetch User Info using OIDC userinfo endpoint\n const profileResponse = await fetch(\"https://api.linkedin.com/v2/userinfo\", {\n headers: {\n \"Authorization\": `Bearer ${accessToken}`\n }\n });\n\n if (!profileResponse.ok) {\n const errBody = await profileResponse.text();\n logger.error(\"Failed to get LinkedIn user info\", { detail: errBody });\n return null;\n }\n\n const profileData = await profileResponse.json() as {\n sub: string;\n email?: string;\n name?: string;\n picture?: string;\n email_verified?: boolean;\n };\n\n if (!profileData.email) {\n logger.error(\"LinkedIn user has no email (the 'email' scope may not have been granted)\");\n return null;\n }\n\n return {\n providerId: profileData.sub,\n email: profileData.email,\n displayName: profileData.name || null,\n photoUrl: profileData.picture || null,\n emailVerified: providerVerifiedEmail(profileData.email_verified)\n };\n } catch (error) {\n logger.error(\"LinkedIn OAuth error\", { error: error });\n return null;\n }\n }\n };\n}\n","import type { OAuthProvider, OAuthProviderProfile } from \"./interfaces\";\nimport { logger } from \"../utils/logger\";\nimport { oauthCodeFlowSchema, pkceTokenParams, providerVerifiedEmail, type OAuthCodeFlowPayload } from \"./oauth-code-flow\";\n\n/**\n * Creates a GitHub OAuth Provider integration.\n *\n * Flow: Frontend receives an authorization `code` via the GitHub OAuth redirect.\n * This provider exchanges the code for an access token, then fetches the user's\n * profile and primary email from the GitHub API.\n */\nexport function createGitHubProvider(config: { clientId: string; clientSecret: string }): OAuthProvider<OAuthCodeFlowPayload> {\n return {\n id: \"github\",\n schema: oauthCodeFlowSchema(),\n verify: async (payload: OAuthCodeFlowPayload): Promise<OAuthProviderProfile | null> => {\n try {\n // Exchange code for access token\n const tokenResponse = await fetch(\"https://github.com/login/oauth/access_token\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/json\"\n },\n body: JSON.stringify({\n client_id: config.clientId,\n client_secret: config.clientSecret,\n code: payload.code,\n redirect_uri: payload.redirectUri,\n ...pkceTokenParams(payload.codeVerifier)\n })\n });\n\n if (!tokenResponse.ok) {\n logger.error(\"Failed to get GitHub access token\", { detail: await tokenResponse.text() });\n return null;\n }\n\n const tokenData = await tokenResponse.json() as { access_token?: string; error?: string };\n if (tokenData.error || !tokenData.access_token) {\n logger.error(\"GitHub token exchange error\", { detail: tokenData.error });\n return null;\n }\n\n const accessToken = tokenData.access_token;\n\n // Fetch user profile\n const profileResponse = await fetch(\"https://api.github.com/user\", {\n headers: {\n \"Authorization\": `Bearer ${accessToken}`,\n \"Accept\": \"application/vnd.github+json\",\n \"User-Agent\": \"Rebase-Auth\"\n }\n });\n\n if (!profileResponse.ok) {\n logger.error(\"Failed to get GitHub user info\", { detail: await profileResponse.text() });\n return null;\n }\n\n const profileData = await profileResponse.json() as {\n id: number;\n login: string;\n name?: string | null;\n avatar_url?: string | null;\n email?: string | null;\n };\n\n // `/user`'s `email` is the *public profile* address — GitHub\n // applies no verification to it, and it is often absent\n // anyway. `/user/emails` is the only endpoint that reports a\n // `verified` flag, so ask it unconditionally rather than\n // treating the profile field as a fast path that skips the\n // check: that fast path is precisely what used to reach the\n // hardcoded `emailVerified: true` below.\n let email = profileData.email;\n let emailVerified = false;\n\n const emailsResponse = await fetch(\"https://api.github.com/user/emails\", {\n headers: {\n \"Authorization\": `Bearer ${accessToken}`,\n \"Accept\": \"application/vnd.github+json\",\n \"User-Agent\": \"Rebase-Auth\"\n }\n });\n\n if (emailsResponse.ok) {\n const emails = await emailsResponse.json() as Array<{\n email: string;\n primary: boolean;\n verified: boolean;\n }>;\n const chosen = emails.find(e => e.primary && e.verified)\n || emails.find(e => e.verified)\n || (email ? emails.find(e => e.email.toLowerCase() === email!.toLowerCase()) : undefined);\n if (chosen) {\n email = chosen.email;\n emailVerified = providerVerifiedEmail(chosen.verified);\n }\n }\n\n if (!email) {\n logger.error(\"GitHub user has no email\");\n return null;\n }\n\n return {\n providerId: String(profileData.id),\n email,\n displayName: profileData.name || profileData.login || null,\n photoUrl: profileData.avatar_url || null,\n emailVerified\n };\n } catch (error) {\n logger.error(\"GitHub OAuth error\", { error: error });\n return null;\n }\n }\n };\n}\n","import jwt from \"jsonwebtoken\";\nimport { createPublicKey, type KeyObject } from \"crypto\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * Minimal JWKS-backed id_token verification, shared by the OIDC providers.\n *\n * Apple's id_token used to be `split(\".\")` + `JSON.parse`, justified by the\n * comment \"we only need the payload\". That skips `aud` — nothing confirmed the\n * token was minted for *this* Services ID rather than another one under the\n * same Apple team — along with `iss`, `exp` and the signature. Microsoft did\n * not request an id_token at all and inferred verification from a directory\n * attribute instead.\n *\n * Deliberately hand-rolled rather than pulling in a JWKS client: the only\n * cryptography here is `jsonwebtoken`'s (already a dependency) and Node's own\n * JWK→KeyObject import. What this file adds is key discovery and a cache.\n */\n\nexport interface OidcIdTokenClaims {\n sub: string;\n iss: string;\n aud: string | string[];\n exp: number;\n email?: string;\n email_verified?: boolean | string;\n nonce?: string;\n [claim: string]: unknown;\n}\n\nexport interface VerifyOidcIdTokenOptions {\n /** The compact JWS to verify. */\n idToken: string;\n /** JWKS endpoint of the issuer. */\n jwksUri: string;\n /**\n * Expected `iss`. A `RegExp` is for multi-tenant issuers whose tenant id is\n * part of the issuer URL (Entra ID with `tenantId: \"common\"`); everything\n * else passes the exact string.\n */\n issuer: string | RegExp;\n /** Expected `aud` — the client/services id this backend is configured with. */\n audience: string;\n /** Permitted signing algorithms. Defaults to RS256, which is what Apple and Entra use. */\n algorithms?: jwt.Algorithm[];\n /** Seconds of clock skew tolerated on `exp`/`iat`. Defaults to 60. */\n clockToleranceSec?: number;\n /** Expected `nonce`, when the client bound one to the authorization request. */\n nonce?: string;\n /** Injection point for tests; defaults to global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\ninterface Jwk {\n kid?: string;\n kty?: string;\n alg?: string;\n use?: string;\n [param: string]: unknown;\n}\n\ninterface CachedJwks {\n keys: Jwk[];\n fetchedAt: number;\n}\n\nconst JWKS_TTL_MS = 10 * 60 * 1000;\nconst jwksCache = new Map<string, CachedJwks>();\n\n/** Drops the JWKS cache. Exported for tests; nothing in the runtime calls it. */\nexport function resetJwksCache(): void {\n jwksCache.clear();\n}\n\nasync function loadJwks(jwksUri: string, fetchImpl: typeof fetch, forceRefresh: boolean): Promise<Jwk[]> {\n const cached = jwksCache.get(jwksUri);\n if (!forceRefresh && cached && Date.now() - cached.fetchedAt < JWKS_TTL_MS) {\n return cached.keys;\n }\n const response = await fetchImpl(jwksUri);\n if (!response.ok) {\n throw new Error(`JWKS fetch failed (${response.status})`);\n }\n const body = await response.json() as { keys?: Jwk[] };\n const keys = Array.isArray(body.keys) ? body.keys : [];\n jwksCache.set(jwksUri, { keys, fetchedAt: Date.now() });\n return keys;\n}\n\nfunction toKeyObject(jwk: Jwk): KeyObject {\n // `format: \"jwk\"` is Node's own importer — no third-party JWK parsing.\n return createPublicKey({ key: jwk as Record<string, unknown>, format: \"jwk\" } as Parameters<typeof createPublicKey>[0]);\n}\n\nfunction decodeKid(idToken: string): string | undefined {\n const decoded = jwt.decode(idToken, { complete: true });\n return decoded?.header?.kid;\n}\n\n/**\n * Verify an id_token's signature, `aud`, `iss` and `exp`, and return its claims.\n *\n * Throws on any failure — callers treat a throw as \"reject this sign-in\",\n * never as \"continue without the claims\".\n */\nexport async function verifyOidcIdToken(options: VerifyOidcIdTokenOptions): Promise<OidcIdTokenClaims> {\n const {\n idToken,\n jwksUri,\n issuer,\n audience,\n algorithms = [\"RS256\"],\n clockToleranceSec = 60,\n nonce,\n fetchImpl = fetch\n } = options;\n\n if (!idToken || typeof idToken !== \"string\") {\n throw new Error(\"id_token missing\");\n }\n\n const kid = decodeKid(idToken);\n\n // An unknown `kid` is the normal signal that the issuer rotated its keys,\n // so miss the cache once before giving up.\n let keys = await loadJwks(jwksUri, fetchImpl, false);\n let jwk = keys.find((k) => !kid || k.kid === kid);\n if (!jwk) {\n keys = await loadJwks(jwksUri, fetchImpl, true);\n jwk = keys.find((k) => !kid || k.kid === kid);\n }\n if (!jwk) {\n throw new Error(`No JWKS key matches kid \"${kid ?? \"<none>\"}\"`);\n }\n\n const claims = jwt.verify(idToken, toKeyObject(jwk), {\n algorithms,\n audience,\n clockTolerance: clockToleranceSec\n }) as OidcIdTokenClaims;\n\n // `jsonwebtoken` only compares `iss` against a string or a list, so the\n // multi-tenant case is checked here rather than passed through.\n const issuerOk = typeof issuer === \"string\" ? claims.iss === issuer : issuer.test(String(claims.iss ?? \"\"));\n if (!issuerOk) {\n throw new Error(`Unexpected id_token issuer \"${claims.iss}\"`);\n }\n\n if (nonce !== undefined && claims.nonce !== nonce) {\n throw new Error(\"id_token nonce mismatch\");\n }\n\n return claims;\n}\n\n/**\n * `verifyOidcIdToken` that reports failure as `null` and logs it, for the one\n * call site that has to decide between \"reject\" and \"degrade\".\n */\nexport async function tryVerifyOidcIdToken(\n providerId: string,\n options: VerifyOidcIdTokenOptions\n): Promise<OidcIdTokenClaims | null> {\n try {\n return await verifyOidcIdToken(options);\n } catch (error) {\n logger.error(`[${providerId}] id_token verification failed`, {\n error: error instanceof Error ? error.message : error\n });\n return null;\n }\n}\n","import type { OAuthProvider, OAuthProviderProfile } from \"./interfaces\";\nimport { logger } from \"../utils/logger\";\nimport { oauthCodeFlowSchema, pkceTokenParams, providerVerifiedEmail, type OAuthCodeFlowPayload } from \"./oauth-code-flow\";\nimport { tryVerifyOidcIdToken } from \"./oidc-id-token\";\n\n/** Endpoints that federate to more than one directory. */\nconst MULTI_TENANT_ENDPOINTS = new Set([\"common\", \"organizations\", \"consumers\"]);\n\n/**\n * Creates a Microsoft / Entra ID (Azure AD) OAuth Provider integration.\n *\n * Supports both personal Microsoft accounts and work/school (Azure AD) accounts\n * via the \"common\" tenant endpoint. Uses the authorization code flow.\n *\n * ## On `emailVerified`\n *\n * Microsoft Graph exposes no email-verification field, and `mail` is *not* a\n * substitute: it is a directory attribute a tenant administrator sets to any\n * string they like, through Graph or AAD Connect sync. With the default\n * `tenantId: \"common\"` every Entra tenant in the world is an accepted issuer,\n * so \"there is a `mail` value\" would mean \"anybody who can create a free\n * tenant can nominate any address\" — including one that already has an account\n * here.\n *\n * The only signal Microsoft offers is the `xms_edov` (\"email domain owner\n * verified\") optional claim on the id_token, so that is what this provider\n * reads, off a signature-verified token. If the app registration does not emit\n * `xms_edov`, sign-in still works and the address comes back unverified —\n * which routes the user to `POST /auth/link/microsoft` rather than silently\n * handing them somebody else's account.\n */\nexport function createMicrosoftProvider(config: {\n clientId: string;\n clientSecret: string;\n /** Tenant ID. Defaults to \"common\" which allows both personal and organizational accounts. */\n tenantId?: string;\n}): OAuthProvider<OAuthCodeFlowPayload> {\n const tenantId = config.tenantId || \"common\";\n const multiTenant = MULTI_TENANT_ENDPOINTS.has(tenantId.toLowerCase());\n\n const jwksUri = `https://login.microsoftonline.com/${tenantId}/discovery/v2.0/keys`;\n // Single-tenant apps pin the issuer exactly. Multi-tenant ones cannot —\n // the tenant id is part of the issuer URL — so accept any well-formed\n // tenant GUID and lean on `xms_edov` for the verification decision.\n const issuer: string | RegExp = multiTenant\n ? /^https:\\/\\/login\\.microsoftonline\\.com\\/[0-9a-fA-F-]{36}\\/v2\\.0$/\n : `https://login.microsoftonline.com/${tenantId}/v2.0`;\n\n if (multiTenant) {\n logger.warn(\n `[Rebase] Microsoft provider configured with tenantId \"${tenantId}\", which accepts sign-ins from `\n + \"every Entra tenant. Email addresses are reported as unverified unless the app registration emits \"\n + \"the xms_edov optional claim. Set an explicit tenantId, or enable xms_edov, to allow automatic \"\n + \"linking onto existing accounts.\"\n );\n }\n\n return {\n id: \"microsoft\",\n schema: oauthCodeFlowSchema(),\n verify: async (payload: OAuthCodeFlowPayload): Promise<OAuthProviderProfile | null> => {\n try {\n // Exchange code for access token\n const tokenResponse = await fetch(\n `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n client_id: config.clientId,\n client_secret: config.clientSecret,\n code: payload.code,\n redirect_uri: payload.redirectUri,\n grant_type: \"authorization_code\",\n scope: \"openid profile email User.Read\",\n ...pkceTokenParams(payload.codeVerifier)\n })\n }\n );\n\n if (!tokenResponse.ok) {\n logger.error(\"Failed to get Microsoft access token\", { detail: await tokenResponse.text() });\n return null;\n }\n\n const tokenData = await tokenResponse.json() as { access_token: string; id_token?: string };\n const accessToken = tokenData.access_token;\n\n // The `openid` scope above means Entra returns an id_token. If\n // one is present it must verify — a token that fails signature,\n // audience or issuer checks is not something to shrug off and\n // continue past.\n let idTokenClaims: Record<string, unknown> | null = null;\n if (tokenData.id_token) {\n idTokenClaims = await tryVerifyOidcIdToken(\"microsoft\", {\n idToken: tokenData.id_token,\n jwksUri,\n issuer,\n audience: config.clientId\n });\n if (!idTokenClaims) return null;\n }\n\n // Fetch user profile from Microsoft Graph\n const profileResponse = await fetch(\"https://graph.microsoft.com/v1.0/me\", {\n headers: { \"Authorization\": `Bearer ${accessToken}` }\n });\n\n if (!profileResponse.ok) {\n logger.error(\"Failed to get Microsoft user info\", { detail: await profileResponse.text() });\n return null;\n }\n\n const profileData = await profileResponse.json() as {\n id: string;\n displayName?: string | null;\n mail?: string | null;\n userPrincipalName?: string | null;\n };\n\n const email = profileData.mail || profileData.userPrincipalName;\n if (!email) {\n logger.error(\"Microsoft user has no email\");\n return null;\n }\n\n // `xms_edov` is asserted about the id_token's own email claim,\n // so it only carries over to the Graph address when the two\n // agree.\n const claimedEmail = (idTokenClaims?.email ?? idTokenClaims?.preferred_username) as string | undefined;\n const emailVerified = providerVerifiedEmail(idTokenClaims?.xms_edov)\n && typeof claimedEmail === \"string\"\n && claimedEmail.toLowerCase() === email.toLowerCase();\n\n // Attempt to fetch profile photo URL (Graph returns binary, not a URL).\n // We skip this and let the frontend use the Microsoft Graph photo endpoint.\n return {\n // Keyed on the Graph object id, unchanged: it is what every\n // already-linked identity row holds.\n providerId: profileData.id,\n email,\n displayName: profileData.displayName || null,\n photoUrl: null,\n emailVerified\n };\n } catch (error) {\n logger.error(\"Microsoft OAuth error\", { error: error });\n return null;\n }\n }\n };\n}\n","import type { OAuthProvider, OAuthProviderProfile } from \"./interfaces\";\nimport { z } from \"zod\";\nimport jwt from \"jsonwebtoken\";\nimport { logger } from \"../utils/logger\";\nimport { oauthCodeFlowSchema, pkceTokenParams, providerVerifiedEmail, type OAuthCodeFlowPayload } from \"./oauth-code-flow\";\nimport { tryVerifyOidcIdToken } from \"./oidc-id-token\";\n\nconst APPLE_ISSUER = \"https://appleid.apple.com\";\nconst APPLE_JWKS_URI = \"https://appleid.apple.com/auth/keys\";\n\n/** Extra field Apple's client sends that no other provider has. */\nexport interface AppleCodeFlowPayload extends OAuthCodeFlowPayload {\n /**\n * Apple sends the user's *name* only on the first authorization, so the\n * frontend has to forward it. It is display data and nothing else — the\n * identity email comes from the id_token, never from the request body.\n */\n user?: { name?: { firstName?: string; lastName?: string } };\n}\n\n/**\n * Creates an Apple Sign In OAuth Provider integration.\n *\n * Apple requires a client secret that is a signed JWT, regenerated on each\n * token exchange (valid up to 6 months). This provider handles that automatically.\n *\n * Required Apple Developer configuration:\n * - Services ID (clientId)\n * - Key ID from the private key registered with Apple\n * - Team ID from Apple Developer account\n * - Private key (.p8 file contents) downloaded from Apple Developer portal\n */\nexport function createAppleProvider(config: {\n clientId: string;\n teamId: string;\n keyId: string;\n /** The raw PEM contents of the .p8 private key file */\n privateKey: string;\n}): OAuthProvider<AppleCodeFlowPayload> {\n /**\n * Generate a client_secret JWT signed with the Apple private key.\n * Apple requires this instead of a static client_secret.\n */\n async function generateClientSecret(): Promise<string> {\n return jwt.sign({}, config.privateKey, {\n algorithm: \"ES256\",\n keyid: config.keyId,\n issuer: config.teamId,\n expiresIn: \"180d\",\n audience: \"https://appleid.apple.com\",\n subject: config.clientId\n });\n }\n\n return {\n id: \"apple\",\n schema: oauthCodeFlowSchema().extend({\n /**\n * Apple sends user info only on first authorization; the frontend\n * must forward it. Only the *name* is accepted — an `email` here\n * used to be promoted to the account's identity whenever the\n * id_token had no `email` claim, and an attacker chooses whether\n * that claim is absent simply by omitting the `email` scope from\n * their own authorization request.\n */\n user: z.object({\n name: z.object({\n firstName: z.string().optional(),\n lastName: z.string().optional()\n }).optional()\n }).optional()\n }),\n verify: async (payload: AppleCodeFlowPayload): Promise<OAuthProviderProfile | null> => {\n try {\n const clientSecret = await generateClientSecret();\n\n // Exchange code for tokens\n const tokenResponse = await fetch(\"https://appleid.apple.com/auth/token\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n client_id: config.clientId,\n client_secret: clientSecret,\n code: payload.code,\n grant_type: \"authorization_code\",\n redirect_uri: payload.redirectUri,\n ...pkceTokenParams(payload.codeVerifier)\n })\n });\n\n if (!tokenResponse.ok) {\n logger.error(\"Failed to get Apple access token\", { detail: await tokenResponse.text() });\n return null;\n }\n\n const tokenData = await tokenResponse.json() as { id_token?: string };\n\n // Verify the id_token against Apple's JWKS rather than\n // base64-decoding it. The signature is the least of it: `aud`\n // is what confirms the token was minted for *this* Services ID\n // and not another one under the same Apple team, and `exp`\n // stops a captured token being replayed.\n const decoded = tokenData.id_token\n ? await tryVerifyOidcIdToken(\"apple\", {\n idToken: tokenData.id_token,\n jwksUri: APPLE_JWKS_URI,\n issuer: APPLE_ISSUER,\n audience: config.clientId\n })\n : null;\n\n if (!decoded) {\n logger.error(\"Apple token exchange returned no verifiable id_token\");\n return null;\n }\n\n // The id_token is the only acceptable source of the address.\n const email = decoded.email;\n if (!email) {\n logger.error(\"Apple id_token carries no email claim\");\n return null;\n }\n\n let displayName: string | null = null;\n if (payload.user?.name) {\n const parts = [payload.user.name.firstName, payload.user.name.lastName].filter(Boolean);\n displayName = parts.length > 0 ? parts.join(\" \") : null;\n }\n\n return {\n providerId: decoded.sub,\n email,\n displayName,\n photoUrl: null, // Apple does not provide a profile photo\n emailVerified: providerVerifiedEmail(decoded.email_verified)\n };\n } catch (error) {\n logger.error(\"Apple OAuth error\", { error: error });\n return null;\n }\n }\n };\n}\n","import type { OAuthProvider, OAuthProviderProfile } from \"./interfaces\";\nimport { logger } from \"../utils/logger\";\nimport { oauthCodeFlowSchema, pkceTokenParams, type OAuthCodeFlowPayload } from \"./oauth-code-flow\";\n\n/**\n * Creates a Facebook / Meta OAuth Provider integration.\n *\n * Uses the authorization code flow to exchange a code for an access token,\n * then fetches user profile from the Facebook Graph API.\n */\nexport function createFacebookProvider(config: { clientId: string; clientSecret: string }): OAuthProvider<OAuthCodeFlowPayload> {\n return {\n id: \"facebook\",\n schema: oauthCodeFlowSchema(),\n verify: async (payload: OAuthCodeFlowPayload): Promise<OAuthProviderProfile | null> => {\n try {\n // Exchange code for access token.\n //\n // POSTed as a form body, not a query string: the URL is the\n // part of a request that outbound proxies, APM spans and\n // `fetch` error objects log by default, and a long-lived app\n // secret sitting in a log aggregator is a compromise nobody\n // notices. Graph accepts both.\n const tokenResponse = await fetch(\"https://graph.facebook.com/v19.0/oauth/access_token\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n client_id: config.clientId,\n client_secret: config.clientSecret,\n redirect_uri: payload.redirectUri,\n code: payload.code,\n ...pkceTokenParams(payload.codeVerifier)\n })\n });\n\n if (!tokenResponse.ok) {\n logger.error(\"Failed to get Facebook access token\", { detail: await tokenResponse.text() });\n return null;\n }\n\n const tokenData = await tokenResponse.json() as { access_token: string };\n const accessToken = tokenData.access_token;\n\n // Fetch user profile with email and profile picture\n const profileUrl = new URL(\"https://graph.facebook.com/v19.0/me\");\n profileUrl.searchParams.set(\"fields\", \"id,name,email,picture.type(large)\");\n\n const profileResponse = await fetch(profileUrl.toString(), {\n headers: { \"Authorization\": `Bearer ${accessToken}` }\n });\n\n if (!profileResponse.ok) {\n logger.error(\"Failed to get Facebook user info\", { detail: await profileResponse.text() });\n return null;\n }\n\n const profileData = await profileResponse.json() as {\n id: string;\n name?: string;\n email?: string;\n picture?: { data?: { url?: string } };\n };\n\n if (!profileData.email) {\n logger.error(\"Facebook user has no email (email permission may not have been granted)\");\n return null;\n }\n\n return {\n providerId: profileData.id,\n email: profileData.email,\n displayName: profileData.name || null,\n photoUrl: profileData.picture?.data?.url || null,\n // Graph exposes no email-verification field on `/me`, so\n // nothing in the response supports a `true` here.\n // `emailVerified` is the sole gate on attaching this\n // identity to a pre-existing account; users who already\n // have one link through `POST /auth/link/facebook`.\n emailVerified: false\n };\n } catch (error) {\n logger.error(\"Facebook OAuth error\", { error: error });\n return null;\n }\n }\n };\n}\n","import type { OAuthProvider, OAuthProviderProfile } from \"./interfaces\";\nimport { logger } from \"../utils/logger\";\nimport { oauthCodeFlowSchema, pkceTokenParams, type OAuthCodeFlowPayload } from \"./oauth-code-flow\";\n\n/**\n * Creates a Twitter/X OAuth 2.0 Provider integration.\n *\n * Uses OAuth 2.0 with PKCE (authorization code flow). The frontend must include\n * the PKCE `code_verifier` when sending the authorization code.\n *\n * Twitter API v2 requires the \"tweet.read\" and \"users.read\" scopes at minimum,\n * plus \"offline.access\" if refresh tokens are needed on Twitter's side.\n */\nexport function createTwitterProvider(config: { clientId: string; clientSecret: string }): OAuthProvider<OAuthCodeFlowPayload> {\n return {\n id: \"twitter\",\n // The one provider where PKCE is not optional: Twitter's token\n // endpoint rejects an exchange without a verifier.\n schema: oauthCodeFlowSchema({ pkce: \"required\" }),\n verify: async (payload: OAuthCodeFlowPayload): Promise<OAuthProviderProfile | null> => {\n try {\n // Twitter OAuth 2.0 uses Basic auth for the token endpoint\n const basicAuth = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString(\"base64\");\n\n // Exchange code for access token\n const tokenResponse = await fetch(\"https://api.twitter.com/2/oauth2/token\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n \"Authorization\": `Basic ${basicAuth}`\n },\n body: new URLSearchParams({\n code: payload.code,\n grant_type: \"authorization_code\",\n redirect_uri: payload.redirectUri,\n ...pkceTokenParams(payload.codeVerifier)\n })\n });\n\n if (!tokenResponse.ok) {\n logger.error(\"Failed to get Twitter access token\", { detail: await tokenResponse.text() });\n return null;\n }\n\n const tokenData = await tokenResponse.json() as { access_token: string };\n const accessToken = tokenData.access_token;\n\n // Fetch user profile from Twitter API v2\n const profileResponse = await fetch(\n \"https://api.twitter.com/2/users/me?user.fields=id,name,username,profile_image_url\",\n {\n headers: { \"Authorization\": `Bearer ${accessToken}` }\n }\n );\n\n if (!profileResponse.ok) {\n logger.error(\"Failed to get Twitter user info\", { detail: await profileResponse.text() });\n return null;\n }\n\n const profileResult = await profileResponse.json() as {\n data: {\n id: string;\n name: string;\n username: string;\n profile_image_url?: string;\n };\n };\n\n const profileData = profileResult.data;\n\n // Twitter does NOT expose email via API v2 by default.\n // It requires elevated access. If we can't get an email, we\n // generate a placeholder email — the user can update it later.\n // For apps with elevated access, fetch from v1.1 endpoint.\n let email: string | null = null;\n let emailVerified = false;\n try {\n const emailResponse = await fetch(\n \"https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true\",\n {\n headers: { \"Authorization\": `Bearer ${accessToken}` }\n }\n );\n if (emailResponse.ok) {\n const emailData = await emailResponse.json() as { email?: string };\n email = emailData.email || null;\n if (email) {\n emailVerified = true;\n }\n }\n } catch {\n // Elevated access not available — fall through\n }\n\n if (!email) {\n // Construct a deterministic placeholder email from the Twitter user ID.\n // This allows the account to be created and linked; the user should\n // update their email through the profile settings.\n email = `${profileData.id}@twitter.placeholder.rebase`;\n // Placeholder emails are NOT verified — prevents auto-linking to existing accounts\n emailVerified = false;\n }\n\n return {\n providerId: profileData.id,\n email,\n displayName: profileData.name || profileData.username || null,\n photoUrl: profileData.profile_image_url?.replace(\"_normal\", \"_400x400\") || null,\n emailVerified\n };\n } catch (error) {\n logger.error(\"Twitter OAuth error\", { error: error });\n return null;\n }\n }\n };\n}\n","import type { OAuthProvider, OAuthProviderProfile } from \"./interfaces\";\nimport { logger } from \"../utils/logger\";\nimport { oauthCodeFlowSchema, pkceTokenParams, providerVerifiedEmail, type OAuthCodeFlowPayload } from \"./oauth-code-flow\";\n\n/**\n * Creates a Discord OAuth2 Provider integration.\n *\n * Uses the authorization code flow (with PKCE when the client supplies a\n * verifier). Requires the \"identify\" and \"email\" scopes to retrieve the\n * user's email and profile information.\n */\nexport function createDiscordProvider(config: { clientId: string; clientSecret: string }): OAuthProvider<OAuthCodeFlowPayload> {\n return {\n id: \"discord\",\n schema: oauthCodeFlowSchema(),\n verify: async (payload: OAuthCodeFlowPayload): Promise<OAuthProviderProfile | null> => {\n try {\n // Exchange code for access token\n const tokenResponse = await fetch(\"https://discord.com/api/v10/oauth2/token\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n client_id: config.clientId,\n client_secret: config.clientSecret,\n grant_type: \"authorization_code\",\n code: payload.code,\n redirect_uri: payload.redirectUri,\n ...pkceTokenParams(payload.codeVerifier)\n })\n });\n\n if (!tokenResponse.ok) {\n logger.error(\"Failed to get Discord access token\", { detail: await tokenResponse.text() });\n return null;\n }\n\n const tokenData = await tokenResponse.json() as { access_token: string };\n const accessToken = tokenData.access_token;\n\n // Fetch user profile\n const profileResponse = await fetch(\"https://discord.com/api/v10/users/@me\", {\n headers: { \"Authorization\": `Bearer ${accessToken}` }\n });\n\n if (!profileResponse.ok) {\n logger.error(\"Failed to get Discord user info\", { detail: await profileResponse.text() });\n return null;\n }\n\n const profileData = await profileResponse.json() as {\n id: string;\n username: string;\n global_name?: string | null;\n avatar?: string | null;\n email?: string | null;\n verified?: boolean;\n };\n\n if (!profileData.email) {\n logger.error(\"Discord user has no email (email scope may not have been granted)\");\n return null;\n }\n\n // Build avatar URL\n let photoUrl: string | null = null;\n if (profileData.avatar) {\n const ext = profileData.avatar.startsWith(\"a_\") ? \"gif\" : \"png\";\n photoUrl = `https://cdn.discordapp.com/avatars/${profileData.id}/${profileData.avatar}.${ext}?size=256`;\n }\n\n return {\n providerId: profileData.id,\n email: profileData.email,\n displayName: profileData.global_name || profileData.username || null,\n photoUrl,\n emailVerified: providerVerifiedEmail(profileData.verified)\n };\n } catch (error) {\n logger.error(\"Discord OAuth error\", { error: error });\n return null;\n }\n }\n };\n}\n","import type { OAuthProvider, OAuthProviderProfile } from \"./interfaces\";\nimport { logger } from \"../utils/logger\";\nimport { oauthCodeFlowSchema, pkceTokenParams, type OAuthCodeFlowPayload } from \"./oauth-code-flow\";\n\n/**\n * Creates a GitLab OAuth Provider integration.\n * Works with both GitLab.com and self-hosted instances.\n *\n * `baseUrl` is configuration only and must stay that way: a caller-supplied\n * instance URL would make this provider an SSRF primitive and an\n * arbitrary-identity oracle in one step.\n */\nexport function createGitLabProvider(config: {\n clientId: string;\n clientSecret: string;\n baseUrl?: string;\n}): OAuthProvider<OAuthCodeFlowPayload> {\n const gitlabUrl = (config.baseUrl || \"https://gitlab.com\").replace(/\\/$/, \"\");\n\n return {\n id: \"gitlab\",\n schema: oauthCodeFlowSchema(),\n verify: async (payload: OAuthCodeFlowPayload): Promise<OAuthProviderProfile | null> => {\n try {\n const tokenResponse = await fetch(`${gitlabUrl}/oauth/token`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n client_id: config.clientId,\n client_secret: config.clientSecret,\n code: payload.code,\n grant_type: \"authorization_code\",\n redirect_uri: payload.redirectUri,\n ...pkceTokenParams(payload.codeVerifier)\n })\n });\n\n if (!tokenResponse.ok) {\n logger.error(\"Failed to get GitLab access token\", { detail: await tokenResponse.text() });\n return null;\n }\n\n const tokenData = await tokenResponse.json() as { access_token: string };\n\n const profileResponse = await fetch(`${gitlabUrl}/api/v4/user`, {\n headers: { \"Authorization\": `Bearer ${tokenData.access_token}` }\n });\n\n if (!profileResponse.ok) {\n logger.error(\"Failed to get GitLab user info\", { detail: await profileResponse.text() });\n return null;\n }\n\n const p = await profileResponse.json() as {\n id: number; username: string; name?: string;\n email?: string; avatar_url?: string | null;\n confirmed_at?: string | null;\n };\n\n if (!p.email) { logger.error(\"GitLab user has no email\"); return null; }\n\n return {\n providerId: String(p.id),\n email: p.email,\n displayName: p.name || p.username || null,\n photoUrl: p.avatar_url || null,\n // `/api/v4/user` timestamps the address confirmation for\n // the authenticated user. That timestamp is the only\n // verification signal GitLab gives us, so it is the only\n // thing allowed to produce a `true` — a self-hosted\n // instance with confirmation disabled reports nothing here\n // and correctly comes back unverified.\n emailVerified: Boolean(p.confirmed_at)\n };\n } catch (error) {\n logger.error(\"GitLab OAuth error\", { error: error });\n return null;\n }\n }\n };\n}\n","import type { OAuthProvider, OAuthProviderProfile } from \"./interfaces\";\nimport { logger } from \"../utils/logger\";\nimport { oauthCodeFlowSchema, pkceTokenParams, providerVerifiedEmail, type OAuthCodeFlowPayload } from \"./oauth-code-flow\";\n\n/**\n * Creates a Bitbucket OAuth Provider integration (OAuth 2.0 consumer).\n */\nexport function createBitbucketProvider(config: { clientId: string; clientSecret: string }): OAuthProvider<OAuthCodeFlowPayload> {\n return {\n id: \"bitbucket\",\n schema: oauthCodeFlowSchema(),\n verify: async (payload: OAuthCodeFlowPayload): Promise<OAuthProviderProfile | null> => {\n try {\n const basicAuth = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString(\"base64\");\n\n const tokenResponse = await fetch(\"https://bitbucket.org/site/oauth2/access_token\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n \"Authorization\": `Basic ${basicAuth}`\n },\n body: new URLSearchParams({\n grant_type: \"authorization_code\",\n code: payload.code,\n redirect_uri: payload.redirectUri,\n ...pkceTokenParams(payload.codeVerifier)\n })\n });\n\n if (!tokenResponse.ok) {\n logger.error(\"Failed to get Bitbucket access token\", { detail: await tokenResponse.text() });\n return null;\n }\n\n const tokenData = await tokenResponse.json() as { access_token: string };\n const accessToken = tokenData.access_token;\n\n const profileResponse = await fetch(\"https://api.bitbucket.org/2.0/user\", {\n headers: { \"Authorization\": `Bearer ${accessToken}` }\n });\n\n if (!profileResponse.ok) {\n logger.error(\"Failed to get Bitbucket user info\", { detail: await profileResponse.text() });\n return null;\n }\n\n const p = await profileResponse.json() as {\n uuid: string; display_name?: string; username?: string;\n links?: { avatar?: { href?: string } };\n };\n\n // Bitbucket doesn't return email in user profile — fetch from /emails endpoint\n const emailsResponse = await fetch(\"https://api.bitbucket.org/2.0/user/emails\", {\n headers: { \"Authorization\": `Bearer ${accessToken}` }\n });\n\n let selected: { email: string; is_confirmed: boolean } | undefined;\n if (emailsResponse.ok) {\n const emailData = await emailsResponse.json() as {\n values: Array<{ email: string; is_primary: boolean; is_confirmed: boolean }>;\n };\n selected = emailData.values.find(e => e.is_primary && e.is_confirmed)\n || emailData.values.find(e => e.is_confirmed);\n }\n\n if (!selected?.email) { logger.error(\"Bitbucket user has no verified email\"); return null; }\n\n return {\n providerId: p.uuid,\n email: selected.email,\n displayName: p.display_name || p.username || null,\n photoUrl: p.links?.avatar?.href || null,\n // Read off the address we actually chose rather than\n // restating the filter above as a literal `true` — the two\n // cannot drift apart this way.\n emailVerified: providerVerifiedEmail(selected.is_confirmed)\n };\n } catch (error) {\n logger.error(\"Bitbucket OAuth error\", { error: error });\n return null;\n }\n }\n };\n}\n","import type { OAuthProvider, OAuthProviderProfile } from \"./interfaces\";\nimport { logger } from \"../utils/logger\";\nimport { oauthCodeFlowSchema, pkceTokenParams, providerVerifiedEmail, type OAuthCodeFlowPayload } from \"./oauth-code-flow\";\n\n/**\n * Creates a Slack OAuth Provider integration (OAuth 2.0 / \"Sign in with Slack\").\n * Uses the OpenID Connect flow with the \"openid,email,profile\" scopes.\n */\nexport function createSlackProvider(config: { clientId: string; clientSecret: string }): OAuthProvider<OAuthCodeFlowPayload> {\n return {\n id: \"slack\",\n schema: oauthCodeFlowSchema(),\n verify: async (payload: OAuthCodeFlowPayload): Promise<OAuthProviderProfile | null> => {\n try {\n const tokenResponse = await fetch(\"https://slack.com/api/openid.connect.token\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n client_id: config.clientId,\n client_secret: config.clientSecret,\n code: payload.code,\n redirect_uri: payload.redirectUri,\n grant_type: \"authorization_code\",\n ...pkceTokenParams(payload.codeVerifier)\n })\n });\n\n if (!tokenResponse.ok) {\n logger.error(\"Failed to get Slack access token\", { detail: await tokenResponse.text() });\n return null;\n }\n\n const tokenData = await tokenResponse.json() as { ok: boolean; access_token?: string; error?: string };\n if (!tokenData.ok || !tokenData.access_token) {\n logger.error(\"Slack token exchange failed\", { detail: tokenData.error });\n return null;\n }\n\n const profileResponse = await fetch(\"https://slack.com/api/openid.connect.userInfo\", {\n headers: { \"Authorization\": `Bearer ${tokenData.access_token}` }\n });\n\n if (!profileResponse.ok) {\n logger.error(\"Failed to get Slack user info\", { detail: await profileResponse.text() });\n return null;\n }\n\n const p = await profileResponse.json() as {\n ok: boolean; sub: string; name?: string;\n email?: string; picture?: string; email_verified?: boolean;\n };\n\n if (!p.ok || !p.email) {\n logger.error(\"Slack user has no email\");\n return null;\n }\n\n return {\n providerId: p.sub,\n email: p.email,\n displayName: p.name || null,\n photoUrl: p.picture || null,\n emailVerified: providerVerifiedEmail(p.email_verified)\n };\n } catch (error) {\n logger.error(\"Slack OAuth error\", { error: error });\n return null;\n }\n }\n };\n}\n","import type { OAuthProvider, OAuthProviderProfile } from \"./interfaces\";\nimport { logger } from \"../utils/logger\";\nimport { oauthCodeFlowSchema, pkceTokenParams, type OAuthCodeFlowPayload } from \"./oauth-code-flow\";\n\n/**\n * Creates a Spotify OAuth Provider integration.\n * Uses the authorization code flow with the \"user-read-email\" scope.\n */\nexport function createSpotifyProvider(config: { clientId: string; clientSecret: string }): OAuthProvider<OAuthCodeFlowPayload> {\n return {\n id: \"spotify\",\n schema: oauthCodeFlowSchema(),\n verify: async (payload: OAuthCodeFlowPayload): Promise<OAuthProviderProfile | null> => {\n try {\n const basicAuth = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString(\"base64\");\n\n const tokenResponse = await fetch(\"https://accounts.spotify.com/api/token\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n \"Authorization\": `Basic ${basicAuth}`\n },\n body: new URLSearchParams({\n grant_type: \"authorization_code\",\n code: payload.code,\n redirect_uri: payload.redirectUri,\n ...pkceTokenParams(payload.codeVerifier)\n })\n });\n\n if (!tokenResponse.ok) {\n logger.error(\"Failed to get Spotify access token\", { detail: await tokenResponse.text() });\n return null;\n }\n\n const tokenData = await tokenResponse.json() as { access_token: string };\n\n const profileResponse = await fetch(\"https://api.spotify.com/v1/me\", {\n headers: { \"Authorization\": `Bearer ${tokenData.access_token}` }\n });\n\n if (!profileResponse.ok) {\n logger.error(\"Failed to get Spotify user info\", { detail: await profileResponse.text() });\n return null;\n }\n\n const p = await profileResponse.json() as {\n id: string; display_name?: string | null;\n email?: string; images?: Array<{ url: string }>;\n };\n\n if (!p.email) { logger.error(\"Spotify user has no email\"); return null; }\n\n return {\n providerId: p.id,\n email: p.email,\n displayName: p.display_name || null,\n photoUrl: p.images?.[0]?.url || null,\n // `/v1/me` carries no verification field, so there is\n // nothing here to read — and `emailVerified` is the sole\n // gate on attaching this identity to a pre-existing\n // account. Reporting \"not verified\" is the honest answer;\n // users with an existing account link through\n // `POST /auth/link/spotify`, which proves ownership with a\n // live session instead.\n emailVerified: false\n };\n } catch (error) {\n logger.error(\"Spotify OAuth error\", { error: error });\n return null;\n }\n }\n };\n}\n","/**\n * Custom Auth Adapter Factory\n *\n * Provides a minimal-config way for users with existing auth systems\n * to plug into Rebase. Only `verifyRequest` is required — everything\n * else is optional.\n *\n * @example\n * ```ts\n * import { createCustomAuthAdapter } from \"@rebasepro/server\";\n *\n * const auth = createCustomAuthAdapter({\n * verifyRequest: async (request) => {\n * const token = request.headers.get(\"Authorization\")?.replace(\"Bearer \", \"\");\n * if (!token) return null;\n * const decoded = jwt.verify(token, MY_SECRET);\n * return {\n * uid: decoded.sub,\n * email: decoded.email,\n * displayName: decoded.name,\n * roles: decoded.roles || [],\n * isAdmin: decoded.roles?.includes(\"admin\") ?? false,\n * };\n * },\n * });\n * ```\n */\n\nimport type {\n AuthAdapter,\n AuthAdapterCapabilities,\n CustomAuthAdapterOptions\n} from \"@rebasepro/types\";\n\n/**\n * Create a custom auth adapter from minimal options.\n *\n * This is the recommended entry point for users who already have their own\n * auth system and just need to tell Rebase how to extract the user from\n * incoming requests.\n *\n * @param options - Configuration options. Only `verifyRequest` is required.\n * @returns A fully-formed `AuthAdapter` ready for `initializeRebaseBackend()`.\n */\nexport function createCustomAuthAdapter(options: CustomAuthAdapterOptions): AuthAdapter {\n const defaultCapabilities: AuthAdapterCapabilities = {\n hasBuiltInAuthRoutes: false,\n emailPasswordLogin: false,\n registrationEnabled: false,\n passwordReset: false,\n // Off unless the adapter's own createAdminRoutes() mounts a\n // reset-password route and opts in via `capabilities`.\n adminPasswordReset: false,\n sessionManagement: false,\n profileUpdate: false,\n emailVerification: false,\n magicLink: false,\n anonymousLogin: false,\n enabledProviders: [],\n ...options.capabilities\n };\n\n const resolvedVerifyToken = options.verifyToken\n ?? (async (token: string) => {\n // Synthesize a minimal Request so adapters that only implement\n // verifyRequest still work for WebSocket token verification.\n const syntheticRequest = new Request(\"http://localhost/_ws_auth\", {\n headers: { Authorization: `Bearer ${token}` }\n });\n return options.verifyRequest(syntheticRequest);\n });\n\n return {\n id: \"custom\",\n\n serviceKey: options.serviceKey,\n\n verifyRequest: options.verifyRequest,\n\n verifyToken: resolvedVerifyToken,\n\n userManagement: options.userManagement,\n\n transformAuthResponse: options.transformAuthResponse,\n\n getCapabilities() {\n return defaultCapabilities;\n }\n };\n}\n","// Auth module exports\nexport * from \"./interfaces\";\n\nexport { configureJwt, isJwtConfigured, generateAccessToken, verifyAccessToken, generateRefreshToken, hashRefreshToken, getRefreshTokenExpiry, getAccessTokenExpiry, generateDownloadToken, verifyDownloadToken, getJwks, hasAsymmetricSigningKey } from \"./jwt\";\nexport type { JwtConfig, AccessTokenPayload, DownloadTokenPayload } from \"./jwt\";\nexport { createJwksRoutes } from \"./jwks-routes\";\nexport type { JwtSigningKeyConfig, JwtSigningAlgorithm, PublicJwk } from \"./jwt-keys\";\n\nexport { hashPassword, verifyPassword, validatePasswordStrength } from \"./password\";\n// Constant-time compare for static secrets (service keys), so token checks\n// outside this package cannot accidentally use ===.\nexport { safeCompare } from \"./crypto-utils\";\nexport type { PasswordValidationResult } from \"./password\";\n\nexport type { AuthHooks, AuthMethod, ResolvedAuthHooks } from \"./auth-hooks\";\nexport { resolveAuthHooks } from \"./auth-hooks\";\n\nexport { generateSecurePassword, generateSecureToken, hashToken, prepareAdminUserValues, finalizeAdminUserCreation } from \"./admin-user-ops\";\nexport type { AdminUserContext, AdminUserPrepareResult } from \"./admin-user-ops\";\n\n// OAuth Providers\nexport { createGoogleProvider } from \"./google-oauth\";\nexport type { GoogleProviderConfig } from \"./google-oauth\";\nexport { createLinkedinProvider } from \"./linkedin-oauth\";\nexport { createGitHubProvider } from \"./github-oauth\";\nexport { createMicrosoftProvider } from \"./microsoft-oauth\";\nexport { createAppleProvider } from \"./apple-oauth\";\nexport { createFacebookProvider } from \"./facebook-oauth\";\nexport { createTwitterProvider } from \"./twitter-oauth\";\nexport { createDiscordProvider } from \"./discord-oauth\";\nexport { createGitLabProvider } from \"./gitlab-oauth\";\nexport { createBitbucketProvider } from \"./bitbucket-oauth\";\nexport { createSlackProvider } from \"./slack-oauth\";\nexport { createSpotifyProvider } from \"./spotify-oauth\";\nexport type { AppleCodeFlowPayload } from \"./apple-oauth\";\n\n// Shared OAuth machinery — the controls every provider, including a custom one\n// passed through `auth.providers`, is expected to go through.\nexport { oauthCodeFlowSchema, pkceTokenParams, providerVerifiedEmail } from \"./oauth-code-flow\";\nexport type { OAuthCodeFlowPayload } from \"./oauth-code-flow\";\nexport { verifyOidcIdToken, tryVerifyOidcIdToken } from \"./oidc-id-token\";\nexport type { OidcIdTokenClaims, VerifyOidcIdTokenOptions } from \"./oidc-id-token\";\nexport { decideOAuthAutoLink, isRedirectUriAllowed } from \"./oauth-signin-policy\";\nexport type { AutoLinkDecision, AutoLinkRefusal } from \"./oauth-signin-policy\";\n\nexport { requireAuth, requireAdmin, optionalAuth, extractUserFromToken, createAuthMiddleware, queryTokenAuth, fileTokenAuth } from \"./middleware\";\nexport type { AuthMiddlewareOptions, AuthResult } from \"./middleware\";\n\n\nexport { createAuthRoutes } from \"./routes\";\nexport type { AuthModuleConfig, CookieAuthConfig } from \"./routes\";\nexport { buildBuiltinAuthCapabilities } from \"./capabilities\";\nexport type { BuiltinAuthCapabilityInputs } from \"./capabilities\";\n\nexport { mountMagicLinkRoutes } from \"./magic-link-routes\";\n\n// Bot protection. The verifier is injectable, so no provider SDK enters this\n// package's dependency graph — the built-in one is a form post.\nexport {\n buildCaptchaMiddlewares,\n createCaptchaMiddleware,\n createHttpCaptchaVerifier,\n resolveCaptchaVerifier,\n DEFAULT_CAPTCHA_ROUTES\n} from \"./captcha\";\nexport type {\n CaptchaConfig,\n CaptchaProvider,\n CaptchaRoute,\n CaptchaVerifier,\n CaptchaVerifyRequest,\n CaptchaVerifyResult\n} from \"./captcha\";\n\nexport { createResetPasswordRoute } from \"./reset-password-admin\";\nexport type { ResetPasswordRouteConfig } from \"./reset-password-admin\";\n\n\nexport { createRateLimiter, defaultAuthLimiter, strictAuthLimiter, createDataRateLimiter, apiKeyKeyGenerator } from \"./rate-limiter\";\nexport type { DataRateLimitConfig } from \"./rate-limiter\";\nexport { MemoryRateLimitStore } from \"./rate-limit-store\";\nexport type { RateLimitStore, RateLimitDecision } from \"./rate-limit-store\";\nexport { createSqlRateLimitStore } from \"./sql-rate-limit-store\";\nexport type { SqlRateLimitStoreOptions } from \"./sql-rate-limit-store\";\nexport { resolveRateLimitStoreKind, RateLimitStoreConfigurationError } from \"./resolve-rate-limit-store\";\nexport type { RateLimitStoreKind, RateLimitStoreEnv } from \"./resolve-rate-limit-store\";\n\n// API Keys\nexport { createApiKeyStore, createApiKeyRoutes, createPersonalKeyRoutes, isApiKeyToken, resolveApiKey, validateApiKey, httpMethodToOperation } from \"./api-keys\";\nexport type { ApiKey, ApiKeyKind, ApiKeyMasked, ApiKeyWithSecret, CreateApiKeyRequest, CreatePersonalKeyRequest, UpdateApiKeyRequest, ApiKeyStore, DataOperation, KeyTargets } from \"./api-keys\";\n\n// Scopes\nexport { requireScope, requireScopeByMethod, hasScope, callerScopes, getAccessModel, configureAccess, accessModelFromCollections, AccessModelError } from \"./access\";\nexport type { KeyOwnerResolver } from \"./access\";\n\n// Auth Adapters\nexport { createBuiltinAuthAdapter } from \"./builtin-auth-adapter\";\nexport type { BuiltinAuthAdapterConfig } from \"./builtin-auth-adapter\";\nexport { createCustomAuthAdapter } from \"./custom-auth-adapter\";\nexport { createAdapterAuthMiddleware } from \"./adapter-middleware\";\nexport type { AdapterAuthMiddlewareOptions } from \"./adapter-middleware\";\n"],"mappings":"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"}
|