@rebasepro/server 0.21.2-canary.g1ea48be → 0.23.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.
Files changed (115) hide show
  1. package/dist/{GCSStorageController-CLIJXwGS.js → GCSStorageController-CjrA4PMo.js} +37 -23
  2. package/dist/GCSStorageController-CjrA4PMo.js.map +1 -0
  3. package/dist/{S3StorageController-Dcuf8lMA.js → S3StorageController-B6pKDNVj.js} +2 -2
  4. package/dist/{S3StorageController-Dcuf8lMA.js.map → S3StorageController-B6pKDNVj.js.map} +1 -1
  5. package/dist/api/ast-schema-editor.d.ts +35 -0
  6. package/dist/api/live-schema-routes.d.ts +14 -0
  7. package/dist/api/rest/api-generator.d.ts +117 -35
  8. package/dist/api/rest/auth-collection-writes.d.ts +85 -0
  9. package/dist/api/rest/field-access-query.d.ts +6 -2
  10. package/dist/api/rest/idempotency.d.ts +7 -1
  11. package/dist/api/rest/nested-write-access.d.ts +46 -0
  12. package/dist/api/rest/write-validation.d.ts +32 -0
  13. package/dist/{ast-schema-editor-CslO8Oje.js → ast-schema-editor-Mvr50v_S.js} +104 -4
  14. package/dist/ast-schema-editor-Mvr50v_S.js.map +1 -0
  15. package/dist/auth/address-ownership.d.ts +53 -0
  16. package/dist/auth/admin-user-ops.d.ts +35 -2
  17. package/dist/auth/auth-hooks.d.ts +4 -0
  18. package/dist/auth/captcha.d.ts +5 -0
  19. package/dist/auth/interfaces.d.ts +36 -6
  20. package/dist/auth/jwt.d.ts +17 -0
  21. package/dist/auth/oauth-signin-policy.d.ts +25 -8
  22. package/dist/auth/rate-limiter.d.ts +31 -1
  23. package/dist/auth/session-routes.d.ts +7 -0
  24. package/dist/auth/token-revocation.d.ts +4 -1
  25. package/dist/{auth-DZXQRmYD.js → auth-B-GIMpDG.js} +643 -230
  26. package/dist/auth-B-GIMpDG.js.map +1 -0
  27. package/dist/backend-DTAOsLQc.js +30 -0
  28. package/dist/backend-DTAOsLQc.js.map +1 -0
  29. package/dist/backup/backup-common.d.ts +19 -0
  30. package/dist/{backup-DzI9jLwc.js → backup-D7YR94N3.js} +69 -8
  31. package/dist/backup-D7YR94N3.js.map +1 -0
  32. package/dist/boot/driver.d.ts +10 -0
  33. package/dist/boot/env.d.ts +2 -2
  34. package/dist/boot/fetch-bundle.d.ts +18 -1
  35. package/dist/boot/rls-audit-option.d.ts +26 -0
  36. package/dist/boot/sources.d.ts +1 -0
  37. package/dist/{contract-routes-eLxV0le1.js → contract-routes-CbFjuBwa.js} +2 -2
  38. package/dist/{contract-routes-eLxV0le1.js.map → contract-routes-CbFjuBwa.js.map} +1 -1
  39. package/dist/cron/cron-routes.d.ts +7 -2
  40. package/dist/cron/cron-scheduler.d.ts +128 -8
  41. package/dist/cron/cron-store.d.ts +72 -8
  42. package/dist/cron/index.d.ts +1 -1
  43. package/dist/{cron-loader-CQjvjpEw.js → cron-loader-DfTj2Hbi.js} +2 -2
  44. package/dist/{cron-loader-CQjvjpEw.js.map → cron-loader-DfTj2Hbi.js.map} +1 -1
  45. package/dist/{cron-routes-cv0hSd-O.js → cron-routes-eE8nif_b.js} +33 -12
  46. package/dist/cron-routes-eE8nif_b.js.map +1 -0
  47. package/dist/{cron-scheduler-COPQxlEq.js → cron-scheduler-B0pLfAix.js} +394 -68
  48. package/dist/cron-scheduler-B0pLfAix.js.map +1 -0
  49. package/dist/{cron-store-Bdm7JqFB.js → cron-store-TcoGz-xS.js} +133 -12
  50. package/dist/cron-store-TcoGz-xS.js.map +1 -0
  51. package/dist/{ddl-bootstrap-CfNvxMuK.js → ddl-bootstrap-C6mo0Kmz.js} +2 -25
  52. package/dist/ddl-bootstrap-C6mo0Kmz.js.map +1 -0
  53. package/dist/email/link-base.d.ts +5 -4
  54. package/dist/email/smtp-email-service.d.ts +13 -1
  55. package/dist/email/templates.d.ts +9 -0
  56. package/dist/email/types.d.ts +3 -2
  57. package/dist/env.d.ts +24 -5
  58. package/dist/{history-recorder-CYfkqP2X.js → history-recorder-B4MpJfJK.js} +8 -6
  59. package/dist/history-recorder-B4MpJfJK.js.map +1 -0
  60. package/dist/{history-store-B1CBwLfi.js → history-store-BhxWOuz9.js} +2 -2
  61. package/dist/{history-store-B1CBwLfi.js.map → history-store-BhxWOuz9.js.map} +1 -1
  62. package/dist/index.d.ts +6 -2
  63. package/dist/index.es.js +2769 -878
  64. package/dist/index.es.js.map +1 -1
  65. package/dist/init/docs.d.ts +5 -2
  66. package/dist/init/global-callbacks.d.ts +21 -0
  67. package/dist/init/shutdown.d.ts +8 -3
  68. package/dist/jobs/index.d.ts +2 -2
  69. package/dist/jobs/job-queue.d.ts +23 -2
  70. package/dist/jobs/job-store.d.ts +37 -5
  71. package/dist/jobs/types.d.ts +8 -6
  72. package/dist/{jobs-2fO2BI8P.js → jobs-CazMYhyy.js} +305 -166
  73. package/dist/jobs-CazMYhyy.js.map +1 -0
  74. package/dist/{jwt-C4OW-DNq.js → jwt-DnQHNFCl.js} +77 -25
  75. package/dist/{jwt-C4OW-DNq.js.map → jwt-DnQHNFCl.js.map} +1 -1
  76. package/dist/{keys-Qfc4XieN.js → keys-CogCQpxG.js} +18 -3
  77. package/dist/{keys-Qfc4XieN.js.map → keys-CogCQpxG.js.map} +1 -1
  78. package/dist/{logs-routes-3EEzPjhl.js → logs-routes-Bj4TYYUl.js} +7 -4
  79. package/dist/{logs-routes-3EEzPjhl.js.map → logs-routes-Bj4TYYUl.js.map} +1 -1
  80. package/dist/mcp/mcp-routes.d.ts +38 -2
  81. package/dist/mcp/mcp-tools.d.ts +7 -1
  82. package/dist/mcp/oauth-routes.d.ts +27 -0
  83. package/dist/mcp/oauth-store.d.ts +29 -13
  84. package/dist/metrics/history-recorder.d.ts +1 -1
  85. package/dist/{openapi-generator-D8ZIN_ss.js → openapi-generator-O_O24MAT.js} +34 -12
  86. package/dist/openapi-generator-O_O24MAT.js.map +1 -0
  87. package/dist/{query-parser-CoIOmflr.js → query-parser-DGRVFNM3.js} +39 -30
  88. package/dist/query-parser-DGRVFNM3.js.map +1 -0
  89. package/dist/rls-audit/index.d.ts +4 -0
  90. package/dist/{schema-editor-routes-DdLihzp0.js → schema-editor-routes-C5-lh_jO.js} +2 -2
  91. package/dist/{schema-editor-routes-DdLihzp0.js.map → schema-editor-routes-C5-lh_jO.js.map} +1 -1
  92. package/dist/{src-BYax9_rm.js → src-pmvW7BFx.js} +189 -14
  93. package/dist/src-pmvW7BFx.js.map +1 -0
  94. package/dist/{src-Br6ARbs6.js → src-vkcwKXbT.js} +2 -27
  95. package/dist/{src-Br6ARbs6.js.map → src-vkcwKXbT.js.map} +1 -1
  96. package/dist/storage/GCSStorageController.d.ts +11 -1
  97. package/dist/storage/keys.d.ts +12 -0
  98. package/dist/storage/rendition-cache.d.ts +11 -1
  99. package/dist/storage/request-keys.d.ts +67 -0
  100. package/dist/storage/types.d.ts +17 -1
  101. package/dist/types-BfKcm9do.js.map +1 -1
  102. package/package.json +9 -9
  103. package/dist/GCSStorageController-CLIJXwGS.js.map +0 -1
  104. package/dist/ast-schema-editor-CslO8Oje.js.map +0 -1
  105. package/dist/auth-DZXQRmYD.js.map +0 -1
  106. package/dist/backup-DzI9jLwc.js.map +0 -1
  107. package/dist/cron-routes-cv0hSd-O.js.map +0 -1
  108. package/dist/cron-scheduler-COPQxlEq.js.map +0 -1
  109. package/dist/cron-store-Bdm7JqFB.js.map +0 -1
  110. package/dist/ddl-bootstrap-CfNvxMuK.js.map +0 -1
  111. package/dist/history-recorder-CYfkqP2X.js.map +0 -1
  112. package/dist/jobs-2fO2BI8P.js.map +0 -1
  113. package/dist/openapi-generator-D8ZIN_ss.js.map +0 -1
  114. package/dist/query-parser-CoIOmflr.js.map +0 -1
  115. package/dist/src-BYax9_rm.js.map +0 -1
@@ -3,20 +3,21 @@ import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { i as __toESM, n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
- import { P as revokeInternalTableSql, at as isAnonymousUid, it as ANONYMOUS_USER_ID } from "./src-BYax9_rm.js";
7
- import "./src-Br6ARbs6.js";
8
- import { n as createDdlBootstrapper, o as isSQLAdmin } from "./ddl-bootstrap-CfNvxMuK.js";
6
+ import { F as revokeInternalTableSql, ct as ANONYMOUS_USER_ID, lt as isAnonymousUid } from "./src-pmvW7BFx.js";
7
+ import "./src-vkcwKXbT.js";
8
+ import { t as isSQLAdmin } from "./backend-DTAOsLQc.js";
9
9
  import { t as firstSqlRow } from "./sql-rows-C6GEc2oE.js";
10
10
  import { r as logger } from "./logger-DO2PZc4i.js";
11
11
  import { r as errorHandler, t as ApiError } from "./errors-DWsX4yTd.js";
12
- import { a as resolveListLimitParam } from "./query-parser-CoIOmflr.js";
13
- import { C as require_jsonwebtoken, E as randomInt$1, O as sha256Hex, _ as verifyAccessToken, b as verifyMfaPendingToken, c as getAccessTokenExpiry, d as getRefreshTokenTtlMs, f as hasAsymmetricSigningKey, i as generateDownloadToken, l as getJwks, m as isJwtConfigured, n as configureJwt, o as generateMfaPendingToken, p as hashRefreshToken, r as generateAccessToken, s as generateRefreshToken, t as MAX_COOKIE_AGE_MS, u as getRefreshTokenExpiry, v as verifyDownloadToken, w as constantTimeEqual } from "./jwt-C4OW-DNq.js";
12
+ import { a as resolveListLimitParam } from "./query-parser-DGRVFNM3.js";
13
+ import { C as require_jsonwebtoken, E as randomInt$1, O as sha256Hex, _ as verifyAccessToken, b as verifyMfaPendingToken, c as getAccessTokenExpiry, d as getRefreshTokenTtlMs, f as hasAsymmetricSigningKey, i as generateDownloadToken, l as getJwks, m as isJwtConfigured, n as configureJwt, o as generateMfaPendingToken, p as hashRefreshToken, r as generateAccessToken, s as generateRefreshToken, t as MAX_COOKIE_AGE_MS, u as getRefreshTokenExpiry, v as verifyDownloadToken, w as constantTimeEqual } from "./jwt-DnQHNFCl.js";
14
14
  import { n as hasAdministrativeRole, r as isAdministrativeRole, t as ADMINISTRATIVE_ROLES } from "./admin-roles-vYdp_Pil.js";
15
- import { c as tryCanonicalStorageKey, i as canonicalStorageId } from "./keys-Qfc4XieN.js";
15
+ import { a as canonicalStorageId, l as tryCanonicalStorageKey } from "./keys-CogCQpxG.js";
16
+ import { n as createDdlBootstrapper } from "./ddl-bootstrap-C6mo0Kmz.js";
16
17
  import { createHash, randomBytes, randomInt } from "node:crypto";
17
18
  import { Hono } from "hono";
18
19
  import { promisify } from "util";
19
- import { createCipheriv, createDecipheriv, createHash as createHash$1, createHmac, createPublicKey, randomBytes as randomBytes$1, randomUUID, scrypt, timingSafeEqual } from "crypto";
20
+ import { createCipheriv, createDecipheriv, createHash as createHash$1, createHmac, createPublicKey, randomBytes as randomBytes$1, randomUUID as randomUUID$1, scrypt, timingSafeEqual } from "crypto";
20
21
  import { getConnInfo } from "@hono/node-server/conninfo";
21
22
  import { createMiddleware } from "hono/factory";
22
23
  import { z } from "zod";
@@ -553,6 +554,16 @@ If you didn't create an account with ${appName}, you can safely ignore this emai
553
554
  };
554
555
  }
555
556
  /**
557
+ * How long an invitation's set-password link stays valid.
558
+ *
559
+ * One number for both halves: `finalizeAdminUserCreation` mints the token for
560
+ * this long and the template below tells the invitee this long. They were two
561
+ * literals, and the email said "1 hour" for a token that lived 24 — so an
562
+ * invitee who opened it the next morning believed the link was dead.
563
+ */
564
+ var USER_INVITATION_LINK_TTL_MS = 1440 * 60 * 1e3;
565
+ var USER_INVITATION_LINK_TTL_HOURS = USER_INVITATION_LINK_TTL_MS / (3600 * 1e3);
566
+ /**
556
567
  * Default user invitation email template
557
568
  * Sent when an admin creates a new user account
558
569
  */
@@ -593,7 +604,7 @@ function getUserInvitationTemplate(setPasswordUrl, user, appName = DEFAULT_APP_N
593
604
  </p>
594
605
 
595
606
  <div style="${styles.warning}">
596
- ⏰ This link will expire in 1 hour for security reasons.
607
+ ⏰ This link will expire in ${USER_INVITATION_LINK_TTL_HOURS} hours for security reasons.
597
608
  </div>
598
609
 
599
610
  <div style="${styles.footer}">
@@ -616,7 +627,7 @@ An account has been created for you on ${appName}.
616
627
  Click this link to set your password and get started:
617
628
  ${setPasswordUrl}
618
629
 
619
- This link will expire in 1 hour for security reasons.
630
+ This link will expire in ${USER_INVITATION_LINK_TTL_HOURS} hours for security reasons.
620
631
 
621
632
  If you weren't expecting this invitation, you can safely ignore this email.
622
633
  `.trim();
@@ -838,12 +849,86 @@ No action is needed.
838
849
  };
839
850
  }
840
851
  //#endregion
852
+ //#region src/email/link-base.ts
853
+ /**
854
+ * The config fields consulted for each link kind, most specific first.
855
+ *
856
+ * `resetPasswordUrl` is the general fallback because it is the only one any boot
857
+ * path sets, and in every real deployment all three point at the same frontend.
858
+ */
859
+ var LINK_BASE_FIELDS = {
860
+ resetPassword: ["resetPasswordUrl"],
861
+ verifyEmail: ["verifyEmailUrl", "resetPasswordUrl"],
862
+ magicLink: ["magicLinkUrl", "resetPasswordUrl"]
863
+ };
864
+ /**
865
+ * True when `value` is an absolute `http(s)` URL — the only kind a mail client
866
+ * can follow. A protocol-relative or path-only base is treated as missing.
867
+ */
868
+ function isAbsoluteHttpUrl(value) {
869
+ try {
870
+ const url = new URL(value);
871
+ return url.protocol === "http:" || url.protocol === "https:";
872
+ } catch {
873
+ return false;
874
+ }
875
+ }
876
+ /** Drop a trailing slash so callers can append `/verify-email?token=…`. */
877
+ function normalizeBase(value) {
878
+ return value.replace(/\/+$/, "");
879
+ }
880
+ /**
881
+ * The absolute base URL for a link kind, or `""` when the config has none.
882
+ *
883
+ * Callers append their path to the result. `""` is only reachable on a config
884
+ * that never went through {@link assertEmailLinkBases} — a hand-constructed
885
+ * `SMTPEmailService` in a test, say — and is kept rather than thrown so that a
886
+ * misconfiguration cannot turn a password-reset request into a 500 that
887
+ * distinguishes existing accounts from missing ones.
888
+ */
889
+ function resolveEmailLinkBase(config, kind) {
890
+ if (!config) return "";
891
+ for (const field of LINK_BASE_FIELDS[kind]) {
892
+ const value = config[field];
893
+ if (typeof value === "string" && isAbsoluteHttpUrl(value)) return normalizeBase(value);
894
+ }
895
+ return "";
896
+ }
897
+ /**
898
+ * Throw when an email configuration cannot produce a followable link of every
899
+ * kind it sends.
900
+ *
901
+ * Called from `createEmailService`, i.e. from every boot path that wires email
902
+ * up (the managed runtime, both driver bootstrappers, and any app that passes
903
+ * `auth.email`). A base that is set but relative is reported separately from one
904
+ * that is missing, because the two have different fixes.
905
+ */
906
+ function assertEmailLinkBases(config) {
907
+ const relative = [
908
+ "resetPasswordUrl",
909
+ "verifyEmailUrl",
910
+ "magicLinkUrl"
911
+ ].filter((field) => typeof config[field] === "string" && config[field] !== "" && !isAbsoluteHttpUrl(config[field]));
912
+ if (relative.length > 0) throw new Error(`Email is configured with a relative link base: ${relative.map((f) => `email.${f}`).join(", ")}. Emailed links must be absolute (e.g. "https://app.example.com") — a mail client has no base document to resolve a relative href against, so the link is dead.`);
913
+ const unresolved = Object.keys(LINK_BASE_FIELDS).filter((kind) => !resolveEmailLinkBase(config, kind));
914
+ if (unresolved.length === Object.keys(LINK_BASE_FIELDS).length) throw new Error("Email is configured but no base URL for emailed links is set. Password-reset, verification and magic-link emails would carry relative hrefs, which are dead links in every mail client. Set FRONTEND_URL (or `auth.email.resetPasswordUrl`) to an absolute URL such as \"https://app.example.com\".");
915
+ if (unresolved.length > 0) throw new Error(`Email is configured without a base URL for ${unresolved.map((kind) => LINK_KIND_LABELS[kind]).join(" and ")} emails, so their links would be relative hrefs — dead in every mail client. Those links fall back to \`auth.email.resetPasswordUrl\` only: set FRONTEND_URL (or \`auth.email.resetPasswordUrl\`) to an absolute URL such as "https://app.example.com".`);
916
+ }
917
+ /** How each link kind is named in a boot error. */
918
+ var LINK_KIND_LABELS = {
919
+ resetPassword: "password-reset",
920
+ verifyEmail: "verification",
921
+ magicLink: "magic-link"
922
+ };
923
+ //#endregion
841
924
  //#region src/auth/admin-user-ops.ts
842
925
  /**
843
926
  * Admin User Operations
844
927
  *
845
928
  * Shared utilities and orchestration for admin-initiated user management
846
- * (user creation via REST API, password reset via admin panel).
929
+ * (user creation via REST API, password reset via admin panel), and the rules
930
+ * user administration holds to whichever door it arrives by — the admin user
931
+ * routes, or the auth collection through the data API.
847
932
  *
848
933
  * Hook resolution order:
849
934
  * 1. Collection-level hook (`auth.onCreateUser` on the collection) — closest to the data
@@ -965,9 +1050,9 @@ async function finalizeAdminUserCreation(entity, clearPassword, ctx) {
965
1050
  if (!!(ctx.emailService && ctx.emailService.isConfigured())) try {
966
1051
  const token = generateSecureToken();
967
1052
  const tokenHash = hashToken$1(token);
968
- const expiresAt = new Date(Date.now() + 1440 * 60 * 1e3);
1053
+ const expiresAt = new Date(Date.now() + USER_INVITATION_LINK_TTL_MS);
969
1054
  await ctx.authRepo.createPasswordResetToken(entity.id, tokenHash, expiresAt);
970
- const setPasswordUrl = `${ctx.emailConfig?.resetPasswordUrl || ""}/reset-password?token=${token}`;
1055
+ const setPasswordUrl = `${resolveEmailLinkBase(ctx.emailConfig, "resetPassword")}/reset-password?token=${token}`;
971
1056
  const { appName, logoUrl } = resolveEmailBranding(ctx.emailConfig);
972
1057
  const templateFn = ctx.emailConfig?.templates?.userInvitation;
973
1058
  const emailContent = templateFn ? templateFn(setPasswordUrl, {
@@ -1029,6 +1114,88 @@ async function completeUserCreation(prepared, finalize) {
1029
1114
  ...result.emailDeliveryFailed ? { emailDeliveryFailed: true } : {}
1030
1115
  };
1031
1116
  }
1117
+ /**
1118
+ * Refuse a change that leaves no administrator: `leaving` are users losing the
1119
+ * role (demoted, or deleted), and the project must keep at least one.
1120
+ *
1121
+ * Judged over the whole set, so a bulk write that takes every admin at once is
1122
+ * refused even though each of its rows alone would leave one behind.
1123
+ */
1124
+ async function assertAdministratorsRemain(authRepo, leaving, message) {
1125
+ const admins = [];
1126
+ for (const uid of new Set(leaving)) if ((await authRepo.getUserRoleIds(uid)).includes("admin")) admins.push(uid);
1127
+ if (admins.length === 0) return;
1128
+ const { total } = await authRepo.listUsersPaginated({
1129
+ roleId: "admin",
1130
+ limit: 1
1131
+ });
1132
+ if (total <= admins.length) throw ApiError.forbidden(message, "LAST_ADMIN");
1133
+ }
1134
+ /** Refuse deleting these users when that would leave the project with no administrator. */
1135
+ async function assertUserDeletionsAllowed(authRepo, uids) {
1136
+ await assertAdministratorsRemain(authRepo, uids, uids.length > 1 ? "Cannot delete every administrator" : "Cannot delete the last administrator");
1137
+ }
1138
+ /** Refuse setting these users' roles when that would leave the project with no administrator. */
1139
+ async function assertRoleChangesAllowed(authRepo, changes) {
1140
+ await assertAdministratorsRemain(authRepo, changes.filter((change) => !change.roles.includes("admin")).map((change) => change.uid), changes.length > 1 ? "Cannot demote every administrator" : "Cannot demote the last administrator");
1141
+ }
1142
+ /**
1143
+ * A user's new email, in the form sign-in looks it up by — or a 409 when
1144
+ * another account holds it.
1145
+ *
1146
+ * Sign-in, password reset and magic links all find the account by
1147
+ * `normalizeEmail(input)`. An address stored as typed is one those lookups
1148
+ * never match: its owner is locked out, and registering the address again hits
1149
+ * the case-insensitive unique index.
1150
+ */
1151
+ async function normalizeUserEmailChange(authRepo, uid, email) {
1152
+ const normalized = normalizeEmail(email);
1153
+ const holder = await authRepo.getUserByEmail(normalized);
1154
+ if (holder && holder.id !== uid) throw ApiError.conflict("Email already registered", "EMAIL_EXISTS");
1155
+ return normalized;
1156
+ }
1157
+ /**
1158
+ * Run `afterUserDelete` without letting it fail the deletion it follows: the
1159
+ * user is already gone, so a failing listener is logged, not answered.
1160
+ */
1161
+ function runAfterUserDelete(resolvedHooks, uid) {
1162
+ if (!resolvedHooks.afterUserDelete) return;
1163
+ resolvedHooks.afterUserDelete(uid).catch((err) => {
1164
+ logger.error("[AuthHooks] afterUserDelete error", { error: err instanceof Error ? err.message : err });
1165
+ });
1166
+ }
1167
+ /**
1168
+ * The auth adapter's side of user administration through the auth collection
1169
+ * (`AuthAdapter.prepareUserUpdates` and the deletion pair): the admin user
1170
+ * routes' rules and hooks, for the data API's writes to the same rows.
1171
+ */
1172
+ function authCollectionUserAdmin(ctx) {
1173
+ const { authRepo, resolvedHooks } = ctx;
1174
+ return {
1175
+ async prepareUserUpdates(updates) {
1176
+ await assertRoleChangesAllowed(authRepo, updates.flatMap(({ uid, values }) => Array.isArray(values.roles) ? [{
1177
+ uid,
1178
+ roles: values.roles
1179
+ }] : []));
1180
+ const prepared = [];
1181
+ for (const { uid, values } of updates) prepared.push(typeof values.email === "string" ? {
1182
+ ...values,
1183
+ email: await normalizeUserEmailChange(authRepo, uid, values.email)
1184
+ } : values);
1185
+ return prepared;
1186
+ },
1187
+ async prepareUserDeletions(uids) {
1188
+ await assertUserDeletionsAllowed(authRepo, uids);
1189
+ if (resolvedHooks.beforeUserDelete) for (const uid of uids) await resolvedHooks.beforeUserDelete(uid);
1190
+ },
1191
+ async finalizeUserDeletions(uids) {
1192
+ for (const uid of uids) {
1193
+ await authRepo.deleteAllRefreshTokensForUser(uid);
1194
+ runAfterUserDelete(resolvedHooks, uid);
1195
+ }
1196
+ }
1197
+ };
1198
+ }
1032
1199
  //#endregion
1033
1200
  //#region src/auth/token-revocation.ts
1034
1201
  /**
@@ -1124,6 +1291,9 @@ async function revokeAllSessions(authRepo, uid) {
1124
1291
  * `test/password-change-revokes-sessions.test.ts` holds every route that sets a
1125
1292
  * password to this, and fails if anything but this function calls
1126
1293
  * `updatePassword`.
1294
+ *
1295
+ * `null` removes the password instead, for the same reason and with the same
1296
+ * revocation: see `confirmAddressOwnership`.
1127
1297
  */
1128
1298
  async function replaceUserPassword(authRepo, uid, passwordHash) {
1129
1299
  await authRepo.updatePassword(uid, passwordHash);
@@ -1881,7 +2051,7 @@ function createAdapterAuthMiddleware(options) {
1881
2051
  try {
1882
2052
  authenticatedUser = await adapter.verifyRequest(c.req.raw);
1883
2053
  } catch (error) {
1884
- return refuse(c, ApiError.unauthenticated("Unauthorized"));
2054
+ return refuse(c, error instanceof ApiError && error.statusCode >= 400 ? error : ApiError.unauthenticated("Unauthorized"));
1885
2055
  }
1886
2056
  if (authenticatedUser) {
1887
2057
  c.set("user", {
@@ -1894,7 +2064,8 @@ function createAdapterAuthMiddleware(options) {
1894
2064
  c.set("driver", await scopeDataDriver(driver, {
1895
2065
  uid: authenticatedUser.uid,
1896
2066
  roles: authenticatedUser.roles,
1897
- isAnonymous: authenticatedUser.isAnonymous === true
2067
+ isAnonymous: authenticatedUser.isAnonymous === true,
2068
+ ...authenticatedUser.claims ? { claims: authenticatedUser.claims } : {}
1898
2069
  }));
1899
2070
  } catch (error) {
1900
2071
  logger.error("[AUTH-ADAPTER] RLS scoping failed for authenticated user", { error });
@@ -1962,6 +2133,7 @@ async function verifyPassword(password, storedHash) {
1962
2133
  if (!saltHex || !hashHex) return false;
1963
2134
  const salt = Buffer.from(saltHex, "hex");
1964
2135
  const storedKey = Buffer.from(hashHex, "hex");
2136
+ if (storedKey.length !== KEY_LENGTH) return false;
1965
2137
  return timingSafeEqual(await scryptAsync(password, salt, KEY_LENGTH, SCRYPT_PARAMS), storedKey);
1966
2138
  }
1967
2139
  //#endregion
@@ -2020,69 +2192,6 @@ function resolveAuthHooks(hooks) {
2020
2192
  };
2021
2193
  }
2022
2194
  //#endregion
2023
- //#region src/email/link-base.ts
2024
- /**
2025
- * The config fields consulted for each link kind, most specific first.
2026
- *
2027
- * `resetPasswordUrl` is the general fallback because it is the only one any boot
2028
- * path sets, and in every real deployment all three point at the same frontend.
2029
- */
2030
- var LINK_BASE_FIELDS = {
2031
- resetPassword: ["resetPasswordUrl"],
2032
- verifyEmail: ["verifyEmailUrl", "resetPasswordUrl"],
2033
- magicLink: ["magicLinkUrl", "resetPasswordUrl"]
2034
- };
2035
- /**
2036
- * True when `value` is an absolute `http(s)` URL — the only kind a mail client
2037
- * can follow. A protocol-relative or path-only base is treated as missing.
2038
- */
2039
- function isAbsoluteHttpUrl(value) {
2040
- try {
2041
- const url = new URL(value);
2042
- return url.protocol === "http:" || url.protocol === "https:";
2043
- } catch {
2044
- return false;
2045
- }
2046
- }
2047
- /** Drop a trailing slash so callers can append `/verify-email?token=…`. */
2048
- function normalizeBase(value) {
2049
- return value.replace(/\/+$/, "");
2050
- }
2051
- /**
2052
- * The absolute base URL for a link kind, or `""` when the config has none.
2053
- *
2054
- * Callers append their path to the result. `""` is only reachable on a config
2055
- * that never went through {@link assertEmailLinkBases} — a hand-constructed
2056
- * `SMTPEmailService` in a test, say — and is kept rather than thrown so that a
2057
- * misconfiguration cannot turn a password-reset request into a 500 that
2058
- * distinguishes existing accounts from missing ones.
2059
- */
2060
- function resolveEmailLinkBase(config, kind) {
2061
- if (!config) return "";
2062
- for (const field of LINK_BASE_FIELDS[kind]) {
2063
- const value = config[field];
2064
- if (typeof value === "string" && isAbsoluteHttpUrl(value)) return normalizeBase(value);
2065
- }
2066
- return "";
2067
- }
2068
- /**
2069
- * Throw when an email configuration cannot produce a followable link.
2070
- *
2071
- * Called from `createEmailService`, i.e. from every boot path that wires email
2072
- * up (the managed runtime, both driver bootstrappers, and any app that passes
2073
- * `auth.email`). A base that is set but relative is reported separately from one
2074
- * that is missing, because the two have different fixes.
2075
- */
2076
- function assertEmailLinkBases(config) {
2077
- const relative = [
2078
- "resetPasswordUrl",
2079
- "verifyEmailUrl",
2080
- "magicLinkUrl"
2081
- ].filter((field) => typeof config[field] === "string" && config[field] !== "" && !isAbsoluteHttpUrl(config[field]));
2082
- if (relative.length > 0) throw new Error(`Email is configured with a relative link base: ${relative.map((f) => `email.${f}`).join(", ")}. Emailed links must be absolute (e.g. "https://app.example.com") — a mail client has no base document to resolve a relative href against, so the link is dead.`);
2083
- if (!resolveEmailLinkBase(config, "resetPassword") && !resolveEmailLinkBase(config, "verifyEmail")) throw new Error("Email is configured but no base URL for emailed links is set. Password-reset, verification and magic-link emails would carry relative hrefs, which are dead links in every mail client. Set FRONTEND_URL (or `auth.email.resetPasswordUrl`) to an absolute URL such as \"https://app.example.com\".");
2084
- }
2085
- //#endregion
2086
2195
  //#region src/email/smtp-email-service.ts
2087
2196
  var _nodemailer;
2088
2197
  async function loadNodemailer() {
@@ -2136,16 +2245,32 @@ function normalizeMessageId(value) {
2136
2245
  var SMTPEmailService = class {
2137
2246
  transporter = null;
2138
2247
  config;
2139
- _initialized = false;
2248
+ /**
2249
+ * The transporter being built, shared by every send that arrives while it
2250
+ * is.
2251
+ *
2252
+ * A flag set before the `await` said "initialised" while nodemailer was
2253
+ * still loading, so a second send in that window found no transporter and
2254
+ * threw "Email service not configured" — the first burst of mail after a
2255
+ * boot lost every message but one. Every caller awaits this one promise
2256
+ * instead. Cleared on failure, so one bad moment is not remembered for the
2257
+ * life of the process.
2258
+ */
2259
+ transporterReady = null;
2140
2260
  constructor(config) {
2141
2261
  this.config = config;
2142
2262
  }
2143
2263
  /**
2144
2264
  * Lazily initialize the SMTP transporter on first use
2145
2265
  */
2146
- async ensureTransporter() {
2147
- if (this._initialized) return;
2148
- this._initialized = true;
2266
+ ensureTransporter() {
2267
+ if (!this.transporterReady) this.transporterReady = this.buildTransporter().catch((error) => {
2268
+ this.transporterReady = null;
2269
+ throw error;
2270
+ });
2271
+ return this.transporterReady;
2272
+ }
2273
+ async buildTransporter() {
2149
2274
  if (this.config.smtp) {
2150
2275
  const nodemailer = await loadNodemailer();
2151
2276
  let smtpName = this.config.smtp.name;
@@ -2422,13 +2547,42 @@ function warnIfProxiedButUntrusted(hasForwardedFor) {
2422
2547
  logger.warn("[RateLimit] Requests carry X-Forwarded-For but TRUSTED_PROXY_HOPS is 0, so it is being ignored and every client behind the proxy shares one rate-limit bucket. If this server really is behind a reverse proxy, set TRUSTED_PROXY_HOPS to the number of proxies in front of it (1 for a single ingress or load balancer). If it is exposed directly, this warning means someone is sending the header themselves and 0 is the correct setting.");
2423
2548
  }
2424
2549
  /**
2550
+ * The store this deployment shares its counts in, when it has one.
2551
+ *
2552
+ * Module state, because the limiters that read it are module constants: the
2553
+ * auth limiters are built at import time, before `initializeRebaseBackend`
2554
+ * has read `REBASE_RATE_LIMIT_STORE` or `rateLimit.store`, so they cannot be
2555
+ * handed the store the way the data limiter is. Without this they each kept a
2556
+ * private memory store whatever the deployment asked for, and on N replicas
2557
+ * every login, reset, OTP and MFA limit was N times looser than configured.
2558
+ */
2559
+ var sharedRateLimitStore;
2560
+ /**
2561
+ * Point every named limiter at the deployment's shared store — or, with
2562
+ * `undefined`, back at its own memory. Called once per boot with the store
2563
+ * the data limiters use, and only when that store is shared (SQL, or one the
2564
+ * operator supplied): the default memory store is swept on the data window,
2565
+ * which is not every limiter's window.
2566
+ */
2567
+ function setSharedRateLimitStore(store) {
2568
+ sharedRateLimitStore = store;
2569
+ }
2570
+ /** The deployment's store under `name` when there is one, this limiter's own memory otherwise. */
2571
+ function namedStore(name, windowMs) {
2572
+ const local = new MemoryRateLimitStore(windowMs);
2573
+ return {
2574
+ hit: (key, window, limit) => sharedRateLimitStore ? sharedRateLimitStore.hit(`${name}:${key}`, window, limit) : local.hit(key, window, limit),
2575
+ dispose: () => local.dispose()
2576
+ };
2577
+ }
2578
+ /**
2425
2579
  * Create a rate-limiting middleware.
2426
2580
  *
2427
2581
  * Uses a sliding window: only hits within the last `windowMs` are counted.
2428
2582
  */
2429
2583
  function createRateLimiter(options = {}) {
2430
2584
  const trustedProxyHops = resolveTrustedProxyHops(options.trustedProxyHops);
2431
- const { windowMs = 900 * 1e3, limit = 100, keyGenerator = (c) => defaultKeyGenerator(c, trustedProxyHops), message = "Too many requests, please try again later.", store = new MemoryRateLimitStore(windowMs), resolveLimit } = options;
2585
+ const { windowMs = 900 * 1e3, limit = 100, keyGenerator = (c) => defaultKeyGenerator(c, trustedProxyHops), message = "Too many requests, please try again later.", name, store = name ? namedStore(name, windowMs) : new MemoryRateLimitStore(windowMs), resolveLimit } = options;
2432
2586
  return async (c, next) => {
2433
2587
  const effectiveLimit = resolveLimit ? await resolveLimit(c) : limit;
2434
2588
  if (effectiveLimit === null) return next();
@@ -2490,26 +2644,36 @@ function socketAddress(c) {
2490
2644
  * anonymous callers throttle each other than that any of them throttles nobody.
2491
2645
  */
2492
2646
  function defaultKeyGenerator(c, trustedProxyHops = resolveTrustedProxyHops()) {
2647
+ return clientAddress((name) => c.req.header(name), () => socketAddress(c), trustedProxyHops);
2648
+ }
2649
+ /**
2650
+ * {@link defaultKeyGenerator}'s rule, over a request's headers and connection
2651
+ * address rather than a Hono context — so a door that is not an HTTP request
2652
+ * (the realtime socket's upgrade request) reads the caller's address the same
2653
+ * way.
2654
+ */
2655
+ function clientAddress(header, connectionAddress, trustedProxyHops) {
2493
2656
  if (trustedProxyHops === 0) {
2494
- warnIfProxiedButUntrusted(!!c.req.header("x-forwarded-for"));
2495
- return socketAddress(c) ?? "unknown";
2657
+ warnIfProxiedButUntrusted(!!header("x-forwarded-for"));
2658
+ return connectionAddress() ?? "unknown";
2496
2659
  }
2497
2660
  {
2498
- const forwardedFor = c.req.header("x-forwarded-for");
2661
+ const forwardedFor = header("x-forwarded-for");
2499
2662
  if (forwardedFor) {
2500
2663
  const ips = forwardedFor.split(",").map((s) => s.trim()).filter(Boolean);
2501
2664
  if (ips.length > 0) return ips[Math.max(0, ips.length - trustedProxyHops)];
2502
2665
  }
2503
- const realIp = c.req.header("x-real-ip");
2666
+ const realIp = header("x-real-ip");
2504
2667
  if (realIp) return realIp;
2505
2668
  }
2506
- return socketAddress(c) ?? "unknown";
2669
+ return connectionAddress() ?? "unknown";
2507
2670
  }
2508
2671
  /**
2509
2672
  * Pre-configured rate limiter for general auth endpoints (login, register).
2510
2673
  * 200 requests per 15 minutes per IP.
2511
2674
  */
2512
2675
  var defaultAuthLimiter = createRateLimiter({
2676
+ name: "auth-default",
2513
2677
  windowMs: 900 * 1e3,
2514
2678
  limit: 200,
2515
2679
  message: "Too many authentication attempts, please try again later."
@@ -2519,6 +2683,7 @@ var defaultAuthLimiter = createRateLimiter({
2519
2683
  * 50 requests per 15 minutes per IP.
2520
2684
  */
2521
2685
  var strictAuthLimiter = createRateLimiter({
2686
+ name: "auth-strict",
2522
2687
  windowMs: 900 * 1e3,
2523
2688
  limit: 50,
2524
2689
  message: "Too many requests to this sensitive endpoint, please try again later."
@@ -2577,6 +2742,7 @@ var captureRecipientEmail = async (c, next) => {
2577
2742
  * the limiter is never silently a no-op.
2578
2743
  */
2579
2744
  var recipientEmailLimiter = createRateLimiter({
2745
+ name: "auth-recipient-email",
2580
2746
  windowMs: 900 * 1e3,
2581
2747
  limit: 5,
2582
2748
  keyGenerator: (c) => {
@@ -2592,6 +2758,7 @@ async function notBefore$1(startedAt, floorMs, result) {
2592
2758
  return result;
2593
2759
  }
2594
2760
  var verificationEmailLimiter = createRateLimiter({
2761
+ name: "auth-verification-email",
2595
2762
  windowMs: 900 * 1e3,
2596
2763
  limit: 5,
2597
2764
  keyGenerator: (c) => {
@@ -2632,8 +2799,9 @@ var DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT = 3e3;
2632
2799
  * `enabled: false` rather than pay for it twice.
2633
2800
  */
2634
2801
  function createDataRateLimiter(config = {}) {
2635
- const { windowMs = 900 * 1e3, apiKey: apiKeyLimit = 1e3, user: userLimit = 1e3, anonymous: anonLimit = 300, store = new MemoryRateLimitStore(windowMs) } = config;
2802
+ const { windowMs = 900 * 1e3, store = new MemoryRateLimitStore(windowMs) } = config;
2636
2803
  const trustedProxyHops = resolveTrustedProxyHops(config.trustedProxyHops);
2804
+ const bucketFor = dataRateLimitBuckets(config);
2637
2805
  /**
2638
2806
  * Who this request is, for bucketing only.
2639
2807
  *
@@ -2664,25 +2832,81 @@ function createDataRateLimiter(config = {}) {
2664
2832
  if (!payload?.uid || isAnonymousUid(payload.uid)) return void 0;
2665
2833
  return payload.uid;
2666
2834
  };
2835
+ const bucketOf = async (c) => {
2836
+ if (c.get("user")?.uid === SERVICE_IDENTITY.uid) return null;
2837
+ const apiKey = c.get("apiKey");
2838
+ return bucketFor({
2839
+ apiKey,
2840
+ uid: apiKey ? void 0 : await identify(c),
2841
+ address: () => defaultKeyGenerator(c, trustedProxyHops)
2842
+ });
2843
+ };
2667
2844
  return createRateLimiter({
2668
2845
  windowMs,
2669
2846
  store,
2670
2847
  message: "Too many requests, please try again later.",
2671
- keyGenerator: async (c) => {
2672
- const key = c.get("apiKey");
2673
- if (key) return `api-key:${key.id}`;
2674
- const uid = await identify(c);
2675
- if (uid) return `user:${uid}`;
2676
- return `ip:${defaultKeyGenerator(c, trustedProxyHops)}`;
2677
- },
2678
- resolveLimit: async (c) => {
2679
- if (c.get("user")?.uid === SERVICE_IDENTITY.uid) return null;
2680
- const key = c.get("apiKey");
2681
- if (key) return key.rate_limit ?? apiKeyLimit;
2682
- return await identify(c) ? userLimit : anonLimit;
2683
- }
2848
+ keyGenerator: async (c) => (await bucketOf(c))?.key ?? `ip:${defaultKeyGenerator(c, trustedProxyHops)}`,
2849
+ resolveLimit: async (c) => (await bucketOf(c))?.limit ?? null
2684
2850
  });
2685
2851
  }
2852
+ /**
2853
+ * The data API's per-caller limit, as a check rather than a middleware — for
2854
+ * the door into the same data that is not an HTTP request.
2855
+ *
2856
+ * The realtime socket serves the rows the data API serves, one frame per
2857
+ * request. It counted them per connection only, so a caller bought more budget
2858
+ * by opening more sockets, and a frame never met the per-person limit its HTTP
2859
+ * twin does. Built from the same config and store as
2860
+ * {@link createDataRateLimiter}, it puts a caller in the same bucket under the
2861
+ * same key: one person's allowance, spent through either door.
2862
+ */
2863
+ function createDataRateLimitCheck(config = {}) {
2864
+ const { windowMs = 900 * 1e3, store = new MemoryRateLimitStore(windowMs) } = config;
2865
+ const trustedProxyHops = resolveTrustedProxyHops(config.trustedProxyHops);
2866
+ const bucketFor = dataRateLimitBuckets(config);
2867
+ return async (caller) => {
2868
+ if (caller.uid === SERVICE_IDENTITY.uid) return null;
2869
+ const bucket = bucketFor({
2870
+ uid: caller.uid !== void 0 && !isAnonymousUid(caller.uid) ? caller.uid : void 0,
2871
+ address: () => clientAddress(caller.header, () => caller.socketAddress, trustedProxyHops)
2872
+ });
2873
+ return bucket ? store.hit(bucket.key, windowMs, bucket.limit) : null;
2874
+ };
2875
+ }
2876
+ /**
2877
+ * Which bucket a data request counts in — the rule both doors share.
2878
+ *
2879
+ * Most specific first: an API key by its id, at its own `rate_limit`; a
2880
+ * signed-in user by their uid; anyone else by address at the anonymous
2881
+ * allowance, or not at all when that is `null`.
2882
+ *
2883
+ * The service key never reaches here: it is not a caller to bound. It is the
2884
+ * deployment's own credential and already reaches every row through
2885
+ * `SERVICE_IDENTITY`, so a limit on it protects nothing that is not already
2886
+ * open to whoever holds it — while reliably stopping the backfills,
2887
+ * migrations, imports and server-to-server jobs the key exists for, which
2888
+ * arrive as thousands of legitimate requests. A managed deployment has no
2889
+ * `rateLimit` surface to raise. Each door recognises it itself, where its
2890
+ * identity is known.
2891
+ */
2892
+ function dataRateLimitBuckets(config) {
2893
+ const { apiKey: apiKeyLimit = 1e3, user: userLimit = 1e3, anonymous: anonLimit = 300 } = config;
2894
+ return (who) => {
2895
+ if (who.apiKey) return {
2896
+ key: `api-key:${who.apiKey.id}`,
2897
+ limit: who.apiKey.rate_limit ?? apiKeyLimit
2898
+ };
2899
+ if (who.uid) return {
2900
+ key: `user:${who.uid}`,
2901
+ limit: userLimit
2902
+ };
2903
+ if (anonLimit === null) return null;
2904
+ return {
2905
+ key: `ip:${who.address()}`,
2906
+ limit: anonLimit
2907
+ };
2908
+ };
2909
+ }
2686
2910
  //#endregion
2687
2911
  //#region src/auth/captcha.ts
2688
2912
  /**
@@ -3226,7 +3450,8 @@ async function resolveStepUpPrincipal(c) {
3226
3450
  if (existing?.uid) return {
3227
3451
  uid: existing.uid,
3228
3452
  aal: existing.aal === "aal2" ? "aal2" : "aal1",
3229
- pending: false
3453
+ pending: false,
3454
+ iat: existing.iat
3230
3455
  };
3231
3456
  const token = extractBearerToken(c.req.header("authorization"));
3232
3457
  if (token === void 0) return null;
@@ -3234,13 +3459,15 @@ async function resolveStepUpPrincipal(c) {
3234
3459
  if (session) return {
3235
3460
  uid: session.uid,
3236
3461
  aal: session.aal === "aal2" ? "aal2" : "aal1",
3237
- pending: false
3462
+ pending: false,
3463
+ iat: session.iat
3238
3464
  };
3239
3465
  const pending = await verifyMfaPendingToken(token);
3240
3466
  if (pending) return {
3241
3467
  uid: pending.uid,
3242
3468
  aal: "aal1",
3243
- pending: true
3469
+ pending: true,
3470
+ iat: pending.iat
3244
3471
  };
3245
3472
  return null;
3246
3473
  }
@@ -3253,6 +3480,7 @@ async function resolveStepUpPrincipal(c) {
3253
3480
  * front of this one — they bound different things.
3254
3481
  */
3255
3482
  var mfaVerificationLimiter = createRateLimiter({
3483
+ name: "auth-mfa-verify",
3256
3484
  windowMs: 900 * 1e3,
3257
3485
  limit: MFA_VERIFICATION_ATTEMPTS_PER_WINDOW,
3258
3486
  keyGenerator: async (c) => `mfa-verify:${(await resolveStepUpPrincipal(c))?.uid ?? "unidentified"}`,
@@ -3263,6 +3491,26 @@ function mountMfaRoutes(opts) {
3263
3491
  const authRepo = config.authRepo;
3264
3492
  const emailConfig = config.emailConfig;
3265
3493
  /**
3494
+ * The caller of a challenge route, refused if the user has since ended
3495
+ * every session.
3496
+ *
3497
+ * These two routes resolve their own caller, because a sign-in that
3498
+ * answered `MFA_REQUIRED` has no session yet, and so they skipped the
3499
+ * revocation mark that `requireLiveSession` checks everywhere else. A
3500
+ * stolen access token that had enrolled its own factor (allowed at `aal1`
3501
+ * while the account had none) kept working here after the victim signed
3502
+ * out everywhere or changed their password, and the verify route then
3503
+ * minted it a new session dated after the mark, which the mark could not
3504
+ * reach. The pending token is a first factor accepted at its `iat`, so it
3505
+ * is held to the same mark.
3506
+ */
3507
+ async function resolveLiveStepUpPrincipal(c) {
3508
+ const principal = await resolveStepUpPrincipal(c);
3509
+ if (!principal) throw ApiError.unauthorized("Not authenticated");
3510
+ if (await isAccessTokenRevoked(authRepo, principal)) throw ApiError.unauthorized("Session has been revoked", "SESSION_REVOKED");
3511
+ return principal;
3512
+ }
3513
+ /**
3266
3514
  * Changing which factors an account trusts is itself a privileged act.
3267
3515
  *
3268
3516
  * An account that already has a verified factor may only gain or confirm
@@ -3359,8 +3607,7 @@ function mountMfaRoutes(opts) {
3359
3607
  * `MFA_REQUIRED`, or to step an existing session up to `aal2`.
3360
3608
  */
3361
3609
  router.post("/mfa/challenge", strictAuthLimiter, async (c) => {
3362
- const principal = await resolveStepUpPrincipal(c);
3363
- if (!principal) throw ApiError.unauthorized("Not authenticated");
3610
+ const principal = await resolveLiveStepUpPrincipal(c);
3364
3611
  const challengeSchema = z.object({ factorId: z.string().min(1, "Factor ID is required") });
3365
3612
  const { factorId } = parseBody(challengeSchema, await c.req.json());
3366
3613
  const factor = await authRepo.getMfaFactorById(factorId);
@@ -3380,8 +3627,7 @@ function mountMfaRoutes(opts) {
3380
3627
  * minted, at `aal2`.
3381
3628
  */
3382
3629
  router.post("/mfa/challenge/verify", strictAuthLimiter, mfaVerificationLimiter, async (c) => {
3383
- const principal = await resolveStepUpPrincipal(c);
3384
- if (!principal) throw ApiError.unauthorized("Not authenticated");
3630
+ const principal = await resolveLiveStepUpPrincipal(c);
3385
3631
  const challengeVerifySchema = z.object({
3386
3632
  challengeId: z.string().min(1, "Challenge ID is required"),
3387
3633
  code: z.string().min(1, "Code is required")
@@ -3391,7 +3637,8 @@ function mountMfaRoutes(opts) {
3391
3637
  if (!challenge) throw ApiError.badRequest("Invalid or expired challenge", "INVALID_CHALLENGE");
3392
3638
  const factor = await authRepo.getMfaFactorById(challenge.factorId);
3393
3639
  if (!factor || factor.uid !== principal.uid) throw ApiError.notFound("MFA factor not found");
3394
- if ((challenge.attempts ?? 0) >= MAX_CHALLENGE_ATTEMPTS) {
3640
+ const attempts = (challenge.attempts ?? 0) >= MAX_CHALLENGE_ATTEMPTS ? 6 : typeof authRepo.recordMfaChallengeAttempt === "function" ? await authRepo.recordMfaChallengeAttempt(challengeId) : void 0;
3641
+ if (attempts !== void 0 && attempts > MAX_CHALLENGE_ATTEMPTS) {
3395
3642
  logger.warn("[Security Audit] MFA challenge attempt limit reached", {
3396
3643
  eventType: "auth.mfa.challenge.exhausted",
3397
3644
  uid: principal.uid,
@@ -3406,7 +3653,6 @@ function mountMfaRoutes(opts) {
3406
3653
  isValid = await authRepo.useRecoveryCode(principal.uid, codeHash);
3407
3654
  }
3408
3655
  if (!isValid) {
3409
- const attempts = typeof authRepo.recordMfaChallengeAttempt === "function" ? await authRepo.recordMfaChallengeAttempt(challengeId) : void 0;
3410
3656
  logger.warn("[Security Audit] MFA verification failed", {
3411
3657
  eventType: "auth.mfa.verify.failure",
3412
3658
  uid: principal.uid,
@@ -3597,7 +3843,7 @@ function isAnonymousAuthOpen(policy) {
3597
3843
  //#endregion
3598
3844
  //#region src/auth/session-routes.ts
3599
3845
  function mountSessionRoutes(opts) {
3600
- const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook, requireLiveSession } = opts;
3846
+ const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook, requireLiveSession, registerCaptcha } = opts;
3601
3847
  const authRepo = config.authRepo;
3602
3848
  /**
3603
3849
  * Refuse the anonymous routes unless the deployment opted in.
@@ -3640,7 +3886,7 @@ function mountSessionRoutes(opts) {
3640
3886
  clearRefreshCookie(c, config.cookieAuth);
3641
3887
  const accessToken = extractBearerToken(c.req.header("authorization"));
3642
3888
  if (ops.afterLogout && accessToken !== void 0) {
3643
- const { verifyAccessToken } = await import("./jwt-C4OW-DNq.js").then((n) => n.h);
3889
+ const { verifyAccessToken } = await import("./jwt-DnQHNFCl.js").then((n) => n.h);
3644
3890
  const payload = await verifyAccessToken(accessToken);
3645
3891
  if (payload) ops.afterLogout(payload.uid).catch((err) => {
3646
3892
  logger.error("[AuthHooks] afterLogout error", { error: err instanceof Error ? err.message : err });
@@ -3791,14 +4037,21 @@ function mountSessionRoutes(opts) {
3791
4037
  logger.error("[AuthHooks] onAuthenticated error", { error: err instanceof Error ? err.message : err });
3792
4038
  });
3793
4039
  const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken, "anonymous");
3794
- const finalResponse = await applyTransformHook(authResponse, "anonymous", c.req.raw, user.id);
3795
- return c.json(finalResponse, 201);
4040
+ const transformedResponse = await applyTransformHook(authResponse, "anonymous", c.req.raw, user.id);
4041
+ return c.json(redactRefreshToken(transformedResponse, c, refreshToken, config.cookieAuth), 201);
3796
4042
  });
3797
4043
  /**
3798
4044
  * POST /auth/anonymous/link
3799
4045
  * Upgrade an anonymous user to a permanent account with email/password
4046
+ *
4047
+ * This is registration: before it the row is a guest, after it an account
4048
+ * that signs in with a password. So it answers to registration's controls,
4049
+ * the `register` captcha and `beforeUserCreate` on the credentials the
4050
+ * account is getting. `/anonymous` carries neither, deliberately, because
4051
+ * apps mint guests on page load; the two in a row used to be a way to make
4052
+ * an account that passed neither.
3800
4053
  */
3801
- router.post("/anonymous/link", strictAuthLimiter, requireLiveSession, async (c) => {
4054
+ router.post("/anonymous/link", strictAuthLimiter, requireLiveSession, ...registerCaptcha ? [registerCaptcha] : [], async (c) => {
3802
4055
  assertAnonymousAuthOpen();
3803
4056
  const userCtx = c.get("user");
3804
4057
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
@@ -3809,16 +4062,105 @@ function mountSessionRoutes(opts) {
3809
4062
  if (!passwordValidation.valid) throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
3810
4063
  if (await authRepo.getUserByEmail(normalizeEmail(email))) throw ApiError.conflict("Email already registered", "EMAIL_EXISTS");
3811
4064
  const passwordHash = await ops.hashPassword(password);
3812
- const updatedUser = await authRepo.updateUser(user.id, {
4065
+ let linkData = {
3813
4066
  email: normalizeEmail(email),
3814
- passwordHash,
4067
+ passwordHash
4068
+ };
4069
+ if (ops.beforeUserCreate) linkData = await ops.beforeUserCreate(linkData);
4070
+ const updatedUser = await authRepo.updateUser(user.id, {
4071
+ ...linkData,
4072
+ email: normalizeEmail(linkData.email),
4073
+ passwordHash: linkData.passwordHash,
3815
4074
  isAnonymous: false
3816
4075
  });
3817
4076
  if (!updatedUser) throw ApiError.notFound("User not found");
3818
4077
  const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(user.id, c.req.header("user-agent") || "unknown", c.req.header("x-forwarded-for") || "unknown");
3819
4078
  const authResponse = buildAuthResponse(updatedUser, roleIds, accessToken, refreshToken, "password");
3820
- const finalResponse = await applyTransformHook(authResponse, "anonymous", c.req.raw, user.id);
3821
- return c.json(finalResponse);
4079
+ const transformedResponse = await applyTransformHook(authResponse, "anonymous", c.req.raw, user.id);
4080
+ return c.json(redactRefreshToken(transformedResponse, c, refreshToken, config.cookieAuth));
4081
+ });
4082
+ }
4083
+ //#endregion
4084
+ //#region src/auth/address-ownership.ts
4085
+ /**
4086
+ * What happens when the owner of an address proves it for the first time.
4087
+ *
4088
+ * Nothing verifies the address an account is made with: `POST /auth/register`
4089
+ * takes whatever it is given, and so does a sign-in through a provider that
4090
+ * does not vouch for the address (Spotify and Facebook never do; Discord and
4091
+ * multi-tenant Microsoft sometimes do not). So anyone can make an account for
4092
+ * someone else's address and leave a way in on it — a password, or the
4093
+ * provider identity they signed up with — then wait for the owner.
4094
+ *
4095
+ * The owner turns up by proving the address: a magic link, an email code or a
4096
+ * password reset, each read out of their inbox. That proof used to mark the
4097
+ * account verified and leave the attacker's way in where it was, so the owner
4098
+ * and the attacker shared the account from then on. Now the proof also removes
4099
+ * everything on the account that nobody proved: the password, every identity
4100
+ * whose provider did not vouch for this address, and every session.
4101
+ *
4102
+ * @module
4103
+ */
4104
+ /**
4105
+ * The profile stored with a linked OAuth identity.
4106
+ *
4107
+ * `emailVerified` is recorded so that a later proof of the address can tell an
4108
+ * identity whose provider vouched for it from one whose provider did not.
4109
+ */
4110
+ function identityProfileData(profile) {
4111
+ return {
4112
+ email: profile.email,
4113
+ emailVerified: profile.emailVerified === true
4114
+ };
4115
+ }
4116
+ /**
4117
+ * Did this identity's provider vouch for `email` when it was linked?
4118
+ *
4119
+ * Only whoever controls the inbox can hold a provider account that vouches for
4120
+ * the address, so such an identity is the owner's. An identity linked before
4121
+ * the flag was recorded reads as not vouching: it is detached, and if its
4122
+ * provider does vouch, the next sign-in with it links it again.
4123
+ */
4124
+ function identityVouchesForAddress(identity, email) {
4125
+ const profile = identity.profileData;
4126
+ return profile?.emailVerified === true && typeof profile.email === "string" && normalizeEmail(profile.email) === normalizeEmail(email);
4127
+ }
4128
+ /**
4129
+ * Settle an account whose address its owner has just proven, and mark it
4130
+ * verified.
4131
+ *
4132
+ * Call it only for an account that is not yet verified, from a route that has
4133
+ * just consumed something mailed to the address. It detaches every identity
4134
+ * whose provider did not vouch for the address, sets the password to
4135
+ * `passwordHash` — the one the owner just chose on a reset, or `null` to remove
4136
+ * the one somebody else may have registered with — and ends every session, in
4137
+ * that order and before the account is marked verified.
4138
+ *
4139
+ * A repository that cannot detach an identity makes this refuse with 409 when
4140
+ * there is one to detach. Marking the account verified with the identity still
4141
+ * on it is the takeover itself, and leaving the account unverified while
4142
+ * signing the owner in would share the account all the same.
4143
+ */
4144
+ async function confirmAddressOwnership(authRepo, user, passwordHash) {
4145
+ const unproven = (await authRepo.getUserIdentities(user.id)).filter((identity) => !identityVouchesForAddress(identity, user.email));
4146
+ if (unproven.length > 0) {
4147
+ if (typeof authRepo.unlinkUserIdentity !== "function") {
4148
+ logger.error("[Security Audit] Proof of address refused: the account has sign-in methods nobody proved, and this auth repository cannot remove them", {
4149
+ eventType: "auth.address_proof.refused",
4150
+ uid: user.id,
4151
+ providers: unproven.map((identity) => identity.provider)
4152
+ });
4153
+ throw ApiError.conflict("This account has sign-in methods that were added before its email address was verified, and this backend cannot remove them. Ask an administrator to review the account.", "UNVERIFIED_IDENTITIES");
4154
+ }
4155
+ for (const identity of unproven) await authRepo.unlinkUserIdentity(user.id, identity.provider, identity.providerId);
4156
+ }
4157
+ await replaceUserPassword(authRepo, user.id, passwordHash);
4158
+ await authRepo.setEmailVerified(user.id, true);
4159
+ if (unproven.length > 0 || user.passwordHash) logger.info("[Security Audit] First proof of address removed sign-in methods nobody proved", {
4160
+ eventType: "auth.address_proof.cleared",
4161
+ uid: user.id,
4162
+ removedProviders: unproven.map((identity) => identity.provider),
4163
+ replacedPassword: Boolean(user.passwordHash)
3822
4164
  });
3823
4165
  }
3824
4166
  //#endregion
@@ -3845,13 +4187,22 @@ function mountMagicLinkRoutes(deps) {
3845
4187
  /**
3846
4188
  * POST /auth/magic-link
3847
4189
  * Request a magic link email
4190
+ *
4191
+ * Answers the same words whether or not the address has an account, and
4192
+ * is held to the same three things as its siblings `/otp` and
4193
+ * `/forgot-password` so that nothing else answers the question either:
4194
+ * the same response time, a per-address send limit, and `beforeLogin` on
4195
+ * every attempt. This route had none of the three — the SMTP send was
4196
+ * awaited only for a real account, and the only limit was per IP — so its
4197
+ * timing named the customers and a spread-out run could fill one inbox.
3848
4198
  */
3849
- router.post("/magic-link", strictAuthLimiter, ...captchaMiddleware ? [captchaMiddleware] : [], async (c) => {
4199
+ router.post("/magic-link", strictAuthLimiter, captureRecipientEmail, recipientEmailLimiter, ...captchaMiddleware ? [captchaMiddleware] : [], async (c) => {
4200
+ const startedAt = Date.now();
3850
4201
  const { email } = parseBody(magicLinkSchema, await c.req.json());
3851
4202
  if (!isEmailConfigured()) throw ApiError.serviceUnavailable("Email service not configured. Magic link login is not available.", "EMAIL_NOT_CONFIGURED");
3852
4203
  const user = await authRepo.getUserByEmail(email);
4204
+ if (ops.beforeLogin) await ops.beforeLogin(email, "magic-link");
3853
4205
  if (user) {
3854
- if (ops.beforeLogin) await ops.beforeLogin(email, "magic-link");
3855
4206
  const token = generateSecureToken();
3856
4207
  const tokenHash = hashToken$1(token);
3857
4208
  const expiresAt = getMagicLinkExpiry();
@@ -3866,21 +4217,19 @@ function mountMagicLinkRoutes(deps) {
3866
4217
  email: user.email,
3867
4218
  displayName: user.displayName
3868
4219
  }, appName, logoUrl);
3869
- try {
3870
- await emailService.send({
3871
- to: user.email,
3872
- subject: emailContent.subject,
3873
- html: emailContent.html,
3874
- text: emailContent.text
3875
- });
3876
- } catch (emailError) {
4220
+ emailService.send({
4221
+ to: user.email,
4222
+ subject: emailContent.subject,
4223
+ html: emailContent.html,
4224
+ text: emailContent.text
4225
+ }).catch((emailError) => {
3877
4226
  logger.error("Failed to send magic link email", { error: emailError instanceof Error ? emailError.message : emailError });
3878
- }
4227
+ });
3879
4228
  }
3880
- return c.json({
4229
+ return notBefore$1(startedAt, 400, c.json({
3881
4230
  success: true,
3882
4231
  message: "If an account with that email exists, a magic link has been sent."
3883
- });
4232
+ }));
3884
4233
  });
3885
4234
  /**
3886
4235
  * POST /auth/magic-link/verify
@@ -3892,19 +4241,23 @@ function mountMagicLinkRoutes(deps) {
3892
4241
  const storedToken = await authRepo.findValidMagicLinkToken(tokenHash);
3893
4242
  if (!storedToken) throw ApiError.badRequest("Invalid or expired magic link", "INVALID_TOKEN");
3894
4243
  await authRepo.markMagicLinkTokenUsed(tokenHash);
3895
- const user = await authRepo.getUserById(storedToken.uid);
4244
+ let user = await authRepo.getUserById(storedToken.uid);
3896
4245
  if (!user) throw ApiError.badRequest("Invalid or expired magic link", "INVALID_TOKEN");
3897
4246
  if (!user.emailVerified) {
3898
- await authRepo.setEmailVerified(user.id, true);
3899
- user.emailVerified = true;
4247
+ await confirmAddressOwnership(authRepo, user, null);
4248
+ user = {
4249
+ ...user,
4250
+ emailVerified: true,
4251
+ passwordHash: null
4252
+ };
3900
4253
  }
3901
4254
  const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(user.id, c.req.header("user-agent") || "unknown", c.req.header("x-forwarded-for") || "unknown");
3902
4255
  if (ops.onAuthenticated) ops.onAuthenticated(user, "magic-link").catch((err) => {
3903
4256
  logger.error("[AuthHooks] onAuthenticated error", { error: err instanceof Error ? err.message : err });
3904
4257
  });
3905
4258
  const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken, "magic-link");
3906
- const finalResponse = await applyTransformHook(authResponse, "magic-link", c.req.raw, user.id);
3907
- return c.json(finalResponse);
4259
+ const transformedResponse = await applyTransformHook(authResponse, "magic-link", c.req.raw, user.id);
4260
+ return c.json(redactRefreshToken(transformedResponse, c, refreshToken, config.cookieAuth));
3908
4261
  });
3909
4262
  }
3910
4263
  //#endregion
@@ -3949,6 +4302,7 @@ function otpTokenMaterial(email, code) {
3949
4302
  * replicas with the default, the effective budget is five attempts per replica.
3950
4303
  */
3951
4304
  var otpVerificationLimiter = createRateLimiter({
4305
+ name: "auth-otp-verify",
3952
4306
  windowMs: 900 * 1e3,
3953
4307
  limit: OTP_ATTEMPTS_PER_WINDOW,
3954
4308
  keyGenerator: (c) => {
@@ -3969,6 +4323,7 @@ var otpVerificationLimiter = createRateLimiter({
3969
4323
  * and useless as a mail bomb, matching `verificationEmailLimiter`.
3970
4324
  */
3971
4325
  var otpSendLimiter = createRateLimiter({
4326
+ name: "auth-otp-send",
3972
4327
  windowMs: 900 * 1e3,
3973
4328
  limit: 5,
3974
4329
  keyGenerator: (c) => {
@@ -4077,19 +4432,23 @@ function mountOtpRoutes(deps) {
4077
4432
  const storedToken = await authRepo.findValidMagicLinkToken(tokenHash);
4078
4433
  if (!storedToken) throw ApiError.badRequest("Invalid or expired code", "INVALID_CODE");
4079
4434
  await authRepo.markMagicLinkTokenUsed(tokenHash);
4080
- const user = await authRepo.getUserById(storedToken.uid);
4435
+ let user = await authRepo.getUserById(storedToken.uid);
4081
4436
  if (!user) throw ApiError.badRequest("Invalid or expired code", "INVALID_CODE");
4082
4437
  if (!user.emailVerified) {
4083
- await authRepo.setEmailVerified(user.id, true);
4084
- user.emailVerified = true;
4438
+ await confirmAddressOwnership(authRepo, user, null);
4439
+ user = {
4440
+ ...user,
4441
+ emailVerified: true,
4442
+ passwordHash: null
4443
+ };
4085
4444
  }
4086
4445
  const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(user.id, c.req.header("user-agent") || "unknown", c.req.header("x-forwarded-for") || "unknown");
4087
4446
  if (ops.onAuthenticated) ops.onAuthenticated(user, "otp").catch((err) => {
4088
4447
  logger.error("[AuthHooks] onAuthenticated error", { error: err instanceof Error ? err.message : err });
4089
4448
  });
4090
4449
  const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken, "otp");
4091
- const finalResponse = await applyTransformHook(authResponse, "otp", c.req.raw, user.id);
4092
- return c.json(finalResponse);
4450
+ const transformedResponse = await applyTransformHook(authResponse, "otp", c.req.raw, user.id);
4451
+ return c.json(redactRefreshToken(transformedResponse, c, refreshToken, config.cookieAuth));
4093
4452
  });
4094
4453
  }
4095
4454
  //#endregion
@@ -4097,26 +4456,34 @@ function mountOtpRoutes(deps) {
4097
4456
  /**
4098
4457
  * May an OAuth identity be attached to a pre-existing account found *by email*?
4099
4458
  *
4100
- * Both sides have to be trustworthy, and only one of them used to be checked:
4459
+ * Both sides have to be trustworthy:
4101
4460
  *
4102
4461
  * - the **provider** must have verified the address, or the caller has not
4103
4462
  * shown they control it;
4104
- * - the **local account** must itself be trustworthy — either its address was
4105
- * verified, or it has no password at all (it was created by an OAuth
4106
- * sign-in or an invitation, so there is no credential an attacker could
4107
- * have planted in advance).
4108
- *
4109
- * A refusal is not a dead end: `POST /auth/link/<provider>` attaches the
4110
- * identity once the caller proves ownership by holding a session.
4463
+ * - the **local account**'s address must have been verified too. Whoever
4464
+ * made an unverified account never proved the address, and every way in
4465
+ * they left on it — a password, an identity from a provider that did not
4466
+ * vouch for the address — would go on opening the account the owner is
4467
+ * about to be signed into.
4468
+ *
4469
+ * The second rule used to exempt an account with no password, on the theory
4470
+ * that one made by an OAuth sign-in holds no credential an attacker could have
4471
+ * planted. It holds exactly one: the identity it was made with, which is an
4472
+ * attacker's whenever the provider did not vouch for the address.
4473
+ *
4474
+ * A refusal is not a dead end. `POST /auth/link/<provider>` attaches the
4475
+ * identity once the caller proves ownership by holding a session, and proving
4476
+ * the address (a magic link, an email code, a password reset) verifies the
4477
+ * account — removing what nobody proved — after which this answers yes.
4111
4478
  */
4112
4479
  function decideOAuthAutoLink(args) {
4113
4480
  if (args.providerEmailVerified !== true) return {
4114
4481
  allowed: false,
4115
4482
  reason: "provider-email-unverified"
4116
4483
  };
4117
- if (Boolean(args.existingUser.passwordHash) && !args.existingUser.emailVerified) return {
4484
+ if (!args.existingUser.emailVerified) return {
4118
4485
  allowed: false,
4119
- reason: "local-account-unverified"
4486
+ reason: args.existingUser.passwordHash ? "local-account-unverified" : "local-account-unverified-passwordless"
4120
4487
  };
4121
4488
  return { allowed: true };
4122
4489
  }
@@ -4292,6 +4659,18 @@ function createAuthRoutes(config) {
4292
4659
  });
4293
4660
  }
4294
4661
  /**
4662
+ * Is the account just created the only one there is?
4663
+ *
4664
+ * Two rows at most: the total says whether anyone else exists, and the
4665
+ * one row left when nobody does says whether it is this account. It used
4666
+ * to be `listUsers()` — the whole users table, password hashes included,
4667
+ * on every sign-up, to compare its length with one.
4668
+ */
4669
+ async function isOnlyUser(uid) {
4670
+ const { users, total } = await authRepo.listUsersPaginated({ limit: 2 });
4671
+ return total === 1 && users[0]?.id === uid;
4672
+ }
4673
+ /**
4295
4674
  * Helper to generate and store session tokens.
4296
4675
  *
4297
4676
  * Every route that signs somebody in comes through here — password login,
@@ -4325,7 +4704,7 @@ function createAuthRoutes(config) {
4325
4704
  const accessToken = await generateAccessToken(uid, roleIds, aal, customClaims, isAnonymous);
4326
4705
  const refreshToken = generateRefreshToken();
4327
4706
  await authRepo.createRefreshToken(uid, await hashRefreshToken(refreshToken), getRefreshTokenExpiry(), userAgent, ipAddress, {
4328
- id: randomUUID(),
4707
+ id: randomUUID$1(),
4329
4708
  startedAt: /* @__PURE__ */ new Date(),
4330
4709
  aal
4331
4710
  });
@@ -4360,8 +4739,7 @@ function createAuthRoutes(config) {
4360
4739
  };
4361
4740
  if (ops.beforeUserCreate) createData = await ops.beforeUserCreate(createData);
4362
4741
  const user = await authRepo.createUser(createData);
4363
- const existingUsers = await authRepo.listUsers();
4364
- const isFirstUser = existingUsers.length === 1 && existingUsers[0].id === user.id;
4742
+ const isFirstUser = await isOnlyUser(user.id);
4365
4743
  if (bootstrapRegistration && !isFirstUser) {
4366
4744
  await authRepo.deleteUser(user.id);
4367
4745
  throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
@@ -4424,21 +4802,46 @@ function createAuthRoutes(config) {
4424
4802
  return c.json(finalResponse);
4425
4803
  });
4426
4804
  /**
4805
+ * Turn an OAuth request body into the provider's profile of the caller.
4806
+ *
4807
+ * Both OAuth doors come through here — sign-in and `/link/<provider>` —
4808
+ * because both end with that provider identity resolving to an account,
4809
+ * so both need the same two controls. The link door used to spend the
4810
+ * code without the first and echo the provider's error without the second:
4811
+ * a victim's code leaked through a stale registered URI could be linked to
4812
+ * the attacker's account, after which the victim's own "Sign in with …"
4813
+ * landed there.
4814
+ *
4815
+ * The redirect URI is allowlisted before the code is spent. The provider's
4816
+ * own registered-URI match authorises *every* URI on that OAuth client — a
4817
+ * leftover localhost entry, a staging host, a second product sharing the
4818
+ * client id — and any of them can mint a code this backend would otherwise
4819
+ * accept.
4820
+ *
4821
+ * A provider's error message is logged, never returned: its token-endpoint
4822
+ * error body routinely echoes the client_id, the redirect URI and
4823
+ * diagnostics about the credential state.
4824
+ */
4825
+ async function verifyProviderPayload(provider, payload) {
4826
+ const requestedRedirectUri = typeof payload === "object" && payload !== null && "redirectUri" in payload ? payload.redirectUri : void 0;
4827
+ if (typeof requestedRedirectUri === "string" && !isRedirectUriAllowed(requestedRedirectUri, config.allowedRedirectUris)) throw ApiError.badRequest("redirectUri is not allowed for this backend", "REDIRECT_URI_NOT_ALLOWED");
4828
+ let externalUser;
4829
+ try {
4830
+ externalUser = await provider.verify(payload);
4831
+ } catch (err) {
4832
+ logger.error(`[OAuth] ${provider.id} verification threw`, { error: err instanceof Error ? err.message : String(err) });
4833
+ throw ApiError.unauthorized(`Invalid ${provider.id} credentials`, "OAUTH_ERROR");
4834
+ }
4835
+ if (!externalUser) throw ApiError.unauthorized(`Invalid ${provider.id} credentials`, "INVALID_TOKEN");
4836
+ return externalUser;
4837
+ }
4838
+ /**
4427
4839
  * Dynamically mount OAuth provider routes
4428
4840
  */
4429
4841
  if (config.oauthProviders && config.oauthProviders.length > 0) for (const provider of config.oauthProviders) {
4430
4842
  router.post(`/${provider.id}`, defaultAuthLimiter, async (c) => {
4431
4843
  const payload = parseBody(provider.schema, await c.req.json());
4432
- const requestedRedirectUri = payload.redirectUri;
4433
- if (typeof requestedRedirectUri === "string" && !isRedirectUriAllowed(requestedRedirectUri, config.allowedRedirectUris)) throw ApiError.badRequest("redirectUri is not allowed for this backend", "REDIRECT_URI_NOT_ALLOWED");
4434
- let externalUser;
4435
- try {
4436
- externalUser = await provider.verify(payload);
4437
- } catch (err) {
4438
- logger.error(`[OAuth] ${provider.id} verification threw`, { error: err instanceof Error ? err.message : String(err) });
4439
- throw ApiError.unauthorized(`Invalid ${provider.id} credentials`, "OAUTH_ERROR");
4440
- }
4441
- if (!externalUser) throw ApiError.unauthorized(`Invalid ${provider.id} credentials`, "INVALID_TOKEN");
4844
+ const externalUser = await verifyProviderPayload(provider, payload);
4442
4845
  let user = await authRepo.getUserByIdentity(provider.id, externalUser.providerId);
4443
4846
  if (!user) {
4444
4847
  user = await authRepo.getUserByEmail(externalUser.email);
@@ -4451,7 +4854,7 @@ function createAuthRoutes(config) {
4451
4854
  reason: decision.reason,
4452
4855
  provider: provider.id
4453
4856
  });
4454
- await authRepo.linkUserIdentity(user.id, provider.id, externalUser.providerId, { email: externalUser.email });
4857
+ await authRepo.linkUserIdentity(user.id, provider.id, externalUser.providerId, identityProfileData(externalUser));
4455
4858
  await authRepo.updateUser(user.id, {
4456
4859
  displayName: user.displayName || externalUser.displayName || void 0,
4457
4860
  photoUrl: user.photoUrl || externalUser.photoUrl || void 0
@@ -4472,14 +4875,13 @@ function createAuthRoutes(config) {
4472
4875
  photoUrl: externalUser.photoUrl || void 0,
4473
4876
  emailVerified: externalUser.emailVerified === true
4474
4877
  });
4475
- await authRepo.linkUserIdentity(user.id, provider.id, externalUser.providerId, { email: externalUser.email });
4878
+ await authRepo.linkUserIdentity(user.id, provider.id, externalUser.providerId, identityProfileData(externalUser));
4476
4879
  if (ops.afterUserCreate) try {
4477
4880
  await ops.afterUserCreate(user);
4478
4881
  } catch (err) {
4479
4882
  logger.error("[AuthHooks] afterUserCreate error", { error: err instanceof Error ? err.message : err });
4480
4883
  }
4481
- const allUsers = await authRepo.listUsers();
4482
- const isFirstUser = allUsers.length === 1 && allUsers[0].id === user.id;
4884
+ const isFirstUser = await isOnlyUser(user.id);
4483
4885
  if (bootstrapRegistration && !isFirstUser) {
4484
4886
  await authRepo.deleteUser(user.id);
4485
4887
  throw ApiError.forbidden(noSignupsMessage, "REGISTRATION_DISABLED");
@@ -4522,26 +4924,21 @@ function createAuthRoutes(config) {
4522
4924
  const userCtx = c.get("user");
4523
4925
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
4524
4926
  const payload = parseBody(provider.schema, await c.req.json());
4525
- let externalUser;
4526
- try {
4527
- externalUser = await provider.verify(payload);
4528
- } catch (err) {
4529
- const msg = err instanceof Error ? err.message : String(err);
4530
- throw ApiError.unauthorized(`${provider.id} link failed: ${msg}`, "OAUTH_ERROR");
4531
- }
4532
- if (!externalUser) throw ApiError.unauthorized(`Invalid ${provider.id} credentials`, "INVALID_TOKEN");
4927
+ const externalUser = await verifyProviderPayload(provider, payload);
4533
4928
  const identityOwner = await authRepo.getUserByIdentity(provider.id, externalUser.providerId);
4534
4929
  if (identityOwner && identityOwner.id !== userCtx.uid) throw ApiError.conflict(`That ${provider.id} account is already linked to a different user.`, "IDENTITY_ALREADY_LINKED");
4535
4930
  if (identityOwner) return c.json({
4536
4931
  success: true,
4537
4932
  provider: provider.id,
4538
- alreadyLinked: true
4933
+ alreadyLinked: true,
4934
+ photoURL: externalUser.photoUrl ?? null
4539
4935
  });
4540
- await authRepo.linkUserIdentity(userCtx.uid, provider.id, externalUser.providerId, { email: externalUser.email });
4936
+ await authRepo.linkUserIdentity(userCtx.uid, provider.id, externalUser.providerId, identityProfileData(externalUser));
4541
4937
  return c.json({
4542
4938
  success: true,
4543
4939
  provider: provider.id,
4544
- alreadyLinked: false
4940
+ alreadyLinked: false,
4941
+ photoURL: externalUser.photoUrl ?? null
4545
4942
  });
4546
4943
  });
4547
4944
  }
@@ -4595,7 +4992,9 @@ function createAuthRoutes(config) {
4595
4992
  const storedToken = await authRepo.findValidPasswordResetToken(tokenHash);
4596
4993
  if (!storedToken) throw ApiError.badRequest("Invalid or expired reset token", "INVALID_TOKEN");
4597
4994
  const passwordHash = await ops.hashPassword(password);
4598
- await replaceUserPassword(authRepo, storedToken.uid, passwordHash);
4995
+ const account = await authRepo.getUserById(storedToken.uid);
4996
+ if (account && !account.emailVerified) await confirmAddressOwnership(authRepo, account, passwordHash);
4997
+ else await replaceUserPassword(authRepo, storedToken.uid, passwordHash);
4599
4998
  await authRepo.markPasswordResetTokenUsed(tokenHash);
4600
4999
  if (ops.onPasswordReset) ops.onPasswordReset(storedToken.uid).catch((err) => {
4601
5000
  logger.error("[AuthHooks] onPasswordReset error", { error: err instanceof Error ? err.message : err });
@@ -4748,7 +5147,14 @@ function createAuthRoutes(config) {
4748
5147
  };
4749
5148
  if (!supersededAt) if (authRepo.markRefreshTokenRotated) await authRepo.markRefreshTokenRotated(tokenHash);
4750
5149
  else await authRepo.deleteRefreshToken(tokenHash);
4751
- await authRepo.createRefreshToken(storedToken.uid, await hashRefreshToken(newRefreshToken), getRefreshTokenExpiry(), userAgent, ipAddress, session);
5150
+ const presentedKept = Boolean(supersededAt) || typeof authRepo.markRefreshTokenRotated === "function";
5151
+ if (await authRepo.createRefreshToken(storedToken.uid, await hashRefreshToken(newRefreshToken), getRefreshTokenExpiry(), userAgent, ipAddress, presentedKept ? {
5152
+ ...session,
5153
+ rotatedFrom: tokenHash
5154
+ } : session) === false) {
5155
+ clearRefreshCookie(c, config.cookieAuth);
5156
+ throw ApiError.unauthorized("Session has been revoked", "SESSION_REVOKED");
5157
+ }
4752
5158
  authRepo.pruneRefreshTokens?.(storedToken.uid, session.id, new Date(Date.now() - reuseWindowMs)).catch((err) => {
4753
5159
  logger.warn("[Auth] Refresh token prune failed", { error: err instanceof Error ? err.message : String(err) });
4754
5160
  });
@@ -4775,7 +5181,8 @@ function createAuthRoutes(config) {
4775
5181
  buildAuthResponse,
4776
5182
  createSessionAndTokens,
4777
5183
  applyTransformHook,
4778
- requireLiveSession
5184
+ requireLiveSession,
5185
+ registerCaptcha: captcha.register
4779
5186
  });
4780
5187
  mountMfaRoutes({
4781
5188
  router,
@@ -4910,7 +5317,7 @@ function createResetPasswordRoute(config) {
4910
5317
  const expiresAt = new Date(Date.now() + 3600 * 1e3);
4911
5318
  await authRepo.createPasswordResetToken(existing.id, tokenHash, expiresAt);
4912
5319
  try {
4913
- const setPasswordUrl = `${emailConfig?.resetPasswordUrl || ""}/reset-password?token=${token}`;
5320
+ const setPasswordUrl = `${resolveEmailLinkBase(emailConfig, "resetPassword")}/reset-password?token=${token}`;
4914
5321
  const { appName, logoUrl } = resolveEmailBranding(emailConfig);
4915
5322
  const templateFn = emailConfig?.templates?.passwordReset;
4916
5323
  const emailContent = templateFn ? templateFn(setPasswordUrl, {
@@ -5032,15 +5439,18 @@ function createAdminUsersRoute(config) {
5032
5439
  }
5033
5440
  const uid = "uid" in user ? user.uid : void 0;
5034
5441
  if (!uid) throw ApiError.unauthorized("User ID not found in auth context");
5035
- if (isAnonymousUid(uid)) {
5442
+ const refuseAnonymousCaller = () => {
5036
5443
  logger.warn("[Security Audit] Bootstrap denied: anonymous caller", {
5037
5444
  eventType: "auth.bootstrap.denied.anonymous",
5038
5445
  callerId: uid
5039
5446
  });
5040
5447
  throw ApiError.forbidden("An anonymous session cannot claim the initial admin role. Register a real account and bootstrap from it, or assign the admin role using the service key.", "BOOTSTRAP_ANONYMOUS");
5041
- }
5042
- if (!await authRepo.getUserById(uid)) throw ApiError.notFound("Authenticated user does not exist in the database.", "USER_NOT_FOUND");
5043
- const registeredUsers = users.filter((u) => !isAnonymousUid(u.id));
5448
+ };
5449
+ if (isAnonymousUid(uid)) refuseAnonymousCaller();
5450
+ const caller = await authRepo.getUserById(uid);
5451
+ if (!caller) throw ApiError.notFound("Authenticated user does not exist in the database.", "USER_NOT_FOUND");
5452
+ if (caller.isAnonymous === true) refuseAnonymousCaller();
5453
+ const registeredUsers = users.filter((u) => u.isAnonymous !== true && !isAnonymousUid(u.id));
5044
5454
  if (registeredUsers.length > 0) {
5045
5455
  const earliest = registeredUsers.reduce((a, b) => {
5046
5456
  const at = new Date(a.createdAt).getTime();
@@ -5135,7 +5545,8 @@ function createAdminUsersRoute(config) {
5135
5545
  passwordHash: prepResult.values.passwordHash,
5136
5546
  displayName: prepResult.values.displayName,
5137
5547
  photoUrl: prepResult.values.photoUrl,
5138
- metadata: prepResult.values.metadata
5548
+ metadata: prepResult.values.metadata,
5549
+ emailVerified: prepResult.values.emailVerified === true
5139
5550
  };
5140
5551
  if (ops.beforeUserCreate) createData = await ops.beforeUserCreate(createData);
5141
5552
  const user = await authRepo.createUser(createData);
@@ -5160,8 +5571,12 @@ function createAdminUsersRoute(config) {
5160
5571
  const uid = c.req.param("uid");
5161
5572
  const { password, email, displayName, roles } = await c.req.json();
5162
5573
  if (!await authRepo.getUserById(uid)) throw ApiError.notFound("User not found");
5574
+ if (roles !== void 0 && Array.isArray(roles)) await assertRoleChangesAllowed(authRepo, [{
5575
+ uid,
5576
+ roles
5577
+ }]);
5163
5578
  const updates = {};
5164
- if (email !== void 0) updates.email = normalizeEmail(email);
5579
+ if (email !== void 0) updates.email = await normalizeUserEmailChange(authRepo, uid, email);
5165
5580
  if (displayName !== void 0) updates.displayName = displayName;
5166
5581
  let passwordHash;
5167
5582
  if (password) {
@@ -5171,17 +5586,7 @@ function createAdminUsersRoute(config) {
5171
5586
  }
5172
5587
  if (Object.keys(updates).length > 0) await authRepo.updateUser(uid, updates);
5173
5588
  if (passwordHash) await replaceUserPassword(authRepo, uid, passwordHash);
5174
- if (roles !== void 0 && Array.isArray(roles)) {
5175
- const wasAdmin = (await authRepo.getUserRoleIds(uid)).includes("admin");
5176
- const willBeAdmin = roles.includes("admin");
5177
- if (wasAdmin && !willBeAdmin) {
5178
- if ((await authRepo.listUsersPaginated({
5179
- roleId: "admin",
5180
- limit: 1
5181
- })).total <= 1) throw ApiError.forbidden("Cannot demote the last administrator", "LAST_ADMIN");
5182
- }
5183
- await authRepo.setUserRoles(uid, roles);
5184
- }
5589
+ if (roles !== void 0 && Array.isArray(roles)) await authRepo.setUserRoles(uid, roles);
5185
5590
  const result = await authRepo.getUserWithRoles(uid);
5186
5591
  const adminUser = toAdminUser(result.user, result.roles.map((r) => r.id));
5187
5592
  return c.json({ user: adminUser });
@@ -5190,17 +5595,11 @@ function createAdminUsersRoute(config) {
5190
5595
  const uid = c.req.param("uid");
5191
5596
  if (c.get("user")?.uid === uid) throw ApiError.badRequest("Cannot delete your own account", "SELF_DELETE");
5192
5597
  if (!await authRepo.getUserById(uid)) throw ApiError.notFound("User not found");
5193
- if ((await authRepo.getUserRoleIds(uid)).includes("admin")) {
5194
- if ((await authRepo.listUsersPaginated({
5195
- roleId: "admin",
5196
- limit: 1
5197
- })).total <= 1) throw ApiError.forbidden("Cannot delete the last administrator", "LAST_ADMIN");
5198
- }
5598
+ await assertUserDeletionsAllowed(authRepo, [uid]);
5199
5599
  if (ops.beforeUserDelete) await ops.beforeUserDelete(uid);
5600
+ await authRepo.deleteAllRefreshTokensForUser(uid);
5200
5601
  await authRepo.deleteUser(uid);
5201
- if (ops.afterUserDelete) ops.afterUserDelete(uid).catch((err) => {
5202
- logger.error("[AuthHooks] afterUserDelete error", { error: err instanceof Error ? err.message : err });
5203
- });
5602
+ runAfterUserDelete(ops, uid);
5204
5603
  return c.json({ success: true });
5205
5604
  });
5206
5605
  return router;
@@ -5217,6 +5616,29 @@ function createAdminUsersRoute(config) {
5217
5616
  function createBuiltinAuthAdapter(config) {
5218
5617
  const { authRepository, emailService, emailConfig, allowRegistration = false, disableSelfRegistration = false, allowAnonymous = false, allowUserLookup = false, captcha, defaultRole, oauthProviders = [], allowedRedirectUris, serviceKey, authHooks, collectionAuthConfig, enableMagicLink = false, enableEmailOtp = false, cookieAuth } = config;
5219
5618
  const resolvedOps = resolveAuthHooks(authHooks);
5619
+ /**
5620
+ * The caller's roles as the database has them now.
5621
+ *
5622
+ * Read per request because the token's `roles` claim can be a whole
5623
+ * access-token lifetime stale — a demoted admin still holds a token that
5624
+ * says `admin`. So when the read fails, falling back to the claim hands
5625
+ * that token exactly the access the lookup exists to take away; during a
5626
+ * database blip it scoped a demoted admin with admin RLS. This refuses
5627
+ * instead, with the 503 `createRequireAuth` already gives the admin
5628
+ * routes: the adapter middleware answers with it, and the socket treats
5629
+ * a throwing adapter as an invalid token.
5630
+ */
5631
+ async function resolveLiveRoles(uid) {
5632
+ try {
5633
+ return await authRepository.getUserRoleIds(uid);
5634
+ } catch (error) {
5635
+ logger.warn("[Auth] Could not resolve roles for a request; refusing it", {
5636
+ uid,
5637
+ error
5638
+ });
5639
+ throw new ApiError(503, "ROLE_LOOKUP_FAILED", "Could not verify your permissions. Please try again.");
5640
+ }
5641
+ }
5220
5642
  return {
5221
5643
  id: "rebase-builtin",
5222
5644
  serviceKey,
@@ -5239,15 +5661,7 @@ function createBuiltinAuthAdapter(config) {
5239
5661
  });
5240
5662
  return null;
5241
5663
  }
5242
- let roles = payload.roles || [];
5243
- try {
5244
- roles = await authRepository.getUserRoleIds(payload.uid);
5245
- } catch (err) {
5246
- logger.warn("Role lookup from repository failed, using token roles as fallback", {
5247
- uid: payload.uid,
5248
- error: err
5249
- });
5250
- }
5664
+ const roles = await resolveLiveRoles(payload.uid);
5251
5665
  const isAdmin = hasAdministrativeRole(roles);
5252
5666
  return {
5253
5667
  uid: payload.uid,
@@ -5256,6 +5670,7 @@ function createBuiltinAuthAdapter(config) {
5256
5670
  roles,
5257
5671
  isAdmin,
5258
5672
  isAnonymous: payload.isAnonymous === true,
5673
+ ...payload.claims ? { claims: payload.claims } : {},
5259
5674
  rawToken: token
5260
5675
  };
5261
5676
  },
@@ -5276,15 +5691,7 @@ function createBuiltinAuthAdapter(config) {
5276
5691
  });
5277
5692
  return null;
5278
5693
  }
5279
- let roles = payload.roles || [];
5280
- try {
5281
- roles = await authRepository.getUserRoleIds(payload.uid);
5282
- } catch (err) {
5283
- logger.warn("Role lookup from repository failed, using token roles as fallback", {
5284
- uid: payload.uid,
5285
- error: err
5286
- });
5287
- }
5694
+ const roles = await resolveLiveRoles(payload.uid);
5288
5695
  const isAdmin = hasAdministrativeRole(roles);
5289
5696
  return {
5290
5697
  uid: payload.uid,
@@ -5293,6 +5700,7 @@ function createBuiltinAuthAdapter(config) {
5293
5700
  roles,
5294
5701
  isAdmin,
5295
5702
  isAnonymous: payload.isAnonymous === true,
5703
+ ...payload.claims ? { claims: payload.claims } : {},
5296
5704
  rawToken: token
5297
5705
  };
5298
5706
  },
@@ -5371,6 +5779,10 @@ function createBuiltinAuthAdapter(config) {
5371
5779
  collectionAuthConfig
5372
5780
  });
5373
5781
  },
5782
+ ...authCollectionUserAdmin({
5783
+ authRepo: authRepository,
5784
+ resolvedHooks: resolvedOps
5785
+ }),
5374
5786
  async getCapabilities() {
5375
5787
  let needsSetup = false;
5376
5788
  try {
@@ -5444,6 +5856,7 @@ function createUserManagementFromRepo(repo, resolvedOps) {
5444
5856
  },
5445
5857
  async deleteUser(id) {
5446
5858
  if (resolvedOps.beforeUserDelete) await resolvedOps.beforeUserDelete(id);
5859
+ await repo.deleteAllRefreshTokensForUser(id);
5447
5860
  await repo.deleteUser(id);
5448
5861
  if (resolvedOps.afterUserDelete) resolvedOps.afterUserDelete(id).catch((err) => {
5449
5862
  logger.error("[AuthHooks] afterUserDelete error", { error: err instanceof Error ? err.message : err });
@@ -7177,6 +7590,6 @@ var auth_exports = /* @__PURE__ */ __exportAll({
7177
7590
  verifyPassword: () => verifyPassword
7178
7591
  });
7179
7592
  //#endregion
7180
- export { requireAdmin as $, defaultAuthLimiter as A, resolveEmailLinkBase as B, providerVerifiedEmail as C, raw as Ct, DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT as D, isPublicStoragePath as Dt, isBootstrapWindowOpen as E, PUBLIC_STORAGE_PREFIX as Et, extractLinks as F, createAdapterAuthMiddleware as G, hashPassword as H, registerDevEmailSink as I, extractUserFromToken as J, createAuthMiddleware as K, SMTPEmailService as L, activeDevEmailSink as M, clearActiveDevEmailSink as N, createDataRateLimiter as O, createDevEmailSink as P, queryTokenAuth as Q, createEmailService as R, pkceTokenParams as S, html as St, createBuiltinAuthAdapter as T, isOperationAllowed as Tt, validatePasswordStrength as U, resolveAuthHooks as V, verifyPassword as W, optionalAuth as X, fileTokenAuth as Y, publicObjectAuth as Z, verifyOidcIdToken as _, getUserInvitationTemplate as _t, resolveRateLimitStoreKind as a, validateApiKey as at, createGoogleProvider as b, RawHtml as bt, createSlackProvider as c, SERVICE_IDENTITY as ct, createDiscordProvider as d, completeUserCreation as dt, requireAuth as et, createTwitterProvider as f, generateSecurePassword as ft, tryVerifyOidcIdToken as g, getPasswordResetTemplate as gt, createMicrosoftProvider as h, getMagicLinkTemplate as ht, createApiKeyStore as i, isApiKeyToken as it, MemoryRateLimitStore as j, createRateLimiter as k, createBitbucketProvider as l, scopeDataDriver as lt, createAppleProvider as m, getEmailVerificationTemplate as mt, createCustomAuthAdapter as n, createFunctionApiKeyGuard as nt, createSqlRateLimitStore as o, extractBearerToken as ot, createFacebookProvider as p, getEmailOtpTemplate as pt, createRequireAuth as q, createApiKeyRoutes as r, createStorageApiKeyGuard as rt, createSpotifyProvider as s, safeCompare as st, auth_exports as t, createApiKeyPreAuth as tt, createGitLabProvider as u, supportsRlsScoping as ut, createGitHubProvider as v, getWelcomeEmailTemplate as vt, createJwksRoutes as w, httpMethodToOperation as wt, oauthCodeFlowSchema as x, escapeHtml as xt, createLinkedinProvider as y, resolveEmailBranding as yt, assertEmailLinkBases as z };
7593
+ export { requireAdmin as $, createRateLimiter as A, isPublicStoragePath as At, createEmailService as B, providerVerifiedEmail as C, RawHtml as Ct, DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT as D, httpMethodToOperation as Dt, isBootstrapWindowOpen as E, raw as Et, clearActiveDevEmailSink as F, createAdapterAuthMiddleware as G, hashPassword as H, createDevEmailSink as I, extractUserFromToken as J, createAuthMiddleware as K, extractLinks as L, setSharedRateLimitStore as M, MemoryRateLimitStore as N, createDataRateLimitCheck as O, isOperationAllowed as Ot, activeDevEmailSink as P, queryTokenAuth as Q, registerDevEmailSink as R, pkceTokenParams as S, resolveEmailBranding as St, createBuiltinAuthAdapter as T, html as Tt, validatePasswordStrength as U, resolveAuthHooks as V, verifyPassword as W, optionalAuth as X, fileTokenAuth as Y, publicObjectAuth as Z, verifyOidcIdToken as _, getEmailVerificationTemplate as _t, resolveRateLimitStoreKind as a, validateApiKey as at, createGoogleProvider as b, getUserInvitationTemplate as bt, createSlackProvider as c, SERVICE_IDENTITY as ct, createDiscordProvider as d, isAccessTokenRevoked as dt, requireAuth as et, createTwitterProvider as f, completeUserCreation as ft, tryVerifyOidcIdToken as g, getEmailOtpTemplate as gt, createMicrosoftProvider as h, resolveEmailLinkBase as ht, createApiKeyStore as i, isApiKeyToken as it, defaultAuthLimiter as j, createDataRateLimiter as k, PUBLIC_STORAGE_PREFIX as kt, createBitbucketProvider as l, scopeDataDriver as lt, createAppleProvider as m, assertEmailLinkBases as mt, createCustomAuthAdapter as n, createFunctionApiKeyGuard as nt, createSqlRateLimitStore as o, extractBearerToken as ot, createFacebookProvider as p, generateSecurePassword as pt, createRequireAuth as q, createApiKeyRoutes as r, createStorageApiKeyGuard as rt, createSpotifyProvider as s, safeCompare as st, auth_exports as t, createApiKeyPreAuth as tt, createGitLabProvider as u, supportsRlsScoping as ut, createGitHubProvider as v, getMagicLinkTemplate as vt, createJwksRoutes as w, escapeHtml as wt, oauthCodeFlowSchema as x, getWelcomeEmailTemplate as xt, createLinkedinProvider as y, getPasswordResetTemplate as yt, SMTPEmailService as z };
7181
7594
 
7182
- //# sourceMappingURL=auth-DZXQRmYD.js.map
7595
+ //# sourceMappingURL=auth-B-GIMpDG.js.map