@rebasepro/server 0.17.3 → 0.18.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 (147) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +5 -1
  3. package/bin/rebase-server.js +16 -1
  4. package/dist/{GCSStorageController-Bl7nmhdv.js → GCSStorageController-ZXPoNqW3.js} +21 -10
  5. package/dist/GCSStorageController-ZXPoNqW3.js.map +1 -0
  6. package/dist/{S3StorageController-CWvvrRpF.js → S3StorageController-5pAXyv31.js} +24 -10
  7. package/dist/S3StorageController-5pAXyv31.js.map +1 -0
  8. package/dist/admin-roles-vYdp_Pil.js +36 -0
  9. package/dist/admin-roles-vYdp_Pil.js.map +1 -0
  10. package/dist/{admin_block-BX6YULvJ.js → admin_block-0Xu0r6eZ.js} +4 -1
  11. package/dist/admin_block-0Xu0r6eZ.js.map +1 -0
  12. package/dist/api/errors.d.ts +37 -0
  13. package/dist/api/live-schema-routes.d.ts +14 -0
  14. package/dist/api/logs-routes.d.ts +8 -0
  15. package/dist/api/rest/api-generator.d.ts +33 -0
  16. package/dist/api/root-error-handler.d.ts +62 -0
  17. package/dist/api/schema-drift.d.ts +39 -0
  18. package/dist/api/types.d.ts +55 -0
  19. package/dist/{ast-schema-editor-CvkYOvri.js → ast-schema-editor-CNgFJ3NF.js} +23 -7
  20. package/dist/ast-schema-editor-CNgFJ3NF.js.map +1 -0
  21. package/dist/auth/api-keys/api-key-routes.d.ts +12 -0
  22. package/dist/auth/auth-hooks.d.ts +13 -2
  23. package/dist/auth/exclude-auth-secrets.d.ts +14 -0
  24. package/dist/auth/jwt.d.ts +22 -1
  25. package/dist/auth/mfa-routes.d.ts +10 -1
  26. package/dist/auth/middleware.d.ts +22 -0
  27. package/dist/auth/rate-limiter.d.ts +50 -0
  28. package/dist/auth/registration-policy.d.ts +27 -0
  29. package/dist/auth/rls-scope.d.ts +9 -1
  30. package/dist/auth/routes.d.ts +8 -1
  31. package/dist/auth/seed-admin.d.ts +73 -0
  32. package/dist/auth/session-routes.d.ts +10 -0
  33. package/dist/{auth-DkbzMZUS.js → auth-BRiOuyq8.js} +283 -198
  34. package/dist/auth-BRiOuyq8.js.map +1 -0
  35. package/dist/{backup-CRdZkA6c.js → backup-C8P6Cl3G.js} +2 -2
  36. package/dist/{backup-CRdZkA6c.js.map → backup-C8P6Cl3G.js.map} +1 -1
  37. package/dist/boot/boot.d.ts +18 -0
  38. package/dist/boot/bundle.d.ts +29 -6
  39. package/dist/boot/ddl-bootstrap.d.ts +2 -0
  40. package/dist/boot/env.d.ts +30 -1
  41. package/dist/boot/fetch-bundle.d.ts +24 -1
  42. package/dist/boot/options.d.ts +0 -8
  43. package/dist/boot/provision.d.ts +58 -18
  44. package/dist/boot/resource-adapters.d.ts +11 -1
  45. package/dist/boot/resource-resolvers.d.ts +96 -0
  46. package/dist/boot/sources.d.ts +27 -20
  47. package/dist/collections/validate-config.d.ts +29 -0
  48. package/dist/{contract-routes-CHPVKTZW.js → contract-routes-CusnEB5h.js} +24 -9
  49. package/dist/{contract-routes-CHPVKTZW.js.map → contract-routes-CusnEB5h.js.map} +1 -1
  50. package/dist/cron/cron-loader.d.ts +7 -1
  51. package/dist/cron/cron-scheduler.d.ts +48 -2
  52. package/dist/{cron-loader-d9WMFENB.js → cron-loader-DnmIePn_.js} +23 -4
  53. package/dist/cron-loader-DnmIePn_.js.map +1 -0
  54. package/dist/{cron-routes-DSC-kZXI.js → cron-routes-D3x2ydMa.js} +9 -6
  55. package/dist/cron-routes-D3x2ydMa.js.map +1 -0
  56. package/dist/{cron-scheduler-BB82dWuU.js → cron-scheduler-BpPKpl4i.js} +148 -28
  57. package/dist/cron-scheduler-BpPKpl4i.js.map +1 -0
  58. package/dist/{cron-store-KeN8rN63.js → cron-store-CCQXwgVL.js} +5 -5
  59. package/dist/{cron-store-KeN8rN63.js.map → cron-store-CCQXwgVL.js.map} +1 -1
  60. package/dist/{ddl-bootstrap-5YZCZ8qk.js → ddl-bootstrap-CfNvxMuK.js} +26 -2
  61. package/dist/{ddl-bootstrap-5YZCZ8qk.js.map → ddl-bootstrap-CfNvxMuK.js.map} +1 -1
  62. package/dist/email/index.d.ts +1 -0
  63. package/dist/email/unconfigured.d.ts +20 -0
  64. package/dist/env.d.ts +25 -0
  65. package/dist/errors-HjfaPlvY.js +385 -0
  66. package/dist/errors-HjfaPlvY.js.map +1 -0
  67. package/dist/{function-loader-LLdmBFoL.js → function-loader-xnbDAPfa.js} +9 -3
  68. package/dist/{function-loader-LLdmBFoL.js.map → function-loader-xnbDAPfa.js.map} +1 -1
  69. package/dist/function-routes-gQ0EShVG.js +267 -0
  70. package/dist/function-routes-gQ0EShVG.js.map +1 -0
  71. package/dist/functions/define-function.d.ts +6 -3
  72. package/dist/functions/function-routes.d.ts +8 -4
  73. package/dist/functions/index.js +128 -23
  74. package/dist/functions/index.js.map +1 -1
  75. package/dist/{history-recorder-B2pL3X8b.js → history-recorder-B1FwXx9J.js} +3 -3
  76. package/dist/{history-recorder-B2pL3X8b.js.map → history-recorder-B1FwXx9J.js.map} +1 -1
  77. package/dist/{history-store-BUJ-37Sm.js → history-store-LHXaQywp.js} +2 -2
  78. package/dist/{history-store-BUJ-37Sm.js.map → history-store-LHXaQywp.js.map} +1 -1
  79. package/dist/index.d.ts +16 -4
  80. package/dist/index.es.js +16229 -7676
  81. package/dist/index.es.js.map +1 -1
  82. package/dist/init.d.ts +33 -13
  83. package/dist/{jobs-XXXcZrmj.js → jobs-DkkD9mPV.js} +5 -5
  84. package/dist/{jobs-XXXcZrmj.js.map → jobs-DkkD9mPV.js.map} +1 -1
  85. package/dist/{jwt-DNeOn39r.js → jwt-DkhXwMzR.js} +9 -314
  86. package/dist/jwt-DkhXwMzR.js.map +1 -0
  87. package/dist/keys-g8lbVC_o.js +230 -0
  88. package/dist/keys-g8lbVC_o.js.map +1 -0
  89. package/dist/{logger-DS03e908.js → logger-DO2PZc4i.js} +197 -18
  90. package/dist/logger-DO2PZc4i.js.map +1 -0
  91. package/dist/{logs-routes-DB72iQSr.js → logs-routes-CbsTpozn.js} +74 -6
  92. package/dist/logs-routes-CbsTpozn.js.map +1 -0
  93. package/dist/{openapi-generator-CpZp2Fai.js → openapi-generator-BIBbO1Tq.js} +221 -38
  94. package/dist/openapi-generator-BIBbO1Tq.js.map +1 -0
  95. package/dist/{proxy-CMymhnwG.js → proxy-Czngl3p9.js} +2 -2
  96. package/dist/{proxy-CMymhnwG.js.map → proxy-Czngl3p9.js.map} +1 -1
  97. package/dist/{query-parser-DqXNaBIH.js → query-parser-C68Q9EX4.js} +35 -6
  98. package/dist/{query-parser-DqXNaBIH.js.map → query-parser-C68Q9EX4.js.map} +1 -1
  99. package/dist/queues/runtime.d.ts +56 -0
  100. package/dist/{request-timeout-C8gkc-j7.js → request-timeout-DESvlfrS.js} +4 -6
  101. package/dist/request-timeout-DESvlfrS.js.map +1 -0
  102. package/dist/schema-edit/project-root.d.ts +4 -4
  103. package/dist/{schema-editor-routes-DNV_nBhW.js → schema-editor-routes-CcZKh50q.js} +4 -4
  104. package/dist/{schema-editor-routes-DNV_nBhW.js.map → schema-editor-routes-CcZKh50q.js.map} +1 -1
  105. package/dist/{schemas-DBxgjM9A.js → schemas-C3234HWE.js} +2746 -72
  106. package/dist/schemas-C3234HWE.js.map +1 -0
  107. package/dist/singleton.d.ts +6 -4
  108. package/dist/src-DHK4fHkw.js +1002 -0
  109. package/dist/src-DHK4fHkw.js.map +1 -0
  110. package/dist/{src-C9gKj23J.js → src-Dq-I3Ybx.js} +507 -54
  111. package/dist/src-Dq-I3Ybx.js.map +1 -0
  112. package/dist/storage/GCSStorageController.d.ts +5 -0
  113. package/dist/storage/LocalStorageController.d.ts +14 -0
  114. package/dist/storage/S3StorageController.d.ts +8 -0
  115. package/dist/storage/image-transform.d.ts +35 -0
  116. package/dist/storage/keys.d.ts +23 -0
  117. package/dist/storage/policies.d.ts +22 -0
  118. package/dist/storage/routes.d.ts +9 -0
  119. package/dist/storage/tus-handler.d.ts +43 -1
  120. package/dist/storage/types.d.ts +28 -0
  121. package/dist/types-BfKcm9do.js.map +1 -1
  122. package/dist/utils/dev-port.d.ts +18 -0
  123. package/dist/utils/logger.d.ts +60 -0
  124. package/dist/utils/request-logger.d.ts +2 -1
  125. package/package.json +30 -24
  126. package/dist/GCSStorageController-Bl7nmhdv.js.map +0 -1
  127. package/dist/S3StorageController-CWvvrRpF.js.map +0 -1
  128. package/dist/admin_block-BX6YULvJ.js.map +0 -1
  129. package/dist/ast-schema-editor-CvkYOvri.js.map +0 -1
  130. package/dist/auth-DkbzMZUS.js.map +0 -1
  131. package/dist/cron-loader-d9WMFENB.js.map +0 -1
  132. package/dist/cron-routes-DSC-kZXI.js.map +0 -1
  133. package/dist/cron-scheduler-BB82dWuU.js.map +0 -1
  134. package/dist/errors-DBwpj9N8.js +0 -231
  135. package/dist/errors-DBwpj9N8.js.map +0 -1
  136. package/dist/function-routes-ClT6UQpD.js +0 -41
  137. package/dist/function-routes-ClT6UQpD.js.map +0 -1
  138. package/dist/jwt-DNeOn39r.js.map +0 -1
  139. package/dist/logger-DS03e908.js.map +0 -1
  140. package/dist/logs-routes-DB72iQSr.js.map +0 -1
  141. package/dist/openapi-generator-CpZp2Fai.js.map +0 -1
  142. package/dist/request-timeout-C8gkc-j7.js.map +0 -1
  143. package/dist/schemas-DBxgjM9A.js.map +0 -1
  144. package/dist/src-C9gKj23J.js.map +0 -1
  145. package/dist/src-CcOl05Oq.js +0 -552
  146. package/dist/src-CcOl05Oq.js.map +0 -1
  147. package/dist/utils/logging.d.ts +0 -5
@@ -3,15 +3,17 @@ 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 { D as isAnonymousUid, E as ANONYMOUS_USER_ID } from "./src-C9gKj23J.js";
7
- import "./src-CcOl05Oq.js";
8
- import { n as createDdlBootstrapper, o as isSQLAdmin } from "./ddl-bootstrap-5YZCZ8qk.js";
9
- import { D as randomInt$1, E as constantTimeEqual, O as sha256Hex, S as canonicalStorageId, T as require_jsonwebtoken, _ as verifyMfaPendingToken, a as generateMfaPendingToken, c as getJwks, d as hasAsymmetricSigningKey, f as hashRefreshToken, g as verifyDownloadToken, h as verifyAccessToken, i as generateDownloadToken, l as getRefreshTokenExpiry, n as configureJwt, o as generateRefreshToken, p as isJwtConfigured, r as generateAccessToken, s as getAccessTokenExpiry, t as MAX_COOKIE_AGE_MS, u as getRefreshTokenTtlMs, w as tryCanonicalStorageKey } from "./jwt-DNeOn39r.js";
6
+ import { A as ANONYMOUS_USER_ID, j as isAnonymousUid } from "./src-Dq-I3Ybx.js";
7
+ import "./src-DHK4fHkw.js";
8
+ import { n as createDdlBootstrapper, o as isSQLAdmin } from "./ddl-bootstrap-CfNvxMuK.js";
10
9
  import { t as revokeInternalTableSql } from "./internal-tables-DYVcFFSv.js";
11
- import { t as logger } from "./logger-DS03e908.js";
12
- import { n as errorHandler, t as ApiError } from "./errors-DBwpj9N8.js";
13
- import { a as resolveListLimitParam } from "./query-parser-DqXNaBIH.js";
14
- import { i as object, o as string } from "./schemas-DBxgjM9A.js";
10
+ import { r as logger } from "./logger-DO2PZc4i.js";
11
+ import { n as errorHandler, t as ApiError } from "./errors-HjfaPlvY.js";
12
+ import { a as resolveListLimitParam } from "./query-parser-C68Q9EX4.js";
13
+ import { S as sha256Hex, _ as verifyMfaPendingToken, a as generateMfaPendingToken, b as constantTimeEqual, c as getJwks, d as hasAsymmetricSigningKey, f as hashRefreshToken, g as verifyDownloadToken, h as verifyAccessToken, i as generateDownloadToken, l as getRefreshTokenExpiry, n as configureJwt, o as generateRefreshToken, p as isJwtConfigured, r as generateAccessToken, s as getAccessTokenExpiry, t as MAX_COOKIE_AGE_MS, u as getRefreshTokenTtlMs, x as randomInt$1, y as require_jsonwebtoken } from "./jwt-DkhXwMzR.js";
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-g8lbVC_o.js";
16
+ import { Rn as string, Tn as object } from "./schemas-C3234HWE.js";
15
17
  import { createHash, randomBytes, randomInt } from "node:crypto";
16
18
  import { Hono } from "hono";
17
19
  import { promisify } from "util";
@@ -205,35 +207,6 @@ async function isAccessTokenRevoked(authRepo, payload) {
205
207
  return payload.iat < Math.floor(validAfter.getTime() / 1e3);
206
208
  }
207
209
  //#endregion
208
- //#region src/auth/admin-roles.ts
209
- /**
210
- * Which role names carry administrative privilege.
211
- *
212
- * One definition, because there used to be several and they disagreed.
213
- * `requireAdmin` accepted `admin` **or** `schema-admin`; the guard that refuses
214
- * a dangerous `defaultRole` compared against `admin` alone. So
215
- * `AUTH_DEFAULT_ROLE=schema-admin` passed the guard and made every public
216
- * registrant an administrator — and, since a `schema-admin` may edit users, one
217
- * of them could then grant themselves real `admin` and keep it.
218
- *
219
- * The guard and the check have to read the same list or the gap comes back, so
220
- * neither of them owns it.
221
- *
222
- * `schema-admin` is here deliberately: it can reach the schema editor and the
223
- * SQL surfaces, which is administrative by any definition that matters. If a
224
- * genuinely lesser role is added later, it does not belong in this list — it
225
- * belongs in a capability check of its own.
226
- */
227
- var ADMINISTRATIVE_ROLES = ["admin", "schema-admin"];
228
- /** Does this single role name carry administrative privilege? */
229
- function isAdministrativeRole(role) {
230
- return ADMINISTRATIVE_ROLES.includes(role);
231
- }
232
- /** Does any role in this list carry administrative privilege? */
233
- function hasAdministrativeRole(roles) {
234
- return !!roles?.some(isAdministrativeRole);
235
- }
236
- //#endregion
237
210
  //#region src/auth/rls-scope.ts
238
211
  var import_jsonwebtoken = /* @__PURE__ */ __toESM(require_jsonwebtoken(), 1);
239
212
  /**
@@ -527,6 +500,29 @@ function createApiKeyPreAuth(options) {
527
500
  //#endregion
528
501
  //#region src/auth/middleware.ts
529
502
  /**
503
+ * Answer a refusal with the canonical envelope, through the same formatter
504
+ * every route uses.
505
+ *
506
+ * These middlewares used to hand-build `c.json({ error: { message, code } },
507
+ * 401)` — seven near-copies of it, which is how they came to omit `requestId`
508
+ * while every other error on the server carried it: the one string a bug report
509
+ * quotes was missing from precisely the errors an app hits most. It also meant
510
+ * a routine 401 (a client refreshing before it knows whether a session exists)
511
+ * logged like an incident, because `ApiError.expected` never reached the
512
+ * logger.
513
+ *
514
+ * `errorHandler` is *called* rather than thrown to. A throw only becomes an
515
+ * envelope where an `onError` is registered, and these middlewares are mounted
516
+ * by callers this package does not control — `defineFunction` hands users a
517
+ * Hono app and invites them to put `requireAuth` on a route of it. Making the
518
+ * status depend on the mount point would turn a 401 into a 500 for exactly
519
+ * those callers. Calling the formatter directly gets the envelope with none of
520
+ * that.
521
+ */
522
+ function refuse(c, error) {
523
+ return errorHandler(error, c);
524
+ }
525
+ /**
530
526
  * Hono middleware that requires a valid JWT token via Authorization header.
531
527
  * Returns 401 if token is missing or invalid.
532
528
  *
@@ -539,15 +535,9 @@ function createApiKeyPreAuth(options) {
539
535
  var requireAuth = async (c, next) => {
540
536
  if (c.get("user")) return next();
541
537
  const token = extractBearerToken(c.req.header("authorization"));
542
- if (token === void 0) return c.json({ error: {
543
- message: "Authorization header missing or invalid",
544
- code: "UNAUTHORIZED"
545
- } }, 401);
538
+ if (token === void 0) return refuse(c, ApiError.unauthenticated("Authorization header missing or invalid"));
546
539
  const payload = await verifyAccessToken(token);
547
- if (!payload) return c.json({ error: {
548
- message: "Invalid or expired token",
549
- code: "UNAUTHORIZED"
550
- } }, 401);
540
+ if (!payload) return refuse(c, ApiError.unauthenticated("Invalid or expired token"));
551
541
  c.set("user", payload);
552
542
  return next();
553
543
  };
@@ -562,18 +552,15 @@ var requireAuth = async (c, next) => {
562
552
  * to be accessed via service keys for scripts and server-to-server calls.
563
553
  */
564
554
  function createRequireAuth(options) {
565
- if (!options?.serviceKey) return requireAuth;
555
+ if (!options?.serviceKey && !options?.resolveRoles && !options?.revocationRepo) return requireAuth;
566
556
  const key = options.serviceKey;
567
557
  const resolveRoles = options.resolveRoles;
568
558
  const revocationRepo = options.revocationRepo;
569
559
  return async (c, next) => {
570
560
  if (c.get("user")) return next();
571
561
  const token = extractBearerToken(c.req.header("authorization"));
572
- if (token === void 0) return c.json({ error: {
573
- message: "Authorization header missing or invalid",
574
- code: "UNAUTHORIZED"
575
- } }, 401);
576
- if (safeCompare(token, key)) {
562
+ if (token === void 0) return refuse(c, ApiError.unauthenticated("Authorization header missing or invalid"));
563
+ if (key !== void 0 && safeCompare(token, key)) {
577
564
  c.set("user", {
578
565
  uid: "service",
579
566
  roles: ["admin"]
@@ -581,19 +568,13 @@ function createRequireAuth(options) {
581
568
  return next();
582
569
  }
583
570
  const payload = await verifyAccessToken(token);
584
- if (!payload) return c.json({ error: {
585
- message: "Invalid or expired token",
586
- code: "UNAUTHORIZED"
587
- } }, 401);
588
- if (resolveRoles) try {
589
- if (revocationRepo && await isAccessTokenRevoked(revocationRepo, payload)) return c.json({ error: {
590
- message: "Session has been revoked",
591
- code: "SESSION_REVOKED"
592
- } }, 401);
593
- c.set("user", {
571
+ if (!payload) return refuse(c, ApiError.unauthenticated("Invalid or expired token"));
572
+ if (resolveRoles || revocationRepo) try {
573
+ if (revocationRepo && await isAccessTokenRevoked(revocationRepo, payload)) return refuse(c, ApiError.unauthorized("Session has been revoked", "SESSION_REVOKED"));
574
+ c.set("user", resolveRoles ? {
594
575
  ...payload,
595
576
  roles: await resolveRoles(payload.uid)
596
- });
577
+ } : payload);
597
578
  return next();
598
579
  } catch (error) {
599
580
  logger.warn("[Auth] Could not resolve roles for an admin-gated request", {
@@ -615,14 +596,8 @@ function createRequireAuth(options) {
615
596
  */
616
597
  var requireAdmin = async (c, next) => {
617
598
  const user = c.get("user");
618
- if (!user) return c.json({ error: {
619
- message: "User not authenticated. requireAuth middleware is missing?",
620
- code: "UNAUTHORIZED"
621
- } }, 401);
622
- if (!hasAdministrativeRole(typeof user === "object" && user !== null && "roles" in user ? user.roles || [] : [])) return c.json({ error: {
623
- message: "Admin privileges required for this operation",
624
- code: "FORBIDDEN"
625
- } }, 403);
599
+ if (!user) return refuse(c, ApiError.unauthorized("User not authenticated. requireAuth middleware is missing?"));
600
+ if (!hasAdministrativeRole(typeof user === "object" && user !== null && "roles" in user ? user.roles || [] : [])) return refuse(c, ApiError.forbidden("Admin privileges required for this operation"));
626
601
  return next();
627
602
  };
628
603
  /**
@@ -704,10 +679,7 @@ function createAuthMiddleware(options) {
704
679
  roles: ["anon"]
705
680
  }));
706
681
  } catch (error) {
707
- return c.json({ error: {
708
- message: "Unauthorized",
709
- code: "UNAUTHORIZED"
710
- } }, 401);
682
+ return refuse(c, ApiError.unauthenticated("Unauthorized"));
711
683
  }
712
684
  else {
713
685
  const token = extractBearerToken(c.req.header("authorization"));
@@ -741,7 +713,8 @@ function createAuthMiddleware(options) {
741
713
  try {
742
714
  const user = {
743
715
  uid: payload.uid,
744
- roles: payload.roles
716
+ roles: payload.roles,
717
+ isAnonymous: payload.isAnonymous === true
745
718
  };
746
719
  c.set("driver", await scopeDataDriver(driver, user));
747
720
  } catch (error) {
@@ -751,10 +724,7 @@ function createAuthMiddleware(options) {
751
724
  code: "INTERNAL_ERROR"
752
725
  } }, 500);
753
726
  }
754
- } else return c.json({ error: {
755
- message: "Invalid or expired token",
756
- code: "UNAUTHORIZED"
757
- } }, 401);
727
+ } else return refuse(c, ApiError.unauthenticated("Invalid or expired token"));
758
728
  }
759
729
  else try {
760
730
  c.set("driver", await scopeDataDriver(driver, {
@@ -769,10 +739,7 @@ function createAuthMiddleware(options) {
769
739
  } }, 500);
770
740
  }
771
741
  }
772
- if (enforceAuth && !c.get("user")) return c.json({ error: {
773
- message: "Unauthorized: Authentication required",
774
- code: "UNAUTHORIZED"
775
- } }, 401);
742
+ if (enforceAuth && !c.get("user")) return refuse(c, ApiError.unauthenticated("Unauthorized: Authentication required. This is the API-level gate, not a row rule — a collection granting public reads still needs a caller. Set AUTH_REQUIRE=false (or `auth.requireAuth: false`) to let RLS alone decide."));
776
743
  return next();
777
744
  };
778
745
  }
@@ -914,10 +881,7 @@ var fileTokenAuth = async (c, next) => {
914
881
  * and without it, a client that forgets to forward `?storageId=` on the file
915
882
  * URL reports a "path mismatch" for a path that matches perfectly.
916
883
  */
917
- const denyMismatch = (outcome) => c.json({ error: {
918
- message: outcome === "deny-storage" ? "Forbidden: Scoped token storage mismatch" : "Forbidden: Scoped token path mismatch",
919
- code: "FORBIDDEN"
920
- } }, 403);
884
+ const denyMismatch = (outcome) => refuse(c, ApiError.forbidden(outcome === "deny-storage" ? "Forbidden: Scoped token storage mismatch" : "Forbidden: Scoped token path mismatch"));
921
885
  const bearerToken = extractBearerToken(authHeader);
922
886
  if (bearerToken !== void 0) {
923
887
  const payload = await verifyDownloadToken(bearerToken);
@@ -932,10 +896,7 @@ var fileTokenAuth = async (c, next) => {
932
896
  }
933
897
  if (outcome !== "no-path") return denyMismatch(outcome);
934
898
  }
935
- if (isFileRoute) return c.json({ error: {
936
- message: "Unauthorized: Access JWT not allowed on file routes",
937
- code: "UNAUTHORIZED"
938
- } }, 401);
899
+ if (isFileRoute) return refuse(c, ApiError.unauthenticated("Unauthorized: Access JWT not allowed on file routes"));
939
900
  return next();
940
901
  }
941
902
  if (queryToken) {
@@ -951,10 +912,7 @@ var fileTokenAuth = async (c, next) => {
951
912
  }
952
913
  if (outcome !== "no-path") return denyMismatch(outcome);
953
914
  }
954
- return c.json({ error: {
955
- message: "Unauthorized: Invalid or unauthorized token",
956
- code: "UNAUTHORIZED"
957
- } }, 401);
915
+ return refuse(c, ApiError.unauthenticated("Unauthorized: Invalid or unauthorized token"));
958
916
  }
959
917
  return next();
960
918
  };
@@ -983,34 +941,27 @@ function createAdapterAuthMiddleware(options) {
983
941
  try {
984
942
  authenticatedUser = await adapter.verifyRequest(c.req.raw);
985
943
  } catch (error) {
986
- return c.json({ error: {
987
- message: "Unauthorized",
988
- code: "UNAUTHORIZED"
989
- } }, 401);
944
+ return refuse(c, ApiError.unauthenticated("Unauthorized"));
990
945
  }
991
946
  if (authenticatedUser) {
992
947
  c.set("user", {
993
948
  uid: authenticatedUser.uid,
994
949
  email: authenticatedUser.email,
995
- roles: authenticatedUser.roles
950
+ roles: authenticatedUser.roles,
951
+ isAnonymous: authenticatedUser.isAnonymous === true
996
952
  });
997
953
  try {
998
954
  c.set("driver", await scopeDataDriver(driver, {
999
955
  uid: authenticatedUser.uid,
1000
- roles: authenticatedUser.roles
956
+ roles: authenticatedUser.roles,
957
+ isAnonymous: authenticatedUser.isAnonymous === true
1001
958
  }));
1002
959
  } catch (error) {
1003
960
  logger.error("[AUTH-ADAPTER] RLS scoping failed for authenticated user", { error });
1004
- return c.json({ error: {
1005
- message: "Internal authentication error",
1006
- code: "INTERNAL_ERROR"
1007
- } }, 500);
961
+ return refuse(c, ApiError.internal("Internal authentication error"));
1008
962
  }
1009
963
  } else {
1010
- if (presentedToken !== void 0) return c.json({ error: {
1011
- message: "Invalid or expired token",
1012
- code: "UNAUTHORIZED"
1013
- } }, 401);
964
+ if (presentedToken !== void 0) return refuse(c, ApiError.unauthenticated("Invalid or expired token"));
1014
965
  try {
1015
966
  c.set("driver", await scopeDataDriver(driver, {
1016
967
  uid: ANONYMOUS_USER_ID,
@@ -1018,16 +969,10 @@ function createAdapterAuthMiddleware(options) {
1018
969
  }));
1019
970
  } catch (error) {
1020
971
  logger.error("[AUTH-ADAPTER] Failed to create anon-scoped driver", { error });
1021
- return c.json({ error: {
1022
- message: "Server configuration error",
1023
- code: "INTERNAL_ERROR"
1024
- } }, 500);
972
+ return refuse(c, ApiError.internal("Server configuration error"));
1025
973
  }
1026
974
  }
1027
- if (enforceAuth && !c.get("user")) return c.json({ error: {
1028
- message: "Unauthorized: Authentication required",
1029
- code: "UNAUTHORIZED"
1030
- } }, 401);
975
+ if (enforceAuth && !c.get("user")) return refuse(c, ApiError.unauthenticated("Unauthorized: Authentication required. This is the API-level gate, not a row rule — a collection granting public reads still needs a caller. Set AUTH_REQUIRE=false (or `auth.requireAuth: false`) to let RLS alone decide."));
1031
976
  return next();
1032
977
  };
1033
978
  }
@@ -1511,7 +1456,7 @@ If you weren't expecting this invitation, you can safely ignore this email.
1511
1456
  function getWelcomeEmailTemplate(user, appName = DEFAULT_APP_NAME, loginUrl, logoUrl) {
1512
1457
  const greeting = getGreeting(user);
1513
1458
  const url = loginUrl || "";
1514
- const subject = `¡Bienvenido/a a ${appName}!`;
1459
+ const subject = `Welcome to ${appName}`;
1515
1460
  const body = html`
1516
1461
  <!DOCTYPE html>
1517
1462
  <html>
@@ -1523,35 +1468,29 @@ function getWelcomeEmailTemplate(user, appName = DEFAULT_APP_NAME, loginUrl, log
1523
1468
  <body style="margin: 0; padding: 0; background-color: #f8fafc;">
1524
1469
  <div style="${styles.container}">${renderHeader(appName, logoUrl)}
1525
1470
  <div style="${styles.card}">
1526
- <h1 style="${styles.heading}">¡Bienvenido/a a ${appName}!</h1>
1527
-
1528
- <p style="${styles.paragraph}">
1529
- Hola ${greeting},
1530
- </p>
1531
-
1471
+ <h1 style="${styles.heading}">Welcome to ${appName}</h1>
1472
+
1532
1473
  <p style="${styles.paragraph}">
1533
- Tu cuenta en ${appName} ha sido creada exitosamente.
1534
- Estamos encantados de tenerte con nosotros.
1474
+ Hi ${greeting},
1535
1475
  </p>
1536
-
1476
+
1537
1477
  <p style="${styles.paragraph}">
1538
- Ya puedes acceder a tu panel y empezar a explorar todas las oportunidades
1539
- que tenemos para ti.
1478
+ Your ${appName} account is ready.
1540
1479
  </p>
1541
1480
 
1542
1481
  ${url ? html`
1543
1482
  <div style="text-align: center;">
1544
- <a href="${url}" style="${styles.button}">Ir a mi Panel</a>
1483
+ <a href="${url}" style="${styles.button}">Open ${appName}</a>
1545
1484
  </div>
1546
1485
  ` : raw("")}
1547
-
1486
+
1548
1487
  <p style="${styles.paragraph}">
1549
- Si tienes alguna pregunta, no dudes en contactarnos respondiendo a este correo.
1488
+ If you have any questions, just reply to this email.
1550
1489
  </p>
1551
-
1490
+
1552
1491
  <div style="${styles.footer}">
1553
1492
  <p style="margin: 0;">
1554
- Este correo fue enviado porque se creó una cuenta con esta dirección de email en ${appName}.
1493
+ You received this because an account was created with this email address at ${appName}.
1555
1494
  </p>
1556
1495
  </div>
1557
1496
  </div>
@@ -1560,17 +1499,15 @@ function getWelcomeEmailTemplate(user, appName = DEFAULT_APP_NAME, loginUrl, log
1560
1499
  </html>
1561
1500
  `;
1562
1501
  const text = `
1563
- ¡Bienvenido/a a ${appName}!
1564
-
1565
- Hola ${greeting},
1502
+ Welcome to ${appName}
1566
1503
 
1567
- Tu cuenta en ${appName} ha sido creada exitosamente. Estamos encantados de tenerte con nosotros.
1504
+ Hi ${greeting},
1568
1505
 
1569
- Ya puedes acceder a tu panel y empezar a explorar todas las oportunidades que tenemos para ti.
1506
+ Your ${appName} account is ready.
1570
1507
 
1571
- ${url ? `Ir a mi panel: ${url}` : ""}
1508
+ ${url ? `Open ${appName}: ${url}` : ""}
1572
1509
 
1573
- Si tienes alguna pregunta, no dudes en contactarnos respondiendo a este correo.
1510
+ If you have any questions, just reply to this email.
1574
1511
  `.trim();
1575
1512
  return {
1576
1513
  subject,
@@ -2509,6 +2446,55 @@ var strictAuthLimiter = createRateLimiter({
2509
2446
  * useless as a mail bomb. Unauthenticated requests fall back to the IP bucket so
2510
2447
  * the limiter is never a no-op if it is ever mounted before the auth middleware.
2511
2448
  */
2449
+ /**
2450
+ * Read the recipient address out of the body, so a limiter can key on it.
2451
+ *
2452
+ * A limiter runs before the handler, and Hono caches a parsed body — so this
2453
+ * reads a clone, stashes the address, and lets the handler read the same
2454
+ * request normally. Without it the limiters below have no account to key on
2455
+ * and collapse into one global bucket, which is a limiter in name only.
2456
+ *
2457
+ * A malformed body is left to the handler, which reports it with the message
2458
+ * that names the field; failing here would answer 429 to bad JSON.
2459
+ */
2460
+ var captureRecipientEmail = async (c, next) => {
2461
+ try {
2462
+ const body = await c.req.json();
2463
+ if (typeof body?.email === "string") c.set("recipientEmail", body.email.trim().toLowerCase());
2464
+ } catch {}
2465
+ await next();
2466
+ };
2467
+ /**
2468
+ * Per-ADDRESS throttle for a route that mails an address the caller named.
2469
+ *
2470
+ * `strictAuthLimiter` bounds a machine, and a machine is the attacker's to
2471
+ * multiply. The mailbox being filled belongs to somebody who cannot rotate
2472
+ * anything, and the domain the messages come from is this deployment's
2473
+ * reputation — so a distributed run against one address passed the IP limiter
2474
+ * untouched and cost the operator their sender score.
2475
+ *
2476
+ * Five per fifteen minutes: generous for "I didn't get it, send it again",
2477
+ * useless as a mail bomb. The same numbers as `verificationEmailLimiter`,
2478
+ * which bounds the same act for a signed-in caller.
2479
+ *
2480
+ * Falls back to the IP bucket for a request whose body named no address, so
2481
+ * the limiter is never silently a no-op.
2482
+ */
2483
+ var recipientEmailLimiter = createRateLimiter({
2484
+ windowMs: 900 * 1e3,
2485
+ limit: 5,
2486
+ keyGenerator: (c) => {
2487
+ const email = c.get("recipientEmail");
2488
+ return email ? `recipient-email:${email}` : `recipient-email:ip:${defaultKeyGenerator(c)}`;
2489
+ },
2490
+ message: "Too many emails requested for this address, please try again later."
2491
+ });
2492
+ /** Hold a result until at least `floorMs` has passed since `startedAt`. */
2493
+ async function notBefore$1(startedAt, floorMs, result) {
2494
+ const remaining = floorMs - (Date.now() - startedAt);
2495
+ if (remaining > 0) await new Promise((resolve) => setTimeout(resolve, remaining));
2496
+ return result;
2497
+ }
2512
2498
  var verificationEmailLimiter = createRateLimiter({
2513
2499
  windowMs: 900 * 1e3,
2514
2500
  limit: 5,
@@ -3176,7 +3162,7 @@ var mfaVerificationLimiter = createRateLimiter({
3176
3162
  message: "Too many verification attempts for this account, please try again later."
3177
3163
  });
3178
3164
  function mountMfaRoutes(opts) {
3179
- const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook } = opts;
3165
+ const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook, requireLiveSession } = opts;
3180
3166
  const authRepo = config.authRepo;
3181
3167
  const emailConfig = config.emailConfig;
3182
3168
  /**
@@ -3217,7 +3203,7 @@ function mountMfaRoutes(opts) {
3217
3203
  * POST /auth/mfa/enroll
3218
3204
  * Start MFA enrollment: generate TOTP secret and recovery codes
3219
3205
  */
3220
- router.post("/mfa/enroll", strictAuthLimiter, requireAuth, async (c) => {
3206
+ router.post("/mfa/enroll", strictAuthLimiter, requireLiveSession, async (c) => {
3221
3207
  const userCtx = c.get("user");
3222
3208
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
3223
3209
  await requireStepUpForFactorChange(userCtx);
@@ -3250,7 +3236,7 @@ function mountMfaRoutes(opts) {
3250
3236
  * POST /auth/mfa/verify
3251
3237
  * Verify TOTP code to complete MFA enrollment
3252
3238
  */
3253
- router.post("/mfa/verify", strictAuthLimiter, requireAuth, mfaVerificationLimiter, async (c) => {
3239
+ router.post("/mfa/verify", strictAuthLimiter, requireLiveSession, mfaVerificationLimiter, async (c) => {
3254
3240
  const userCtx = c.get("user");
3255
3241
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
3256
3242
  await requireStepUpForFactorChange(userCtx);
@@ -3359,7 +3345,7 @@ function mountMfaRoutes(opts) {
3359
3345
  * GET /auth/mfa/factors
3360
3346
  * List enrolled MFA factors for the current user
3361
3347
  */
3362
- router.get("/mfa/factors", strictAuthLimiter, requireAuth, async (c) => {
3348
+ router.get("/mfa/factors", strictAuthLimiter, requireLiveSession, async (c) => {
3363
3349
  const userCtx = c.get("user");
3364
3350
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
3365
3351
  const factors = await authRepo.getMfaFactors(userCtx.uid);
@@ -3375,7 +3361,7 @@ function mountMfaRoutes(opts) {
3375
3361
  * DELETE /auth/mfa/unenroll
3376
3362
  * Remove an MFA factor
3377
3363
  */
3378
- router.delete("/mfa/unenroll", strictAuthLimiter, requireAuth, async (c) => {
3364
+ router.delete("/mfa/unenroll", strictAuthLimiter, requireLiveSession, async (c) => {
3379
3365
  const userCtx = c.get("user");
3380
3366
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
3381
3367
  if (userCtx.aal !== "aal2") throw ApiError.forbidden("MFA verification required to unenroll. Please re-authenticate with your second factor.", "AAL2_REQUIRED");
@@ -3440,10 +3426,32 @@ async function assertMfaSatisfied(authRepo, uid) {
3440
3426
  }
3441
3427
  //#endregion
3442
3428
  //#region src/auth/registration-policy.ts
3429
+ /**
3430
+ * Whether the first-come-first-admin window exists at all.
3431
+ *
3432
+ * On a laptop it should: the person at the keyboard is the operator, and a
3433
+ * fresh database that nobody can administer is a dead end. On anything with a
3434
+ * public hostname it must not: the shipped artifacts bring DNS and TLS up
3435
+ * before the operator has typed anything, `GET /auth/config` advertises
3436
+ * `needsSetup`, and whoever reaches `POST /auth/register` first owns the
3437
+ * deployment. So the window is open outside production and closed inside it,
3438
+ * where the operator names the first admin with `REBASE_ADMIN_EMAIL` /
3439
+ * `REBASE_ADMIN_PASSWORD` (see `seed-admin.ts`) or assigns the role with the
3440
+ * service key — two ways in, neither of which is a race.
3441
+ */
3442
+ function isBootstrapWindowOpen(env = process.env) {
3443
+ return env.NODE_ENV !== "production";
3444
+ }
3445
+ /**
3446
+ * What to tell a caller who hit an empty production database with the window
3447
+ * shut. Named here so the register route and the bootstrap route say the same
3448
+ * thing.
3449
+ */
3450
+ var SETUP_REQUIRED_MESSAGE = "This deployment has no administrator yet, and in production the first account is not promoted automatically. Set REBASE_ADMIN_EMAIL and REBASE_ADMIN_PASSWORD before the first boot, or assign the admin role with the service key.";
3443
3451
  /** The full predicate, for callers that already know whether setup is needed. */
3444
3452
  function isRegistrationOpen(policy) {
3445
3453
  if (policy.disableSelfRegistration) return false;
3446
- return policy.needsSetup || !!policy.allowRegistration;
3454
+ return policy.needsSetup && (policy.bootstrapWindowOpen ?? isBootstrapWindowOpen()) || !!policy.allowRegistration;
3447
3455
  }
3448
3456
  /**
3449
3457
  * The same predicate with the bootstrap window assumed closed.
@@ -3492,7 +3500,7 @@ function isAnonymousAuthOpen(policy) {
3492
3500
  //#endregion
3493
3501
  //#region src/auth/session-routes.ts
3494
3502
  function mountSessionRoutes(opts) {
3495
- const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook } = opts;
3503
+ const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook, requireLiveSession } = opts;
3496
3504
  const authRepo = config.authRepo;
3497
3505
  /**
3498
3506
  * Refuse the anonymous routes unless the deployment opted in.
@@ -3535,7 +3543,7 @@ function mountSessionRoutes(opts) {
3535
3543
  clearRefreshCookie(c, config.cookieAuth);
3536
3544
  const accessToken = extractBearerToken(c.req.header("authorization"));
3537
3545
  if (ops.afterLogout && accessToken !== void 0) {
3538
- const { verifyAccessToken } = await import("./jwt-DNeOn39r.js").then((n) => n.m);
3546
+ const { verifyAccessToken } = await import("./jwt-DkhXwMzR.js").then((n) => n.m);
3539
3547
  const payload = await verifyAccessToken(accessToken);
3540
3548
  if (payload) ops.afterLogout(payload.uid).catch((err) => {
3541
3549
  logger.error("[AuthHooks] afterLogout error", { error: err instanceof Error ? err.message : err });
@@ -3547,7 +3555,7 @@ function mountSessionRoutes(opts) {
3547
3555
  * GET /auth/sessions
3548
3556
  * Get active refresh tokens (sessions) for the current user
3549
3557
  */
3550
- router.get("/sessions", requireAuth, async (c) => {
3558
+ router.get("/sessions", requireLiveSession, async (c) => {
3551
3559
  const userCtx = c.get("user");
3552
3560
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
3553
3561
  const currentRefreshToken = c.req.header("x-refresh-token");
@@ -3573,7 +3581,7 @@ function mountSessionRoutes(opts) {
3573
3581
  * DELETE /auth/sessions
3574
3582
  * Delete all refresh tokens for the current user (remote logout every device)
3575
3583
  */
3576
- router.delete("/sessions", requireAuth, async (c) => {
3584
+ router.delete("/sessions", requireLiveSession, async (c) => {
3577
3585
  const userCtx = c.get("user");
3578
3586
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
3579
3587
  await authRepo.deleteAllRefreshTokensForUser(userCtx.uid);
@@ -3587,7 +3595,7 @@ function mountSessionRoutes(opts) {
3587
3595
  * DELETE /auth/sessions/:id
3588
3596
  * Delete a specific refresh token (remote logout)
3589
3597
  */
3590
- router.delete("/sessions/:id", requireAuth, async (c) => {
3598
+ router.delete("/sessions/:id", requireLiveSession, async (c) => {
3591
3599
  const userCtx = c.get("user");
3592
3600
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
3593
3601
  const id = c.req.param("id");
@@ -3604,7 +3612,7 @@ function mountSessionRoutes(opts) {
3604
3612
  * GET /auth/me
3605
3613
  * Get current authenticated user
3606
3614
  */
3607
- router.get("/me", requireAuth, async (c) => {
3615
+ router.get("/me", requireLiveSession, async (c) => {
3608
3616
  const userCtx = c.get("user");
3609
3617
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
3610
3618
  const result = await authRepo.getUserWithRoles(userCtx.uid);
@@ -3629,7 +3637,7 @@ function mountSessionRoutes(opts) {
3629
3637
  * server function. Never returns the email, roles, metadata, or any other
3630
3638
  * field of the looked-up user.
3631
3639
  */
3632
- if (config.allowUserLookup) router.post("/find-user", defaultAuthLimiter, requireAuth, async (c) => {
3640
+ if (config.allowUserLookup) router.post("/find-user", defaultAuthLimiter, requireLiveSession, async (c) => {
3633
3641
  if (!c.get("user")) throw ApiError.unauthorized("Not authenticated");
3634
3642
  const { email } = parseBody(findUserSchema, await c.req.json());
3635
3643
  const user = await authRepo.getUserByEmail(normalizeEmail(email));
@@ -3643,7 +3651,7 @@ function mountSessionRoutes(opts) {
3643
3651
  * PATCH /auth/me
3644
3652
  * Update current authenticated user profile
3645
3653
  */
3646
- router.patch("/me", requireAuth, async (c) => {
3654
+ router.patch("/me", requireLiveSession, async (c) => {
3647
3655
  const userCtx = c.get("user");
3648
3656
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
3649
3657
  const { displayName, photoURL } = parseBody(updateProfileSchema, await c.req.json());
@@ -3694,7 +3702,7 @@ function mountSessionRoutes(opts) {
3694
3702
  * POST /auth/anonymous/link
3695
3703
  * Upgrade an anonymous user to a permanent account with email/password
3696
3704
  */
3697
- router.post("/anonymous/link", strictAuthLimiter, requireAuth, async (c) => {
3705
+ router.post("/anonymous/link", strictAuthLimiter, requireLiveSession, async (c) => {
3698
3706
  assertAnonymousAuthOpen();
3699
3707
  const userCtx = c.get("user");
3700
3708
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
@@ -3853,6 +3861,53 @@ var otpVerificationLimiter = createRateLimiter({
3853
3861
  message: "Too many verification attempts for this account, please try again later."
3854
3862
  });
3855
3863
  /**
3864
+ * Per-address throttle on code *delivery*.
3865
+ *
3866
+ * The verification limiter below has always been keyed per address; the send
3867
+ * route had only `strictAuthLimiter`, which is keyed per IP. An IP is the
3868
+ * attacker's to rotate and the mailbox under attack is not, so a distributed
3869
+ * run put an unbounded number of messages into one person's inbox — from this
3870
+ * deployment's domain, which is the reputation that pays for it.
3871
+ *
3872
+ * Five per fifteen minutes is generous for "I didn't get it, send it again"
3873
+ * and useless as a mail bomb, matching `verificationEmailLimiter`.
3874
+ */
3875
+ var otpSendLimiter = createRateLimiter({
3876
+ windowMs: 900 * 1e3,
3877
+ limit: 5,
3878
+ keyGenerator: (c) => {
3879
+ return `otp-send:${c.get("otpEmail") ?? "unidentified"}`;
3880
+ },
3881
+ message: "Too many sign-in codes requested for this account, please try again later."
3882
+ });
3883
+ /**
3884
+ * How long the send route takes, whoever asked.
3885
+ *
3886
+ * The route answers the same words for a known and an unknown address, which is
3887
+ * the point — but it did not take the same TIME. A known address meant a token
3888
+ * insert, a template render and an SMTP round trip; an unknown one meant
3889
+ * returning immediately. The difference is measurable from anywhere, so the
3890
+ * endpoint that refuses to say whether an account exists said it anyway, one
3891
+ * address at a time and at whatever rate the IP limiter allowed.
3892
+ *
3893
+ * Two things close it, and both are needed. The mail is sent off the response
3894
+ * path (below), which removes the largest and most variable part. And the
3895
+ * handler is held to a floor, which covers the rest — the token write, the
3896
+ * template — without needing the two branches to be instruction-for-instruction
3897
+ * equal, which is not a property anyone can maintain.
3898
+ *
3899
+ * 400ms is chosen to sit above the slow branch's normal cost rather than to
3900
+ * be pleasant: it is a login step a person takes once, and the alternative is
3901
+ * an enumeration oracle.
3902
+ */
3903
+ var OTP_SEND_FLOOR_MS = 400;
3904
+ /** Hold a result until at least `floorMs` has passed since `startedAt`. */
3905
+ async function notBefore(startedAt, floorMs, result) {
3906
+ const remaining = floorMs - (Date.now() - startedAt);
3907
+ if (remaining > 0) await new Promise((resolve) => setTimeout(resolve, remaining));
3908
+ return result;
3909
+ }
3910
+ /**
3856
3911
  * Read the address out of the body and put it where the limiter can key on it.
3857
3912
  *
3858
3913
  * The limiter runs before the handler, and Hono has already consumed nothing —
@@ -3884,12 +3939,13 @@ function mountOtpRoutes(deps) {
3884
3939
  * alternative is an oracle that turns this endpoint into a way to ask "is
3885
3940
  * this person a customer?", one address at a time.
3886
3941
  */
3887
- router.post("/otp", strictAuthLimiter, ...captchaMiddleware ? [captchaMiddleware] : [], async (c) => {
3942
+ router.post("/otp", strictAuthLimiter, captureOtpEmail, otpSendLimiter, ...captchaMiddleware ? [captchaMiddleware] : [], async (c) => {
3943
+ const startedAt = Date.now();
3888
3944
  const { email } = parseBody(requestSchema, await c.req.json());
3889
3945
  if (!isEmailConfigured()) throw ApiError.serviceUnavailable("Email service not configured. One-time code login is not available.", "EMAIL_NOT_CONFIGURED");
3890
3946
  const user = await authRepo.getUserByEmail(email);
3947
+ if (ops.beforeLogin) await ops.beforeLogin(email, "otp");
3891
3948
  if (user) {
3892
- if (ops.beforeLogin) await ops.beforeLogin(email, "otp");
3893
3949
  const code = generateOtpCode();
3894
3950
  await authRepo.createMagicLinkToken(user.id, hashToken(otpTokenMaterial(user.email, code)), new Date(Date.now() + OTP_TTL_MS));
3895
3951
  const { appName, logoUrl } = resolveEmailBranding(emailConfig);
@@ -3901,22 +3957,20 @@ function mountOtpRoutes(deps) {
3901
3957
  email: user.email,
3902
3958
  displayName: user.displayName
3903
3959
  }, appName, logoUrl);
3904
- try {
3905
- await emailService.send({
3906
- to: user.email,
3907
- subject: content.subject,
3908
- html: content.html,
3909
- text: content.text
3910
- });
3911
- } catch (emailError) {
3960
+ emailService.send({
3961
+ to: user.email,
3962
+ subject: content.subject,
3963
+ html: content.html,
3964
+ text: content.text
3965
+ }).catch((emailError) => {
3912
3966
  logger.error("Failed to send one-time code email", { error: emailError instanceof Error ? emailError.message : emailError });
3913
- }
3967
+ });
3914
3968
  }
3915
- return c.json({
3969
+ return notBefore(startedAt, OTP_SEND_FLOOR_MS, c.json({
3916
3970
  success: true,
3917
3971
  message: "If an account with that email exists, a sign-in code has been sent.",
3918
3972
  expiresInSeconds: OTP_TTL_MS / 1e3
3919
- });
3973
+ }));
3920
3974
  });
3921
3975
  /**
3922
3976
  * POST /auth/otp/verify — trade a code for a session.
@@ -4037,6 +4091,19 @@ function createAuthRoutes(config) {
4037
4091
  const router = new Hono();
4038
4092
  router.onError(errorHandler);
4039
4093
  const authRepo = config.authRepo;
4094
+ /**
4095
+ * `requireAuth`, plus the question of whether the session still exists.
4096
+ *
4097
+ * Plain `requireAuth` verifies the signature and the expiry and stops
4098
+ * there, so an access token stays good for its full hour after the account
4099
+ * behind it has signed out everywhere or reset its password — the
4100
+ * revocation watermark those write is checked on the data plane and was not
4101
+ * checked here. On the three self-service routes below that mattered
4102
+ * disproportionately: `/link/<provider>` attaches an OAuth identity to the
4103
+ * account, so a stolen token could be turned into a permanent way back in
4104
+ * AFTER the victim had done the one thing that is supposed to end it.
4105
+ */
4106
+ const requireLiveSession = createRequireAuth({ revocationRepo: authRepo });
4040
4107
  const { emailService, emailConfig, allowRegistration = false } = config;
4041
4108
  const ops = resolveAuthHooks(config.authHooks);
4042
4109
  /**
@@ -4145,9 +4212,11 @@ function createAuthRoutes(config) {
4145
4212
  if (!options?.skipMfaGate) await assertMfaSatisfied(authRepo, uid);
4146
4213
  const aal = options?.aal ?? "aal1";
4147
4214
  const roleIds = (await authRepo.getUserRoles(uid)).map((r) => r.id);
4215
+ const sessionUser = await authRepo.getUserById(uid);
4216
+ const isAnonymous = sessionUser?.isAnonymous === true;
4148
4217
  let customClaims;
4149
4218
  if (ops.customizeAccessToken) {
4150
- const user = await authRepo.getUserById(uid);
4219
+ const user = sessionUser;
4151
4220
  if (user) {
4152
4221
  const defaultClaims = {
4153
4222
  uid,
@@ -4157,7 +4226,7 @@ function createAuthRoutes(config) {
4157
4226
  customClaims = await ops.customizeAccessToken(defaultClaims, user);
4158
4227
  }
4159
4228
  }
4160
- const accessToken = await generateAccessToken(uid, roleIds, aal, customClaims);
4229
+ const accessToken = await generateAccessToken(uid, roleIds, aal, customClaims, isAnonymous);
4161
4230
  const refreshToken = generateRefreshToken();
4162
4231
  await authRepo.createRefreshToken(uid, await hashRefreshToken(refreshToken), getRefreshTokenExpiry(), userAgent, ipAddress, {
4163
4232
  id: randomUUID(),
@@ -4182,6 +4251,7 @@ function createAuthRoutes(config) {
4182
4251
  const { total } = await authRepo.listUsersPaginated({ limit: 1 });
4183
4252
  bootstrapRegistration = total === 0;
4184
4253
  if (!bootstrapRegistration) throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
4254
+ if (!isBootstrapWindowOpen()) throw ApiError.forbidden(SETUP_REQUIRED_MESSAGE, "SETUP_REQUIRED");
4185
4255
  }
4186
4256
  const passwordValidation = ops.validatePasswordStrength(password);
4187
4257
  if (!passwordValidation.valid) throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
@@ -4200,8 +4270,11 @@ function createAuthRoutes(config) {
4200
4270
  await authRepo.deleteUser(user.id);
4201
4271
  throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
4202
4272
  }
4203
- if (isFirstUser) await authRepo.setUserRoles(user.id, ["admin"]);
4204
- else if (config.defaultRole) await authRepo.assignDefaultRole(user.id, config.defaultRole);
4273
+ if (isFirstUser && isBootstrapWindowOpen()) await authRepo.setUserRoles(user.id, ["admin"]);
4274
+ else {
4275
+ if (isFirstUser) logger.warn("[Auth] First account registered in production without promotion; set REBASE_ADMIN_EMAIL/REBASE_ADMIN_PASSWORD or use the service key to assign admin.");
4276
+ if (config.defaultRole) await authRepo.assignDefaultRole(user.id, config.defaultRole);
4277
+ }
4205
4278
  const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(user.id, c.req.header("user-agent") || "unknown", c.req.header("x-forwarded-for") || "unknown");
4206
4279
  sendWelcomeEmail({
4207
4280
  email: user.email,
@@ -4295,6 +4368,7 @@ function createAuthRoutes(config) {
4295
4368
  const { total } = await authRepo.listUsersPaginated({ limit: 1 });
4296
4369
  bootstrapRegistration = total === 0;
4297
4370
  if (!bootstrapRegistration) throw ApiError.forbidden(noSignupsMessage, "REGISTRATION_DISABLED");
4371
+ if (!isBootstrapWindowOpen()) throw ApiError.forbidden(SETUP_REQUIRED_MESSAGE, "SETUP_REQUIRED");
4298
4372
  }
4299
4373
  user = await authRepo.createUser({
4300
4374
  email: normalizeEmail(externalUser.email),
@@ -4314,7 +4388,7 @@ function createAuthRoutes(config) {
4314
4388
  await authRepo.deleteUser(user.id);
4315
4389
  throw ApiError.forbidden(noSignupsMessage, "REGISTRATION_DISABLED");
4316
4390
  }
4317
- if (isFirstUser) await authRepo.setUserRoles(user.id, ["admin"]);
4391
+ if (isFirstUser && isBootstrapWindowOpen()) await authRepo.setUserRoles(user.id, ["admin"]);
4318
4392
  else if (config.defaultRole) await authRepo.assignDefaultRole(user.id, config.defaultRole);
4319
4393
  sendWelcomeEmail({
4320
4394
  email: user.email,
@@ -4348,7 +4422,7 @@ function createAuthRoutes(config) {
4348
4422
  * email plays no part in the decision, so its verification status
4349
4423
  * is irrelevant.
4350
4424
  */
4351
- router.post(`/link/${provider.id}`, defaultAuthLimiter, requireAuth, async (c) => {
4425
+ router.post(`/link/${provider.id}`, defaultAuthLimiter, requireLiveSession, async (c) => {
4352
4426
  const userCtx = c.get("user");
4353
4427
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
4354
4428
  const payload = parseBody(provider.schema, await c.req.json());
@@ -4379,7 +4453,8 @@ function createAuthRoutes(config) {
4379
4453
  * POST /auth/forgot-password
4380
4454
  * Request password reset email
4381
4455
  */
4382
- router.post("/forgot-password", strictAuthLimiter, ...captcha.forgotPassword ? [captcha.forgotPassword] : [], async (c) => {
4456
+ router.post("/forgot-password", strictAuthLimiter, captureRecipientEmail, recipientEmailLimiter, ...captcha.forgotPassword ? [captcha.forgotPassword] : [], async (c) => {
4457
+ const startedAt = Date.now();
4383
4458
  const { email } = parseBody(forgotPasswordSchema, await c.req.json());
4384
4459
  if (!isEmailConfigured()) throw ApiError.serviceUnavailable("Email service not configured. Password reset is not available.", "EMAIL_NOT_CONFIGURED");
4385
4460
  const user = await authRepo.getUserByEmail(email);
@@ -4398,21 +4473,19 @@ function createAuthRoutes(config) {
4398
4473
  email: user.email,
4399
4474
  displayName: user.displayName
4400
4475
  }, appName, logoUrl);
4401
- try {
4402
- await emailService.send({
4403
- to: user.email,
4404
- subject: emailContent.subject,
4405
- html: emailContent.html,
4406
- text: emailContent.text
4407
- });
4408
- } catch (emailError) {
4476
+ emailService.send({
4477
+ to: user.email,
4478
+ subject: emailContent.subject,
4479
+ html: emailContent.html,
4480
+ text: emailContent.text
4481
+ }).catch((emailError) => {
4409
4482
  logger.error("Failed to send password reset email", { error: emailError instanceof Error ? emailError.message : emailError });
4410
- }
4483
+ });
4411
4484
  }
4412
- return c.json({
4485
+ return notBefore$1(startedAt, 400, c.json({
4413
4486
  success: true,
4414
4487
  message: "If an account with that email exists, a password reset link has been sent."
4415
- });
4488
+ }));
4416
4489
  });
4417
4490
  /**
4418
4491
  * POST /auth/reset-password
@@ -4442,7 +4515,7 @@ function createAuthRoutes(config) {
4442
4515
  * POST /auth/change-password
4443
4516
  * Change password for authenticated user
4444
4517
  */
4445
- router.post("/change-password", requireAuth, async (c) => {
4518
+ router.post("/change-password", requireLiveSession, async (c) => {
4446
4519
  const userCtx = c.get("user");
4447
4520
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
4448
4521
  const { oldPassword, newPassword } = parseBody(changePasswordSchema, await c.req.json());
@@ -4471,7 +4544,7 @@ function createAuthRoutes(config) {
4471
4544
  * here: the address is unverified by construction, so an attacker can hold a
4472
4545
  * session for a mailbox that is not theirs.
4473
4546
  */
4474
- router.post("/send-verification", strictAuthLimiter, requireAuth, verificationEmailLimiter, async (c) => {
4547
+ router.post("/send-verification", strictAuthLimiter, requireLiveSession, verificationEmailLimiter, async (c) => {
4475
4548
  const userCtx = c.get("user");
4476
4549
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
4477
4550
  if (!isEmailConfigured()) throw ApiError.serviceUnavailable("Email service not configured. Email verification is not available.", "EMAIL_NOT_CONFIGURED");
@@ -4572,7 +4645,7 @@ function createAuthRoutes(config) {
4572
4645
  };
4573
4646
  customClaims = await ops.customizeAccessToken(defaultClaims, user);
4574
4647
  }
4575
- const newAccessToken = await generateAccessToken(storedToken.uid, roleIds, sessionAal, customClaims);
4648
+ const newAccessToken = await generateAccessToken(storedToken.uid, roleIds, sessionAal, customClaims, user?.isAnonymous === true);
4576
4649
  const newRefreshToken = generateRefreshToken();
4577
4650
  const userAgent = c.req.header("user-agent") || "unknown";
4578
4651
  const ipAddress = c.req.header("x-forwarded-for") || "unknown";
@@ -4609,7 +4682,8 @@ function createAuthRoutes(config) {
4609
4682
  parseBody,
4610
4683
  buildAuthResponse,
4611
4684
  createSessionAndTokens,
4612
- applyTransformHook
4685
+ applyTransformHook,
4686
+ requireLiveSession
4613
4687
  });
4614
4688
  mountMfaRoutes({
4615
4689
  router,
@@ -4618,7 +4692,8 @@ function createAuthRoutes(config) {
4618
4692
  parseBody,
4619
4693
  buildAuthResponse,
4620
4694
  createSessionAndTokens,
4621
- applyTransformHook
4695
+ applyTransformHook,
4696
+ requireLiveSession
4622
4697
  });
4623
4698
  if (config.enableMagicLink) mountMagicLinkRoutes({
4624
4699
  router,
@@ -4863,6 +4938,10 @@ function createAdminUsersRoute(config) {
4863
4938
  break;
4864
4939
  }
4865
4940
  if (hasAdmin) throw ApiError.forbidden("Admin users already exist. Bootstrap not allowed.", "BOOTSTRAP_COMPLETED");
4941
+ if (!isBootstrapWindowOpen()) {
4942
+ logger.warn("[Security Audit] Bootstrap denied: the first-admin window is closed in production", { eventType: "auth.bootstrap.denied.production" });
4943
+ throw ApiError.forbidden(SETUP_REQUIRED_MESSAGE, "SETUP_REQUIRED");
4944
+ }
4866
4945
  const uid = "uid" in user ? user.uid : void 0;
4867
4946
  if (!uid) throw ApiError.unauthorized("User ID not found in auth context");
4868
4947
  if (isAnonymousUid(uid)) {
@@ -5088,6 +5167,7 @@ function createBuiltinAuthAdapter(config) {
5088
5167
  displayName: payload.displayName ?? null,
5089
5168
  roles,
5090
5169
  isAdmin,
5170
+ isAnonymous: payload.isAnonymous === true,
5091
5171
  rawToken: token
5092
5172
  };
5093
5173
  },
@@ -5124,6 +5204,7 @@ function createBuiltinAuthAdapter(config) {
5124
5204
  displayName: payload.displayName ?? null,
5125
5205
  roles,
5126
5206
  isAdmin,
5207
+ isAnonymous: payload.isAnonymous === true,
5127
5208
  rawToken: token
5128
5209
  };
5129
5210
  },
@@ -5205,7 +5286,7 @@ function createBuiltinAuthAdapter(config) {
5205
5286
  async getCapabilities() {
5206
5287
  let needsSetup = false;
5207
5288
  try {
5208
- needsSetup = (await authRepository.listUsersPaginated({ limit: 1 })).total === 0;
5289
+ needsSetup = (await authRepository.listUsersPaginated({ limit: 1 })).total === 0 && isBootstrapWindowOpen();
5209
5290
  } catch (error) {
5210
5291
  logger.warn("[Auth] Could not count users for `needsSetup`; reporting the backend as already set up.", { error: error instanceof Error ? error.message : String(error) });
5211
5292
  }
@@ -6801,17 +6882,21 @@ function validatePermissions(permissions) {
6801
6882
  * edges left to get wrong.
6802
6883
  */
6803
6884
  var rejectApiKeyAuth = async (c, next) => {
6804
- if (c.get("apiKey")) throw ApiError.forbidden("API keys cannot manage API keys. Authenticate as an admin user, or use the service key.", "API_KEY_SELF_MANAGEMENT_FORBIDDEN");
6885
+ if (c.get("apiKey")) throw ApiError.forbidden("API keys cannot manage API keys. Authenticate as an admin user, or use the service key — REBASE_SERVICE_KEY in this server's environment, or `rebase cloud env reveal REBASE_SERVICE_KEY` on Rebase Cloud.", "API_KEY_SELF_MANAGEMENT_FORBIDDEN");
6805
6886
  return next();
6806
6887
  };
6807
6888
  /**
6808
6889
  * Create admin routes for API key management.
6809
6890
  */
6810
6891
  function createApiKeyRoutes(options) {
6811
- const { store, serviceKey } = options;
6892
+ const { store, serviceKey, resolveRoles, revocationRepo } = options;
6812
6893
  const router = new Hono();
6813
6894
  router.onError(errorHandler);
6814
- router.use("/*", createRequireAuth({ serviceKey }));
6895
+ router.use("/*", createRequireAuth({
6896
+ serviceKey,
6897
+ resolveRoles,
6898
+ revocationRepo
6899
+ }));
6815
6900
  router.use("/*", rejectApiKeyAuth);
6816
6901
  router.use("/*", requireAdmin);
6817
6902
  router.get("/", async (c) => {
@@ -7007,6 +7092,6 @@ var auth_exports = /* @__PURE__ */ __exportAll({
7007
7092
  verifyPassword: () => verifyPassword
7008
7093
  });
7009
7094
  //#endregion
7010
- export { html as $, activeDevEmailSink as A, hashPassword as B, providerVerifiedEmail as C, PUBLIC_STORAGE_PREFIX as Ct, createDataRateLimiter as D, DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT as E, SMTPEmailService as F, getEmailVerificationTemplate as G, verifyPassword as H, createEmailService as I, getUserInvitationTemplate as J, getMagicLinkTemplate as K, assertEmailLinkBases as L, createDevEmailSink as M, extractLinks as N, defaultAuthLimiter as O, registerDevEmailSink as P, escapeHtml as Q, resolveEmailLinkBase as R, pkceTokenParams as S, isOperationAllowed as St, createBuiltinAuthAdapter as T, generateSecurePassword as U, validatePasswordStrength as V, getEmailOtpTemplate as W, resolveEmailBranding as X, getWelcomeEmailTemplate as Y, RawHtml as Z, verifyOidcIdToken as _, safeCompare as _t, resolveRateLimitStoreKind as a, fileTokenAuth as at, createGoogleProvider as b, hasAdministrativeRole as bt, createSlackProvider as c, queryTokenAuth as ct, createDiscordProvider as d, createApiKeyPreAuth as dt, raw as et, createTwitterProvider as f, createFunctionApiKeyGuard as ft, tryVerifyOidcIdToken as g, extractBearerToken as gt, createMicrosoftProvider as h, validateApiKey as ht, createApiKeyStore as i, extractUserFromToken as it, clearActiveDevEmailSink as j, MemoryRateLimitStore as k, createBitbucketProvider as l, requireAdmin as lt, createAppleProvider as m, isApiKeyToken as mt, createCustomAuthAdapter as n, createAuthMiddleware as nt, createSqlRateLimitStore as o, optionalAuth as ot, createFacebookProvider as p, createStorageApiKeyGuard as pt, getPasswordResetTemplate as q, createApiKeyRoutes as r, createRequireAuth as rt, createSpotifyProvider as s, publicObjectAuth as st, auth_exports as t, createAdapterAuthMiddleware as tt, createGitLabProvider as u, requireAuth as ut, createGitHubProvider as v, SERVICE_IDENTITY as vt, createJwksRoutes as w, isPublicStoragePath as wt, oauthCodeFlowSchema as x, httpMethodToOperation as xt, createLinkedinProvider as y, scopeDataDriver as yt, resolveAuthHooks as z };
7095
+ export { escapeHtml as $, MemoryRateLimitStore as A, resolveAuthHooks as B, providerVerifiedEmail as C, PUBLIC_STORAGE_PREFIX as Ct, DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT as D, isBootstrapWindowOpen as E, registerDevEmailSink as F, getEmailOtpTemplate as G, validatePasswordStrength as H, SMTPEmailService as I, getPasswordResetTemplate as J, getEmailVerificationTemplate as K, createEmailService as L, clearActiveDevEmailSink as M, createDevEmailSink as N, createDataRateLimiter as O, extractLinks as P, RawHtml as Q, assertEmailLinkBases as R, pkceTokenParams as S, isOperationAllowed as St, createBuiltinAuthAdapter as T, verifyPassword as U, hashPassword as V, generateSecurePassword as W, getWelcomeEmailTemplate as X, getUserInvitationTemplate as Y, resolveEmailBranding as Z, verifyOidcIdToken as _, extractBearerToken as _t, resolveRateLimitStoreKind as a, extractUserFromToken as at, createGoogleProvider as b, scopeDataDriver as bt, createSlackProvider as c, publicObjectAuth as ct, createDiscordProvider as d, requireAuth as dt, html as et, createTwitterProvider as f, createApiKeyPreAuth as ft, tryVerifyOidcIdToken as g, validateApiKey as gt, createMicrosoftProvider as h, isApiKeyToken as ht, createApiKeyStore as i, createRequireAuth as it, activeDevEmailSink as j, defaultAuthLimiter as k, createBitbucketProvider as l, queryTokenAuth as lt, createAppleProvider as m, createStorageApiKeyGuard as mt, createCustomAuthAdapter as n, createAdapterAuthMiddleware as nt, createSqlRateLimitStore as o, fileTokenAuth as ot, createFacebookProvider as p, createFunctionApiKeyGuard as pt, getMagicLinkTemplate as q, createApiKeyRoutes as r, createAuthMiddleware as rt, createSpotifyProvider as s, optionalAuth as st, auth_exports as t, raw as tt, createGitLabProvider as u, requireAdmin as ut, createGitHubProvider as v, safeCompare as vt, createJwksRoutes as w, isPublicStoragePath as wt, oauthCodeFlowSchema as x, httpMethodToOperation as xt, createLinkedinProvider as y, SERVICE_IDENTITY as yt, resolveEmailLinkBase as z };
7011
7096
 
7012
- //# sourceMappingURL=auth-DkbzMZUS.js.map
7097
+ //# sourceMappingURL=auth-BRiOuyq8.js.map