@rebasepro/server 0.24.0 → 0.24.1-canary.g56af4a4

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 (181) hide show
  1. package/dist/GCSStorageController-DivE9bAx.js +402 -0
  2. package/dist/GCSStorageController-DivE9bAx.js.map +1 -0
  3. package/dist/S3StorageController-D_vKs4kP.js +480 -0
  4. package/dist/S3StorageController-D_vKs4kP.js.map +1 -0
  5. package/dist/api/rest/api-generator.d.ts +53 -1
  6. package/dist/api/rest/auth-collection-writes.d.ts +28 -21
  7. package/dist/api/rest/field-access-query.d.ts +28 -8
  8. package/dist/api/rest/field-ops.d.ts +19 -7
  9. package/dist/api/rest/query-body.d.ts +10 -0
  10. package/dist/api/rest/write-validation.d.ts +7 -5
  11. package/dist/api/schema-editor-routes.d.ts +10 -1
  12. package/dist/api/sql-export-routes.d.ts +8 -0
  13. package/dist/api/types.d.ts +11 -0
  14. package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-CSdL0iCk.js} +2 -2
  15. package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-CSdL0iCk.js.map} +1 -1
  16. package/dist/auth/access.d.ts +16 -0
  17. package/dist/auth/admin-user-ops.d.ts +24 -2
  18. package/dist/auth/admin-users-route.d.ts +1 -0
  19. package/dist/auth/expired-token-sweep.d.ts +5 -1
  20. package/dist/auth/interfaces.d.ts +10 -0
  21. package/dist/auth/jwt-crypto.d.ts +7 -0
  22. package/dist/auth/jwt-keys.d.ts +4 -0
  23. package/dist/auth/jwt.d.ts +6 -1
  24. package/dist/auth/rate-limiter.d.ts +13 -1
  25. package/dist/auth/registration-policy.d.ts +17 -0
  26. package/dist/auth/user-picking.d.ts +35 -0
  27. package/dist/{auth-DMLngxn_.js → auth-OPq_FeMs.js} +972 -85
  28. package/dist/auth-OPq_FeMs.js.map +1 -0
  29. package/dist/auth_adapter-CIItCTc8.js +21 -0
  30. package/dist/auth_adapter-CIItCTc8.js.map +1 -0
  31. package/dist/backend-DTAOsLQc.js.map +1 -1
  32. package/dist/backup/backup-common.d.ts +14 -3
  33. package/dist/backup/backup-routes.d.ts +23 -2
  34. package/dist/backup/backup-storage.d.ts +3 -1
  35. package/dist/backup/index.d.ts +3 -1
  36. package/dist/{backup-CN0s50D2.js → backup-5d7XhLX3.js} +170 -33
  37. package/dist/backup-5d7XhLX3.js.map +1 -0
  38. package/dist/boot/driver.d.ts +3 -0
  39. package/dist/boot/env.d.ts +7 -0
  40. package/dist/boot/http-server.d.ts +22 -0
  41. package/dist/boot/options.d.ts +16 -1
  42. package/dist/boot/runtime-cors.d.ts +17 -0
  43. package/dist/boot/static-routing.d.ts +9 -0
  44. package/dist/{contract-routes-fz8i4pxs.js → contract-routes-02lASyJI.js} +3 -19
  45. package/dist/contract-routes-02lASyJI.js.map +1 -0
  46. package/dist/cron/cron-routes.d.ts +1 -1
  47. package/dist/cron/cron-scheduler.d.ts +69 -3
  48. package/dist/cron/cron-store.d.ts +27 -4
  49. package/dist/cron/define-cron.d.ts +6 -2
  50. package/dist/cron/index.d.ts +1 -1
  51. package/dist/{cron-loader-CwaANlOG.js → cron-loader-D7OmleAb.js} +8 -4
  52. package/dist/cron-loader-D7OmleAb.js.map +1 -0
  53. package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-CBfiaqEL.js} +27 -4
  54. package/dist/cron-routes-CBfiaqEL.js.map +1 -0
  55. package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-C2vbETMo.js} +298 -32
  56. package/dist/cron-scheduler-C2vbETMo.js.map +1 -0
  57. package/dist/cron-store-B2Ih0gCN.js +540 -0
  58. package/dist/cron-store-B2Ih0gCN.js.map +1 -0
  59. package/dist/email/smtp-email-service.d.ts +23 -0
  60. package/dist/email/types.d.ts +18 -0
  61. package/dist/env.d.ts +0 -37
  62. package/dist/filter-dialect-REl-TjZr.js +1628 -0
  63. package/dist/filter-dialect-REl-TjZr.js.map +1 -0
  64. package/dist/{function-loader-D7o5Epjj.js → function-loader-BDKKiBmy.js} +4 -3
  65. package/dist/function-loader-BDKKiBmy.js.map +1 -0
  66. package/dist/{function-routes-CaNG4waN.js → function-routes-DWB7BBsi.js} +2 -2
  67. package/dist/function-routes-DWB7BBsi.js.map +1 -0
  68. package/dist/functions/context.d.ts +9 -0
  69. package/dist/functions/define-function.d.ts +7 -4
  70. package/dist/functions/index.d.ts +1 -1
  71. package/dist/functions/index.js +25 -2
  72. package/dist/functions/index.js.map +1 -1
  73. package/dist/functions/request-timeout.d.ts +7 -5
  74. package/dist/history/history-routes.d.ts +9 -0
  75. package/dist/history/retention-sweep.d.ts +81 -0
  76. package/dist/{history-recorder-Nr8zLvoU.js → history-recorder-DHenr3lm.js} +2 -2
  77. package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-DHenr3lm.js.map} +1 -1
  78. package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
  79. package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
  80. package/dist/index.d.ts +6 -5
  81. package/dist/index.es.js +22671 -19191
  82. package/dist/index.es.js.map +1 -1
  83. package/dist/init/shutdown.d.ts +12 -2
  84. package/dist/init.d.ts +16 -6
  85. package/dist/internal-tables-L2t39RdT.js +136 -0
  86. package/dist/internal-tables-L2t39RdT.js.map +1 -0
  87. package/dist/jobs/job-queue.d.ts +8 -0
  88. package/dist/jobs/job-store.d.ts +24 -0
  89. package/dist/jobs/types.d.ts +25 -4
  90. package/dist/{jobs-DqYNfquG.js → jobs-BpD4Q0qR.js} +232 -22
  91. package/dist/jobs-BpD4Q0qR.js.map +1 -0
  92. package/dist/{jwt-R6bSPMjk.js → jwt-Bqcu7cGt.js} +171 -65
  93. package/dist/jwt-Bqcu7cGt.js.map +1 -0
  94. package/dist/load-failure-UvzZgGXT.js +48 -0
  95. package/dist/load-failure-UvzZgGXT.js.map +1 -0
  96. package/dist/mcp/mcp-tools.d.ts +8 -0
  97. package/dist/metrics/index.d.ts +18 -0
  98. package/dist/metrics/pool-wait.d.ts +50 -0
  99. package/dist/offline-8Ks3yp9n-CadBKm2D.js +3305 -0
  100. package/dist/offline-8Ks3yp9n-CadBKm2D.js.map +1 -0
  101. package/dist/offline-store-D9ytNNaa-BCr2vsFB.js +1903 -0
  102. package/dist/offline-store-D9ytNNaa-BCr2vsFB.js.map +1 -0
  103. package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-BqoUh4Ij.js} +365 -81
  104. package/dist/openapi-generator-BqoUh4Ij.js.map +1 -0
  105. package/dist/policy-PdzX-Opd.js +121 -0
  106. package/dist/policy-PdzX-Opd.js.map +1 -0
  107. package/dist/project_manifest-R6y_KHms.js +59 -0
  108. package/dist/project_manifest-R6y_KHms.js.map +1 -0
  109. package/dist/{query-parser-BgiKJKvc.js → query-parser-DK9eMkb9.js} +510 -15
  110. package/dist/query-parser-DK9eMkb9.js.map +1 -0
  111. package/dist/{request-timeout-DgH7j8qO.js → request-timeout-Dnl2_OBc.js} +15 -8
  112. package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
  113. package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
  114. package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
  115. package/dist/schema-edit/apply-schema-change.d.ts +10 -0
  116. package/dist/schema-edit/local-source.d.ts +29 -0
  117. package/dist/schema-edit/remote-source.d.ts +13 -3
  118. package/dist/schema-edit/source-editor.d.ts +28 -0
  119. package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-BFf3i8jm.js} +53 -9
  120. package/dist/schema-editor-routes-BFf3i8jm.js.map +1 -0
  121. package/dist/services/outbound-post.d.ts +50 -0
  122. package/dist/services/outbound-url-guard.d.ts +34 -5
  123. package/dist/services/routed-realtime-service.d.ts +5 -0
  124. package/dist/services/webhook-service.d.ts +66 -14
  125. package/dist/soft-delete-params-47yBGs3_.js +668 -0
  126. package/dist/soft-delete-params-47yBGs3_.js.map +1 -0
  127. package/dist/sql-export-routes-BNNuGGLP.js +240 -0
  128. package/dist/sql-export-routes-BNNuGGLP.js.map +1 -0
  129. package/dist/src-CatHFUym.js.map +1 -1
  130. package/dist/storage/GCSStorageController.d.ts +46 -2
  131. package/dist/storage/LocalStorageController.d.ts +67 -2
  132. package/dist/storage/S3StorageController.d.ts +37 -3
  133. package/dist/storage/image-transform.d.ts +5 -0
  134. package/dist/storage/maintenance-routes.d.ts +23 -0
  135. package/dist/storage/maintenance.d.ts +126 -0
  136. package/dist/storage/rendition-cache.d.ts +9 -11
  137. package/dist/storage/request-principals.d.ts +30 -0
  138. package/dist/storage/routes.d.ts +20 -0
  139. package/dist/storage/source-variables.d.ts +10 -0
  140. package/dist/storage/streams.d.ts +42 -0
  141. package/dist/storage/tus-handler.d.ts +46 -2
  142. package/dist/storage/types.d.ts +82 -7
  143. package/dist/storage/upload-validation.d.ts +13 -0
  144. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
  145. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
  146. package/dist/strings.es-BeZWAodn.js +256 -0
  147. package/dist/strings.es-BeZWAodn.js.map +1 -0
  148. package/dist/upload-validation-Dd-fnsbA.js +172 -0
  149. package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
  150. package/dist/utils/byte-size.d.ts +10 -0
  151. package/dist/utils/compression.d.ts +6 -0
  152. package/dist/utils/load-failure.d.ts +14 -0
  153. package/package.json +5 -5
  154. package/dist/GCSStorageController-BSiP1c-f.js +0 -256
  155. package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
  156. package/dist/S3StorageController-CAwFRgjV.js +0 -276
  157. package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
  158. package/dist/auth-DMLngxn_.js.map +0 -1
  159. package/dist/backup-CN0s50D2.js.map +0 -1
  160. package/dist/contract-routes-fz8i4pxs.js.map +0 -1
  161. package/dist/cron-loader-CwaANlOG.js.map +0 -1
  162. package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
  163. package/dist/cron-scheduler-CYQgco86.js.map +0 -1
  164. package/dist/cron-store-D2Q9-Aco.js +0 -300
  165. package/dist/cron-store-D2Q9-Aco.js.map +0 -1
  166. package/dist/dynamic-import-X40pTZUQ.js +0 -23
  167. package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
  168. package/dist/function-loader-D7o5Epjj.js.map +0 -1
  169. package/dist/function-routes-CaNG4waN.js.map +0 -1
  170. package/dist/jobs-DqYNfquG.js.map +0 -1
  171. package/dist/jwt-R6bSPMjk.js.map +0 -1
  172. package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
  173. package/dist/query-parser-BgiKJKvc.js.map +0 -1
  174. package/dist/request-timeout-DgH7j8qO.js.map +0 -1
  175. package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
  176. package/dist/soft-delete-params-BWPilMPF.js +0 -59
  177. package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
  178. package/dist/src-I3aG1PcY.js +0 -6870
  179. package/dist/src-I3aG1PcY.js.map +0 -1
  180. package/dist/types-BfKcm9do.js +0 -39
  181. package/dist/types-BfKcm9do.js.map +0 -1
@@ -3,14 +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 { P as revokeInternalTableSql, ct as isAnonymousUid, st as ANONYMOUS_USER_ID } from "./src-I3aG1PcY.js";
7
- import { _ as scopesForRoles, a as KEY_MANAGEMENT_SCOPES, b as validateScopes, c as intersectScopes, d as isDataPlaneScope, f as knownScopes, g as scopesBeyond, h as scopeGrantsAny, i as EMPTY_ACCESS_MODEL, l as isAdminScope, m as scopeGrants, n as ADMIN_SCOPES, o as SCOPE_TARGETS, p as parseScope, r as BUILT_IN_SCOPES, s as hasAdminRole, t as ADMIN_ROLE, u as isBuiltInScope, v as summarizeRoles } from "./src-CatHFUym.js";
6
+ import { r as toSnakeCase } from "./strings.es-BeZWAodn.js";
7
+ import { B as rewriteLegacyRlsFunctions, _ as scopesForRoles, a as KEY_MANAGEMENT_SCOPES, b as validateScopes, c as intersectScopes, d as isDataPlaneScope, f as knownScopes, g as scopesBeyond, h as scopeGrantsAny, i as EMPTY_ACCESS_MODEL, l as isAdminScope, m as scopeGrants, n as ADMIN_SCOPES, o as SCOPE_TARGETS, p as parseScope, r as BUILT_IN_SCOPES, s as hasAdminRole, t as ADMIN_ROLE, u as isBuiltInScope, v as summarizeRoles } from "./src-CatHFUym.js";
8
+ import { n as isAnonymousUid, r as policy, t as ANONYMOUS_USER_ID } from "./policy-PdzX-Opd.js";
8
9
  import { t as isSQLAdmin } from "./backend-DTAOsLQc.js";
10
+ import { t as IMPERSONATE_HEADER } from "./auth_adapter-CIItCTc8.js";
11
+ import { n as revokeInternalTableSql, t as REBASE_USER_ROLE } from "./internal-tables-L2t39RdT.js";
9
12
  import { t as firstSqlRow } from "./sql-rows-C6GEc2oE.js";
13
+ import { a as resolveListLimitParam } from "./query-parser-DK9eMkb9.js";
10
14
  import { r as logger } from "./logger-D-S-hO5e.js";
11
15
  import { i as isDataException, r as errorHandler, t as ApiError } from "./errors-D6_y86c5.js";
12
- import { a as resolveListLimitParam } from "./query-parser-BgiKJKvc.js";
13
- import { D as constantTimeEqual, E as require_jsonwebtoken, S as verifyMfaPendingToken, a as generateAccessToken, b as verifyDownloadToken, c as generateMfaPendingToken, d as getJwks, f as getRefreshTokenExpiry, g as isJwtConfigured, h as hashRefreshToken, i as customClaimsOf, j as sha256Hex, k as randomInt$1, l as generateRefreshToken, m as hasAsymmetricSigningKey, n as configureJwt, o as generateDownloadToken, p as getRefreshTokenTtlMs, r as configuredJwtSecret, t as MAX_COOKIE_AGE_MS, u as getAccessTokenExpiry, y as verifyAccessToken } from "./jwt-R6bSPMjk.js";
16
+ import { D as constantTimeEqual, E as require_jsonwebtoken, S as verifyMfaPendingToken, a as generateAccessToken, b as verifyDownloadToken, c as generateMfaPendingToken, d as getJwks, f as getRefreshTokenExpiry, g as isJwtConfigured, h as hashRefreshToken, i as customClaimsOf, j as sha256Hex, k as randomInt$1, l as generateRefreshToken, m as hasAsymmetricSigningKey, n as configureJwt, o as generateDownloadToken, p as getRefreshTokenTtlMs, r as configuredJwtSecret, t as MAX_COOKIE_AGE_MS, u as getAccessTokenExpiry, y as verifyAccessToken } from "./jwt-Bqcu7cGt.js";
14
17
  import { a as canonicalStorageId, o as canonicalStorageKey, r as InvalidStorageKeyError } from "./keys-GAVZqbqx.js";
15
18
  import { n as createDdlBootstrapper } from "./ddl-bootstrap-BaqMSa4Y.js";
16
19
  import { createHash, randomBytes, randomInt } from "node:crypto";
@@ -20,20 +23,6 @@ import { createCipheriv, createDecipheriv, createHash as createHash$1, createHma
20
23
  import { getConnInfo } from "@hono/node-server/conninfo";
21
24
  import { createMiddleware } from "hono/factory";
22
25
  import { z } from "zod";
23
- //#region ../types/src/types/auth_adapter.ts
24
- /**
25
- * The request header that asks for a data-plane request to run as another
26
- * user, named by uid.
27
- *
28
- * The server honours it only on a signed-in administrator's own session —
29
- * never on an API key or the service key — and only through an adapter that
30
- * implements {@link AuthAdapter.resolveUser}. Everywhere else a request that
31
- * carries it is refused, never run as its caller.
32
- *
33
- * @group Auth
34
- */
35
- var IMPERSONATE_HEADER = "x-rebase-impersonate";
36
- //#endregion
37
26
  //#region ../types/src/controllers/storage.ts
38
27
  /**
39
28
  * Path prefix that marks an object as **public**. Files stored under this
@@ -102,8 +91,8 @@ function isPublicStoragePath(path) {
102
91
  * so the key `default/photo.png` sent as it is names the root `photo.png`:
103
92
  * {@link defaultBucketPath} is the spelling that names the key itself.
104
93
  *
105
- * Shared by every storage source that talks to those routes — the SDK's and
106
- * the admin panel's `useBackendStorageSource` — so they address an object alike.
94
+ * Shared by every storage source that talks to those routes, so they address
95
+ * an object alike.
107
96
  *
108
97
  * @group Models
109
98
  */
@@ -169,6 +158,476 @@ function normalizeEmail(email) {
169
158
  return typeof email === "string" ? email.trim().toLowerCase() : email;
170
159
  }
171
160
  //#endregion
161
+ //#region ../common/src/util/policy/sqlToPolicy.ts
162
+ /**
163
+ * A tiny, regex-based SQL "parser" for security rules.
164
+ *
165
+ * This is NOT a full SQL parser. It is designed to handle the subset of SQL
166
+ * commonly used in `USING` and `WITH CHECK` clauses, enough to drive the
167
+ * optimistic client-side UI decision.
168
+ *
169
+ * It handles:
170
+ * - `field = 'literal'`
171
+ * - `field != 'literal'`
172
+ * - `field = current_setting('app.uid')` (or the legacy `app.user_id`)
173
+ * - `A AND B`, `A OR B` — only where the keyword is at the top level
174
+ * - `true`
175
+ * - `IN (...)` (as optimistic true)
176
+ *
177
+ * For anything it doesn't understand, it returns a `raw` expression, which
178
+ * the evaluator treats as "unknown" (and usually optimistic true).
179
+ *
180
+ * **This output also round-trips back into DDL** via `policyToPostgres` (the
181
+ * schema/policy generators), so decomposing a clause the parser only partly
182
+ * understands is not a cosmetic mistake — it emits invalid SQL. When in doubt,
183
+ * prefer `raw`: it is reproduced verbatim.
184
+ */
185
+ /** True when `keyword` starts at `i` as a standalone word. */
186
+ function isKeywordAt(upper, i, keyword) {
187
+ if (!upper.startsWith(keyword, i)) return false;
188
+ const before = i === 0 ? " " : upper[i - 1];
189
+ const after = upper[i + keyword.length] ?? " ";
190
+ return /[\s()]/.test(before) && /[\s()]/.test(after);
191
+ }
192
+ /**
193
+ * Split `sql` on a boolean keyword, but only where it sits at paren depth 0 and
194
+ * outside a string literal. Returns null when it never does, so the caller
195
+ * leaves the clause alone.
196
+ *
197
+ * This used to be `sql.split(/ AND /i)`, which tore subqueries in half: the
198
+ * `AND` inside
199
+ * `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id = rebase.uid())`
200
+ * split the expression, and re-emitting the halves produced
201
+ * `(EXISTS (...) AND m.user_id = rebase.uid())`
202
+ * where `m` is no longer in scope — SQL that Postgres rejects outright with
203
+ * "missing FROM-clause entry for table". Returning null instead keeps such a
204
+ * clause as a `raw` expression, which round-trips verbatim.
205
+ */
206
+ function splitTopLevel(sql, keyword) {
207
+ const upper = sql.toUpperCase();
208
+ const parts = [];
209
+ let depth = 0;
210
+ let inString = false;
211
+ let start = 0;
212
+ for (let i = 0; i < sql.length; i++) {
213
+ const ch = sql[i];
214
+ if (inString) {
215
+ if (ch === "'") if (sql[i + 1] === "'") i++;
216
+ else inString = false;
217
+ continue;
218
+ }
219
+ if (ch === "'") {
220
+ inString = true;
221
+ continue;
222
+ }
223
+ if (ch === "(") {
224
+ depth++;
225
+ continue;
226
+ }
227
+ if (ch === ")") {
228
+ depth--;
229
+ continue;
230
+ }
231
+ if (depth === 0 && isKeywordAt(upper, i, keyword)) {
232
+ parts.push(sql.slice(start, i));
233
+ i += keyword.length - 1;
234
+ start = i + 1;
235
+ }
236
+ }
237
+ if (parts.length === 0) return null;
238
+ parts.push(sql.slice(start));
239
+ const trimmedParts = parts.map((p) => p.trim()).filter((p) => p.length > 0);
240
+ return trimmedParts.length > 1 ? trimmedParts : null;
241
+ }
242
+ /** `SELECT` as the first word of `sql`. */
243
+ var STARTS_WITH_SELECT = /^select(?=[\s(])/i;
244
+ /**
245
+ * Drop redundant wrapping parens (`(a AND b)` → `a AND b`), never `(a) AND (b)`
246
+ * — and never the parens of a scalar subquery: `(SELECT x)` without them is not
247
+ * an expression at all, and re-emitted bare it is a syntax error inside
248
+ * `USING (…)`.
249
+ */
250
+ function stripOuterParens(sql) {
251
+ let s = sql.trim();
252
+ while (wrapsWhole(s) && !STARTS_WITH_SELECT.test(s.slice(1).trim())) s = s.slice(1, -1).trim();
253
+ return s;
254
+ }
255
+ /** Whether `s` is one parenthesised group: `(a AND b)`, not `(a) AND (b)`. */
256
+ function wrapsWhole(s) {
257
+ if (!s.startsWith("(") || !s.endsWith(")")) return false;
258
+ let depth = 0;
259
+ let inString = false;
260
+ for (let i = 0; i < s.length; i++) {
261
+ const ch = s[i];
262
+ if (inString) {
263
+ if (ch === "'") if (s[i + 1] === "'") i++;
264
+ else inString = false;
265
+ continue;
266
+ }
267
+ if (ch === "'") {
268
+ inString = true;
269
+ continue;
270
+ }
271
+ if (ch === "(") depth++;
272
+ else if (ch === ")") {
273
+ depth--;
274
+ if (depth === 0 && i < s.length - 1) return false;
275
+ }
276
+ }
277
+ return true;
278
+ }
279
+ /** `x` when `s` is exactly `(SELECT x)`, otherwise null. */
280
+ function scalarSubqueryBody(s) {
281
+ if (!wrapsWhole(s)) return null;
282
+ const inner = s.slice(1, -1).trim();
283
+ return STARTS_WITH_SELECT.test(inner) ? inner.slice(6).trim() : null;
284
+ }
285
+ /** Whether any part of `expr` fell through to `raw`. */
286
+ function containsRaw(expr) {
287
+ switch (expr.kind) {
288
+ case "raw": return true;
289
+ case "and":
290
+ case "or": return expr.operands.some(containsRaw);
291
+ case "not": return containsRaw(expr.operand);
292
+ case "existsIn": return containsRaw(expr.where);
293
+ default: return false;
294
+ }
295
+ }
296
+ function sqlToPolicy(sql) {
297
+ const trimmed = stripOuterParens(rewriteLegacyRlsFunctions(sql).trim());
298
+ const subqueryBody = scalarSubqueryBody(trimmed);
299
+ if (subqueryBody !== null) {
300
+ const inner = sqlToPolicy(subqueryBody);
301
+ return containsRaw(inner) ? policy.raw(trimmed) : inner;
302
+ }
303
+ if (trimmed.toLowerCase() === "true") return policy.true();
304
+ if (trimmed.toLowerCase() === "false") return policy.false();
305
+ const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
306
+ if (overlapMatch) {
307
+ const roles = overlapMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
308
+ return policy.rolesOverlap(roles);
309
+ }
310
+ const containMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
311
+ if (containMatch) {
312
+ const roles = containMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
313
+ return policy.rolesContain(roles);
314
+ }
315
+ const orParts = splitTopLevel(trimmed, "OR");
316
+ if (orParts) return policy.or(...orParts.map(sqlToPolicy));
317
+ const andParts = splitTopLevel(trimmed, "AND");
318
+ if (andParts) return policy.and(...andParts.map(sqlToPolicy));
319
+ const match = trimmed.match(/^(.+?)\s*(!?=)\s*(.+)$/);
320
+ if (match) {
321
+ const [, leftStr, op, rightStr] = match;
322
+ const left = parseOperand(leftStr.trim());
323
+ const right = parseOperand(rightStr.trim());
324
+ if (left && right) return policy.compare(left, op === "=" ? "eq" : "neq", right);
325
+ }
326
+ return policy.raw(trimmed);
327
+ }
328
+ /**
329
+ * Literals from other BaaS platforms that people compare `rebase.uid()` against
330
+ * out of habit. Mirrors the driver's `FOREIGN_CONVENTION_ROLES` guard on
331
+ * `pgRoles`, one surface over: the same muscle memory inside a `using:` string
332
+ * is the more dangerous spelling, because it inverts a rule instead of
333
+ * emptying a table.
334
+ */
335
+ /**
336
+ * A `Map`, not an object literal.
337
+ *
338
+ * As `Record<string, string>` this was indexed with a literal taken straight
339
+ * out of a policy, so every key on `Object.prototype` answered: a rule
340
+ * comparing `rebase.uid()` to `"valueOf"`, `"toString"`, `"constructor"` or
341
+ * `"hasOwnProperty"` found a truthy "platform" and reported an anonymous-grant
342
+ * risk that does not exist — with the matched function interpolated into the
343
+ * explanation as the platform's name. A security warning that fires on
344
+ * innocent input is worse than none: it is what teaches people to skip the
345
+ * warnings that are real.
346
+ *
347
+ * Same shape as the prototype-pollution class swept out of `setIn`, `getIn`,
348
+ * `mergeDeep` and `unflattenObject` — a data-derived key reaching a plain
349
+ * object. Found by a property test, on the input `"valueOf"`.
350
+ */
351
+ var FOREIGN_CONVENTION_UIDS = /* @__PURE__ */ new Map([
352
+ ["anon", "Supabase"],
353
+ ["authenticated", "Supabase"],
354
+ ["service_role", "Supabase"]
355
+ ]);
356
+ new RegExp(String.raw`rebase\.uid\(\)\s*=\s*'(${[...FOREIGN_CONVENTION_UIDS.keys()].join("|")})'`, "i");
357
+ function parseOperand(str) {
358
+ if (/^current_setting\s*\(\s*'app\.(uid|user_id)'\s*\)$/i.test(str) || /^rebase\.uid\(\)$/i.test(str)) return policy.authUid();
359
+ if (/^\(\s*select\s+rebase\.uid\(\)(?:\s+as\s+"?uid"?)?\s*\)$/i.test(str)) return policy.authUid();
360
+ const literal = parseSingleQuoted(str);
361
+ if (literal !== null) return policy.literal(literal);
362
+ if (/^-?\d+$/.test(str)) return policy.literal(Number(str));
363
+ if (/^-?\d*\.\d+$/.test(str)) return policy.literal(Number(str));
364
+ if (/^true$/i.test(str)) return policy.literal(true);
365
+ if (/^false$/i.test(str)) return policy.literal(false);
366
+ if (/^null$/i.test(str)) return policy.literal(null);
367
+ if (/^\w+$/.test(str) && toSnakeCase(str) !== "") return policy.field(str);
368
+ return null;
369
+ }
370
+ /**
371
+ * Decode a single-quoted SQL literal, or null when `str` is not exactly one.
372
+ *
373
+ * Rejecting is as important as decoding: `'a' = 'b'` is two literals and an
374
+ * operator, not one literal whose body contains a quote, and a regex anchored
375
+ * on the outer quotes would happily read it as the latter. Every interior quote
376
+ * must therefore be part of a `''` pair.
377
+ */
378
+ function parseSingleQuoted(str) {
379
+ if (str.length < 2 || !str.startsWith("'") || !str.endsWith("'")) return null;
380
+ const body = str.slice(1, -1);
381
+ let out = "";
382
+ for (let i = 0; i < body.length; i++) {
383
+ if (body[i] !== "'") {
384
+ out += body[i];
385
+ continue;
386
+ }
387
+ if (body[i + 1] === "'") {
388
+ out += "'";
389
+ i++;
390
+ continue;
391
+ }
392
+ return null;
393
+ }
394
+ return out;
395
+ }
396
+ //#endregion
397
+ //#region ../common/src/util/policy/securityRuleToConditions.ts
398
+ /**
399
+ * Desugars a {@link SecurityRule} — its `access`/`ownerField`/`roles` shortcuts,
400
+ * structured `condition`/`check`, and raw `using`/`withCheck` — into a single
401
+ * normalized {@link PolicyExpression} pair.
402
+ *
403
+ * **This is the linchpin against drift:** both the Postgres DDL generators and
404
+ * the client-side evaluator consume this one function, so there is exactly one
405
+ * definition of what a rule means. In particular, application `roles` are folded
406
+ * into the expression here (AND'd with the base condition, matching how Postgres
407
+ * generates the clause) rather than being handled separately by each consumer.
408
+ */
409
+ function securityRuleToConditions(rule) {
410
+ return {
411
+ usingExpr: withRoles(baseUsing(rule), rule),
412
+ withCheckExpr: withRoles(baseWithCheck(rule), rule)
413
+ };
414
+ }
415
+ function baseUsing(rule) {
416
+ if (rule.condition) return rule.condition;
417
+ if (rule.using != null) return sqlToPolicy(rule.using);
418
+ if (rule.access === "public") return policy.true();
419
+ if (rule.ownerField) return policy.compare(policy.field(rule.ownerField), "eq", policy.authUid());
420
+ return null;
421
+ }
422
+ function baseWithCheck(rule) {
423
+ if (rule.check) return rule.check;
424
+ if (rule.withCheck != null) return sqlToPolicy(rule.withCheck);
425
+ return baseUsing(rule);
426
+ }
427
+ /**
428
+ * AND the base condition with an application-role check, or produce a roles-only
429
+ * condition when there is no base. Mirrors the Postgres generator so that a
430
+ * role-scoped restrictive rule denies exactly the same set of users on both
431
+ * sides.
432
+ */
433
+ function withRoles(base, rule) {
434
+ if (!rule.roles || rule.roles.length === 0) return base;
435
+ const rolesExpr = policy.rolesOverlap(rule.roles);
436
+ if (rule.mode === "restrictive") return base ? policy.or(policy.not(rolesExpr), base) : policy.not(rolesExpr);
437
+ return base ? policy.and(base, rolesExpr) : rolesExpr;
438
+ }
439
+ //#endregion
440
+ //#region ../common/src/util/policy/evaluatePolicy.ts
441
+ /**
442
+ * Evaluates a {@link PolicyExpression} against a user + row, using three-valued
443
+ * (Kleene) logic so that `"unknown"` sub-results propagate soundly.
444
+ *
445
+ * This is the JavaScript twin of {@link policyToPostgres}: both derive from the
446
+ * same expression, so the admin UI matches database enforcement by construction
447
+ * for every non-raw rule.
448
+ */
449
+ function evaluatePolicy(expr, ctx) {
450
+ switch (expr.kind) {
451
+ case "true": return true;
452
+ case "false": return false;
453
+ case "and": return kleeneAnd$1(expr.operands.map((o) => evaluatePolicy(o, ctx)));
454
+ case "or": return kleeneOr(expr.operands.map((o) => evaluatePolicy(o, ctx)));
455
+ case "not": return kleeneNot(evaluatePolicy(expr.operand, ctx));
456
+ case "compare": return evaluateCompare(expr.op, expr.left, expr.right, ctx);
457
+ case "rolesOverlap": {
458
+ const userRoles = ctx.roles ?? [];
459
+ return expr.roles.some((r) => r === "public" || userRoles.includes(r));
460
+ }
461
+ case "rolesContain": {
462
+ const userRoles = ctx.roles ?? [];
463
+ return expr.roles.every((r) => r === "public" || userRoles.includes(r));
464
+ }
465
+ case "authenticated": return ctx.uid != null && !isAnonymousUid(ctx.uid);
466
+ case "registered": return ctx.uid != null && !isAnonymousUid(ctx.uid) && ctx.isAnonymous !== true;
467
+ case "serverContext": return false;
468
+ case "existsIn": return "unknown";
469
+ case "raw": return "unknown";
470
+ }
471
+ }
472
+ function kleeneAnd$1(values) {
473
+ if (values.some((v) => v === false)) return false;
474
+ if (values.some((v) => v === "unknown")) return "unknown";
475
+ return true;
476
+ }
477
+ function kleeneOr(values) {
478
+ if (values.some((v) => v === true)) return true;
479
+ if (values.some((v) => v === "unknown")) return "unknown";
480
+ return false;
481
+ }
482
+ function kleeneNot(value) {
483
+ if (value === "unknown") return "unknown";
484
+ return !value;
485
+ }
486
+ function resolveOperand(operand, ctx) {
487
+ switch (operand.kind) {
488
+ case "literal": return {
489
+ known: true,
490
+ value: operand.value
491
+ };
492
+ case "authUid": return {
493
+ known: true,
494
+ value: ctx.uid ?? "anonymous"
495
+ };
496
+ case "authRoles": return {
497
+ known: true,
498
+ value: ctx.roles ?? []
499
+ };
500
+ case "field":
501
+ if (!ctx.entity) return { known: false };
502
+ return {
503
+ known: true,
504
+ value: ctx.entity.values[operand.name]
505
+ };
506
+ case "outerField": return { known: false };
507
+ case "authClaim": return { known: false };
508
+ }
509
+ }
510
+ function evaluateCompare(op, left, right, ctx) {
511
+ const l = resolveOperand(left, ctx);
512
+ const r = resolveOperand(right, ctx);
513
+ if (!l.known || !r.known) return "unknown";
514
+ const a = l.value;
515
+ const b = r.value;
516
+ if (a === null || b === null) return "unknown";
517
+ if (op === "eq") return a === b;
518
+ if (op === "neq") return a !== b;
519
+ if (typeof a === "string" && typeof b === "string") {
520
+ if (op === "lt") return a < b;
521
+ if (op === "lte") return a <= b;
522
+ if (op === "gt") return a > b;
523
+ if (op === "gte") return a >= b;
524
+ }
525
+ if (typeof a === "number" && typeof b === "number") {
526
+ if (op === "lt") return a < b;
527
+ if (op === "lte") return a <= b;
528
+ if (op === "gt") return a > b;
529
+ if (op === "gte") return a >= b;
530
+ }
531
+ if (typeof a === "bigint" && typeof b === "bigint") {
532
+ if (op === "lt") return a < b;
533
+ if (op === "lte") return a <= b;
534
+ if (op === "gt") return a > b;
535
+ if (op === "gte") return a >= b;
536
+ }
537
+ return "unknown";
538
+ }
539
+ //#endregion
540
+ //#region ../common/src/util/permissions.ts
541
+ /** Combine clause results with AND under three-valued (Kleene) logic. */
542
+ function kleeneAnd(values) {
543
+ if (values.some((v) => v === false)) return false;
544
+ if (values.some((v) => v === "unknown")) return "unknown";
545
+ return true;
546
+ }
547
+ /** The operations a rule covers, mirroring the Postgres generator's resolution. */
548
+ function ruleOperations(rule) {
549
+ return rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
550
+ }
551
+ function ruleApplies(rule, targetOperation) {
552
+ const ops = ruleOperations(rule);
553
+ return ops.includes(targetOperation) || ops.includes("all");
554
+ }
555
+ /**
556
+ * Evaluate a single rule for one operation, returning a tri-state.
557
+ *
558
+ * A `null` clause (the rule contributes no condition for a required clause)
559
+ * denies — matching Postgres, which emits `USING (false)` / `WITH CHECK (false)`
560
+ * in that case. USING applies to SELECT/UPDATE/DELETE; WITH CHECK to
561
+ * INSERT/UPDATE; both must pass for UPDATE.
562
+ */
563
+ function evaluateRuleForOperation(rule, ctx, targetOperation, clauses = "both") {
564
+ const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
565
+ const clause = (expr) => expr === null ? false : evaluatePolicy(expr, ctx);
566
+ const needsUsing = targetOperation !== "insert" && clauses !== "withCheck";
567
+ const needsWithCheck = (targetOperation === "insert" || targetOperation === "update") && clauses !== "using";
568
+ const results = [];
569
+ if (needsUsing) results.push(clause(usingExpr));
570
+ if (needsWithCheck) results.push(clause(withCheckExpr));
571
+ return kleeneAnd(results);
572
+ }
573
+ function resolveTriState(value, onUnknown) {
574
+ if (value === "unknown") return onUnknown === "allow";
575
+ return value;
576
+ }
577
+ /**
578
+ * Decide whether an operation is permitted for a user on a (possibly null) row,
579
+ * by evaluating the collection's security rules with the shared policy model —
580
+ * the same model compiled to Postgres RLS DDL, so the decision matches database
581
+ * enforcement for every non-raw rule.
582
+ *
583
+ * Engine-independent by design. `securityRules` are a declaration about the
584
+ * data, not about Postgres: the engine decides *who* enforces them (Postgres
585
+ * compiles them to RLS DDL, a document driver applies them in-process), never
586
+ * *whether* they hold. Gating this function on the engine's `supportsRLS`
587
+ * capability is what made every `{ onUnknown: "deny" }` call site in the Mongo
588
+ * driver return `true` before it evaluated anything — and it did so only for
589
+ * collections that spelled their engine out, so declaring `engine: "mongodb"`
590
+ * was what switched authorization off.
591
+ *
592
+ * @param options.onUnknown how to treat rules that cannot be decided
593
+ * client-side (raw SQL, or row predicates with no row). Defaults to `"allow"`
594
+ * for optimistic UI gating; enforcement callers should pass `"deny"`.
595
+ * @param options.clauses which half of each rule to evaluate. See
596
+ * {@link PolicyClauses}; defaults to `"both"`.
597
+ */
598
+ function checkOperation(collection, authContext, entity, targetOperation, options) {
599
+ const onUnknown = options?.onUnknown ?? "allow";
600
+ const clauses = options?.clauses ?? "both";
601
+ const securityRules = collection.securityRules;
602
+ if (!securityRules || securityRules.length === 0) return true;
603
+ const applicableRules = securityRules.filter((r) => ruleApplies(r, targetOperation));
604
+ if (applicableRules.length === 0) return false;
605
+ const ctx = {
606
+ uid: authContext.user?.uid,
607
+ roles: authContext.user?.roles ?? [],
608
+ isAnonymous: authContext.user?.isAnonymous === true,
609
+ entity
610
+ };
611
+ let grantedByPermissive = false;
612
+ let deniedByRestrictive = false;
613
+ let hasPermissive = false;
614
+ for (const rule of applicableRules) {
615
+ const mode = rule.mode || "permissive";
616
+ const passed = resolveTriState(evaluateRuleForOperation(rule, ctx, targetOperation, clauses), onUnknown);
617
+ if (mode === "restrictive") {
618
+ if (!passed) {
619
+ deniedByRestrictive = true;
620
+ break;
621
+ }
622
+ } else {
623
+ hasPermissive = true;
624
+ if (passed) grantedByPermissive = true;
625
+ }
626
+ }
627
+ if (deniedByRestrictive) return false;
628
+ return hasPermissive ? grantedByPermissive : false;
629
+ }
630
+ //#endregion
172
631
  //#region src/auth/api-keys/http-operation.ts
173
632
  /**
174
633
  * Map an HTTP method to the operation it performs.
@@ -1155,6 +1614,37 @@ function buildCollectionContext(ctx) {
1155
1614
  };
1156
1615
  }
1157
1616
  /**
1617
+ * The body field with which an administrator waives the password policy for
1618
+ * one create — an import of accounts whose passwords another system chose.
1619
+ * An instruction to the door, never data: it reaches neither the row nor a
1620
+ * create hook.
1621
+ */
1622
+ var SKIP_PASSWORD_POLICY_FIELD = "skipPasswordPolicy";
1623
+ /**
1624
+ * Hold a create body's chosen `password` to the password policy, as
1625
+ * registration and every other door that sets a password are, and return the
1626
+ * body without {@link SKIP_PASSWORD_POLICY_FIELD}.
1627
+ *
1628
+ * The flag waives the policy only when the door says the caller may waive it
1629
+ * (`options.mayWaivePasswordPolicy`, an administrator); from anyone else it
1630
+ * waives nothing, and a refusal says so.
1631
+ *
1632
+ * @throws ApiError 400 `WEAK_PASSWORD`
1633
+ */
1634
+ function enforceCreationPasswordPolicy(body, resolvedHooks, options) {
1635
+ const { [SKIP_PASSWORD_POLICY_FIELD]: skipRequested, ...rest } = body;
1636
+ const waived = skipRequested === true && options?.mayWaivePasswordPolicy === true;
1637
+ const password = rest.password;
1638
+ if (!waived && typeof password === "string" && password !== "") {
1639
+ const validation = resolvedHooks.validatePasswordStrength(password);
1640
+ if (!validation.valid) {
1641
+ const refusedWaiver = skipRequested === true ? `. \`${SKIP_PASSWORD_POLICY_FIELD}\` is honoured only for administrators and the service key` : "";
1642
+ throw ApiError.badRequest(`Password too weak: ${validation.errors.join(". ")}${refusedWaiver}`, "WEAK_PASSWORD");
1643
+ }
1644
+ }
1645
+ return rest;
1646
+ }
1647
+ /**
1158
1648
  * Prepare user values for an admin-initiated user creation.
1159
1649
  *
1160
1650
  * Resolution order:
@@ -1162,10 +1652,14 @@ function buildCollectionContext(ctx) {
1162
1652
  * 2. Backend-level `AuthHooks.onAdminCreateUser` — global override
1163
1653
  * 3. Built-in default — generate password → hash → normalize email
1164
1654
  *
1655
+ * Before any of them, a chosen password is held to the password policy
1656
+ * ({@link enforceCreationPasswordPolicy}).
1657
+ *
1165
1658
  * The caller is responsible for persisting (via `driver.save()`).
1166
1659
  */
1167
- async function prepareAdminUserValues(body, ctx) {
1660
+ async function prepareAdminUserValues(rawBody, ctx, options) {
1168
1661
  const { resolvedHooks, collectionAuthConfig } = ctx;
1662
+ const body = enforceCreationPasswordPolicy(rawBody, resolvedHooks, options);
1169
1663
  if (collectionAuthConfig?.onCreateUser) {
1170
1664
  const collectionCtx = buildCollectionContext(ctx);
1171
1665
  const hookResult = await collectionAuthConfig.onCreateUser(body, collectionCtx);
@@ -1662,6 +2156,30 @@ function extractBearerToken(authHeader) {
1662
2156
  }
1663
2157
  //#endregion
1664
2158
  //#region src/auth/access.ts
2159
+ /**
2160
+ * How a personal key finds its owner in an account store: the account as it
2161
+ * is now — a missing or disabled one is no owner — and its roles.
2162
+ *
2163
+ * One read where the store answers both at once: `getAccountAccessState`, the
2164
+ * statement every access token is already judged against, which reads roles
2165
+ * and `disabled_at` together. Otherwise the account, then its roles — the two
2166
+ * reads this always made, one round trip more per request.
2167
+ */
2168
+ function keyOwnerResolverFor(repo) {
2169
+ if (typeof repo.getAccountAccessState === "function") {
2170
+ const readAccount = repo.getAccountAccessState.bind(repo);
2171
+ return async (uid) => {
2172
+ const account = await readAccount(uid);
2173
+ if (!account || account.disabled) return null;
2174
+ return { roles: account.roles };
2175
+ };
2176
+ }
2177
+ return async (uid) => {
2178
+ const user = await repo.getUserById(uid);
2179
+ if (!user || user.disabled) return null;
2180
+ return { roles: await repo.getUserRoleIds(uid) };
2181
+ };
2182
+ }
1665
2183
  var configured = EMPTY_ACCESS_MODEL;
1666
2184
  var keyOwners;
1667
2185
  /** The access model the running backend serves. Built-ins only until boot configures it. */
@@ -1753,6 +2271,13 @@ function callerScopes(c) {
1753
2271
  if (!user) return [];
1754
2272
  return scopesForRoles(rolesOf(user), configured);
1755
2273
  }
2274
+ /**
2275
+ * Whether this request's caller holds the `admin` role. The service key signs
2276
+ * in as one.
2277
+ */
2278
+ function callerIsAdmin(c) {
2279
+ return hasAdminRole(rolesOf(c.get("user")));
2280
+ }
1756
2281
  /** Does this request's caller hold `scope`, on `target` when one is given? */
1757
2282
  function hasScope(c, scope, target) {
1758
2283
  return scopeGrants(callerScopes(c), scope, target);
@@ -2249,6 +2774,42 @@ function tryRequestedStorageObject(wildcard) {
2249
2774
  }
2250
2775
  }
2251
2776
  //#endregion
2777
+ //#region src/storage/request-principals.ts
2778
+ /**
2779
+ * The two principals a storage read can be granted without a user: the reader
2780
+ * of a declared-public object, and the bearer of a download token.
2781
+ *
2782
+ * `checkAuthorized` does not ask the `storageAuthorize` hook about either — the
2783
+ * public prefix and the token are the authorization. That skip used to be
2784
+ * decided on the principal's `uid` string (`"public"`, `"download-token"`), so
2785
+ * any identity carrying that uid skipped the hook: a custom `AuthAdapter` whose
2786
+ * `verifyRequest` returns `{ uid: "public" }` for a guest could then upload,
2787
+ * delete and list anywhere, with the hook never asked.
2788
+ *
2789
+ * Each principal is now one object, set only by the middleware that granted
2790
+ * it (`publicObjectAuth`, `fileTokenAuth`) and recognised by identity. An
2791
+ * adapter builds a new object per request, so it can name the uid but never be
2792
+ * the grant.
2793
+ */
2794
+ /** The principal `publicObjectAuth` grants an anonymous read of a public object. */
2795
+ var PUBLIC_OBJECT_PRINCIPAL = Object.freeze({
2796
+ uid: "public",
2797
+ roles: ["public"]
2798
+ });
2799
+ /** The principal `fileTokenAuth` grants a read covered by a valid download token. */
2800
+ var DOWNLOAD_TOKEN_PRINCIPAL = Object.freeze({
2801
+ uid: "download-token",
2802
+ roles: ["reader"]
2803
+ });
2804
+ /** Whether `user` is the grant `publicObjectAuth` made — the object, not the uid. */
2805
+ function isPublicObjectGrant(user) {
2806
+ return user === PUBLIC_OBJECT_PRINCIPAL;
2807
+ }
2808
+ /** Whether `user` is the grant `fileTokenAuth` made — the object, not the uid. */
2809
+ function isDownloadTokenGrant(user) {
2810
+ return user === DOWNLOAD_TOKEN_PRINCIPAL;
2811
+ }
2812
+ //#endregion
2252
2813
  //#region src/auth/middleware.ts
2253
2814
  /**
2254
2815
  * Answer a refusal with the canonical envelope, through the same formatter
@@ -2564,10 +3125,7 @@ var queryTokenAuth = async (c, next) => {
2564
3125
  var publicObjectAuth = async (c, next) => {
2565
3126
  if (c.get("user")) return next();
2566
3127
  const requested = tryRequestedStorageObject(storageRequestWildcard(c));
2567
- if (requested && isPublicStorageKey(requested.key)) c.set("user", {
2568
- uid: "public",
2569
- roles: ["public"]
2570
- });
3128
+ if (requested && isPublicStorageKey(requested.key)) c.set("user", PUBLIC_OBJECT_PRINCIPAL);
2571
3129
  return next();
2572
3130
  };
2573
3131
  /**
@@ -2638,10 +3196,7 @@ var fileTokenAuth = async (c, next) => {
2638
3196
  if (payload) {
2639
3197
  const outcome = evaluateGrant(payload);
2640
3198
  if (outcome === "grant") {
2641
- c.set("user", {
2642
- uid: "download-token",
2643
- roles: ["reader"]
2644
- });
3199
+ c.set("user", DOWNLOAD_TOKEN_PRINCIPAL);
2645
3200
  return next();
2646
3201
  }
2647
3202
  if (outcome !== "no-path") return denyMismatch(outcome);
@@ -2654,10 +3209,7 @@ var fileTokenAuth = async (c, next) => {
2654
3209
  if (payload) {
2655
3210
  const outcome = evaluateGrant(payload);
2656
3211
  if (outcome === "grant") {
2657
- c.set("user", {
2658
- uid: "download-token",
2659
- roles: ["reader"]
2660
- });
3212
+ c.set("user", DOWNLOAD_TOKEN_PRINCIPAL);
2661
3213
  return next();
2662
3214
  }
2663
3215
  if (outcome !== "no-path") return denyMismatch(outcome);
@@ -2920,6 +3472,18 @@ function assertSafeHeaders(headers) {
2920
3472
  if (/[\r\n]/.test(value)) throw new Error(`Invalid value for email header "${name}": header values cannot contain CR or LF. A newline here would end the header and begin another one.`);
2921
3473
  }
2922
3474
  }
3475
+ /** Connections a burst of mail may hold open at once, unless `smtp.maxConnections` says otherwise. */
3476
+ var DEFAULT_MAX_CONNECTIONS = 5;
3477
+ /**
3478
+ * How long the connection pool may sit unused before it is closed.
3479
+ *
3480
+ * Long enough that a burst — a team invited at once, a `Promise.all` over
3481
+ * `email.send` — reuses its connections; short enough that a quiet server
3482
+ * holds no socket open to the provider, and that a script which sends one
3483
+ * message and finishes is not kept alive by an idle connection for the ten
3484
+ * minutes nodemailer would otherwise wait.
3485
+ */
3486
+ var POOL_IDLE_CLOSE_MS = 5e3;
2923
3487
  /** `<abc@host>` → `abc@host`. See EmailSendResult.messageId for why. */
2924
3488
  function normalizeMessageId(value) {
2925
3489
  if (typeof value !== "string") return void 0;
@@ -2944,6 +3508,10 @@ var SMTPEmailService = class {
2944
3508
  * life of the process.
2945
3509
  */
2946
3510
  transporterReady = null;
3511
+ /** Sends between their start and their end — the pool is never closed under one. */
3512
+ sending = 0;
3513
+ /** Closes the pool once it has been unused for {@link POOL_IDLE_CLOSE_MS}. */
3514
+ idleTimer;
2947
3515
  constructor(config) {
2948
3516
  this.config = config;
2949
3517
  }
@@ -2976,10 +3544,15 @@ var SMTPEmailService = class {
2976
3544
  }
2977
3545
  }
2978
3546
  this.transporter = nodemailer.createTransport({
3547
+ pool: true,
3548
+ maxConnections: this.config.smtp.maxConnections ?? DEFAULT_MAX_CONNECTIONS,
2979
3549
  name: smtpName,
2980
3550
  host: this.config.smtp.host,
2981
3551
  port: this.config.smtp.port,
2982
3552
  secure: this.config.smtp.secure ?? this.config.smtp.port === 465,
3553
+ connectionTimeout: this.config.smtp.timeoutMs ?? 3e4,
3554
+ greetingTimeout: this.config.smtp.timeoutMs ?? 3e4,
3555
+ socketTimeout: this.config.smtp.timeoutMs ?? 3e4,
2983
3556
  auth: this.config.smtp.auth ? {
2984
3557
  user: this.config.smtp.auth.user,
2985
3558
  pass: this.config.smtp.auth.pass
@@ -2999,6 +3572,16 @@ var SMTPEmailService = class {
2999
3572
  async send(options) {
3000
3573
  assertSafeHeaders(options.headers);
3001
3574
  if (this.config.sendEmail) return await this.config.sendEmail(options) ?? {};
3575
+ this.sending++;
3576
+ clearTimeout(this.idleTimer);
3577
+ try {
3578
+ return await this.sendOverSmtp(options);
3579
+ } finally {
3580
+ this.sending--;
3581
+ this.closeWhenIdle();
3582
+ }
3583
+ }
3584
+ async sendOverSmtp(options) {
3002
3585
  await this.ensureTransporter();
3003
3586
  if (!this.transporter) throw new Error("Email service not configured. Provide SMTP config or sendEmail function.");
3004
3587
  const to = Array.isArray(options.to) ? options.to.join(", ") : options.to;
@@ -3023,6 +3606,35 @@ var SMTPEmailService = class {
3023
3606
  throw new Error(`Failed to send email: ${message}`);
3024
3607
  }
3025
3608
  }
3609
+ /** Arm the idle close once nothing is sending. */
3610
+ closeWhenIdle() {
3611
+ if (this.sending > 0 || !this.transporter) return;
3612
+ clearTimeout(this.idleTimer);
3613
+ this.idleTimer = setTimeout(() => {
3614
+ this.idleTimer = void 0;
3615
+ if (this.sending === 0) this.releaseTransporter();
3616
+ }, POOL_IDLE_CLOSE_MS);
3617
+ this.idleTimer.unref?.();
3618
+ }
3619
+ /**
3620
+ * Close the pool and forget it; the next send builds a fresh one. A closed
3621
+ * nodemailer pool accepts nothing more, so it cannot be kept and reused.
3622
+ */
3623
+ releaseTransporter() {
3624
+ const transporter = this.transporter;
3625
+ this.transporter = null;
3626
+ this.transporterReady = null;
3627
+ transporter?.close?.();
3628
+ }
3629
+ /**
3630
+ * Close the connection pool now, rather than when it next goes idle — for
3631
+ * a shutdown. A send after this opens a new pool.
3632
+ */
3633
+ close() {
3634
+ clearTimeout(this.idleTimer);
3635
+ this.idleTimer = void 0;
3636
+ if (this.sending === 0) this.releaseTransporter();
3637
+ }
3026
3638
  /**
3027
3639
  * Verify SMTP connection (useful for startup checks)
3028
3640
  */
@@ -3171,29 +3783,35 @@ var MemoryRateLimitStore = class {
3171
3783
  }
3172
3784
  async hit(key, windowMs, limit) {
3173
3785
  const now = this.now();
3174
- const timestamps = (this.store.get(key) ?? []).filter((t) => now - t < windowMs);
3175
- if (timestamps.length >= limit) {
3176
- this.store.set(key, timestamps);
3177
- return {
3178
- allowed: false,
3179
- remaining: 0,
3180
- retryAfterMs: Math.max(0, timestamps[0] + windowMs - now)
3786
+ let window = this.store.get(key);
3787
+ if (!window) {
3788
+ window = {
3789
+ stamps: [],
3790
+ head: 0,
3791
+ ordered: true
3181
3792
  };
3793
+ this.store.set(key, window);
3182
3794
  }
3183
- timestamps.push(now);
3184
- this.store.set(key, timestamps);
3795
+ expire(window, now, windowMs);
3796
+ const live = window.stamps.length - window.head;
3797
+ if (live >= limit) return {
3798
+ allowed: false,
3799
+ remaining: 0,
3800
+ retryAfterMs: Math.max(0, window.stamps[window.head] + windowMs - now)
3801
+ };
3802
+ if (live > 0 && now < window.stamps[window.stamps.length - 1]) window.ordered = false;
3803
+ window.stamps.push(now);
3185
3804
  return {
3186
3805
  allowed: true,
3187
- remaining: limit - timestamps.length,
3806
+ remaining: limit - (live + 1),
3188
3807
  retryAfterMs: 0
3189
3808
  };
3190
3809
  }
3191
3810
  sweep(windowMs) {
3192
3811
  const now = this.now();
3193
- for (const [key, timestamps] of this.store.entries()) {
3194
- const live = timestamps.filter((t) => now - t < windowMs);
3195
- if (live.length === 0) this.store.delete(key);
3196
- else this.store.set(key, live);
3812
+ for (const [key, window] of this.store.entries()) {
3813
+ expire(window, now, windowMs);
3814
+ if (window.head === window.stamps.length) this.store.delete(key);
3197
3815
  }
3198
3816
  }
3199
3817
  dispose() {
@@ -3202,6 +3820,25 @@ var MemoryRateLimitStore = class {
3202
3820
  this.store.clear();
3203
3821
  }
3204
3822
  };
3823
+ /** Drop the hits at least `windowMs` old — `now - t < windowMs` stays, as before. */
3824
+ function expire(window, now, windowMs) {
3825
+ if (!window.ordered) {
3826
+ window.stamps = window.stamps.slice(window.head).filter((t) => now - t < windowMs);
3827
+ window.head = 0;
3828
+ if (window.stamps.length === 0) window.ordered = true;
3829
+ return;
3830
+ }
3831
+ const { stamps } = window;
3832
+ let head = window.head;
3833
+ while (head < stamps.length && now - stamps[head] >= windowMs) head++;
3834
+ if (head === stamps.length) {
3835
+ window.stamps = [];
3836
+ window.head = 0;
3837
+ } else if (head >= 32 && head * 2 >= stamps.length) {
3838
+ window.stamps = stamps.slice(head);
3839
+ window.head = 0;
3840
+ } else window.head = head;
3841
+ }
3205
3842
  //#endregion
3206
3843
  //#region src/auth/rate-limiter.ts
3207
3844
  /**
@@ -3269,12 +3906,15 @@ function namedStore(name, windowMs) {
3269
3906
  */
3270
3907
  function createRateLimiter(options = {}) {
3271
3908
  const trustedProxyHops = resolveTrustedProxyHops(options.trustedProxyHops);
3272
- 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;
3909
+ 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, cost } = options;
3273
3910
  return async (c, next) => {
3274
3911
  const effectiveLimit = resolveLimit ? await resolveLimit(c) : limit;
3275
3912
  if (effectiveLimit === null) return next();
3276
3913
  const activeLimit = effectiveLimit ?? limit;
3277
- const decision = await store.hit(await keyGenerator(c), windowMs, activeLimit);
3914
+ const key = await keyGenerator(c);
3915
+ let decision = await store.hit(key, windowMs, activeLimit);
3916
+ const charge = cost ? Math.max(1, Math.floor(await cost(c))) : 1;
3917
+ for (let hit = 1; hit < charge && decision.allowed; hit++) decision = await store.hit(key, windowMs, activeLimit);
3278
3918
  c.header("X-RateLimit-Limit", String(activeLimit));
3279
3919
  c.header("X-RateLimit-Remaining", String(decision.remaining));
3280
3920
  if (!decision.allowed) {
@@ -3496,7 +4136,7 @@ var DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT = 3e3;
3496
4136
  * set them, and one that already has a proxy doing this should pass
3497
4137
  * `enabled: false` rather than pay for it twice.
3498
4138
  */
3499
- function createDataRateLimiter(config = {}) {
4139
+ function createDataRateLimiter(config = {}, options = {}) {
3500
4140
  const { windowMs = 900 * 1e3, store = new MemoryRateLimitStore(windowMs) } = config;
3501
4141
  const trustedProxyHops = resolveTrustedProxyHops(config.trustedProxyHops);
3502
4142
  const bucketFor = dataRateLimitBuckets(config);
@@ -3540,7 +4180,16 @@ function createDataRateLimiter(config = {}) {
3540
4180
  const token = extractBearerToken(c.req.header("authorization"));
3541
4181
  return token !== void 0 && safeCompare(token, config.serviceKey);
3542
4182
  };
3543
- const bucketOf = async (c) => {
4183
+ const bucketsByRequest = /* @__PURE__ */ new WeakMap();
4184
+ const bucketOf = (c) => {
4185
+ let bucket = bucketsByRequest.get(c);
4186
+ if (!bucket) {
4187
+ bucket = resolveBucket(c);
4188
+ bucketsByRequest.set(c, bucket);
4189
+ }
4190
+ return bucket;
4191
+ };
4192
+ const resolveBucket = async (c) => {
3544
4193
  if (c.get("user")?.uid === SERVICE_IDENTITY.uid) return null;
3545
4194
  if (isServiceKey(c)) return null;
3546
4195
  const apiKey = c.get("apiKey");
@@ -3555,7 +4204,8 @@ function createDataRateLimiter(config = {}) {
3555
4204
  store,
3556
4205
  message: "Too many requests, please try again later.",
3557
4206
  keyGenerator: async (c) => (await bucketOf(c))?.key ?? `ip:${defaultKeyGenerator(c, trustedProxyHops)}`,
3558
- resolveLimit: async (c) => (await bucketOf(c))?.limit ?? null
4207
+ resolveLimit: async (c) => (await bucketOf(c))?.limit ?? null,
4208
+ cost: options.cost
3559
4209
  });
3560
4210
  }
3561
4211
  /**
@@ -4455,7 +5105,7 @@ function mountMfaRoutes(opts) {
4455
5105
  router.delete("/mfa/unenroll", strictAuthLimiter, requireLiveSession, async (c) => {
4456
5106
  const userCtx = c.get("user");
4457
5107
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
4458
- if (userCtx.aal !== "aal2") throw ApiError.forbidden("MFA verification required to unenroll. Please re-authenticate with your second factor.", "AAL2_REQUIRED");
5108
+ await requireStepUpForFactorChange(userCtx);
4459
5109
  const unenrollSchema = z.object({ factorId: z.string().min(1, "Factor ID is required") });
4460
5110
  const { factorId } = parseBody(unenrollSchema, await c.req.json());
4461
5111
  const factor = await authRepo.getMfaFactorById(factorId);
@@ -4534,6 +5184,31 @@ function isBootstrapWindowOpen(env = process.env) {
4534
5184
  return env.NODE_ENV !== "production";
4535
5185
  }
4536
5186
  /**
5187
+ * Whether the users table is empty — the question behind the bootstrap window.
5188
+ *
5189
+ * Through `AuthRepository.firstUserIds` when the repository has it: one
5190
+ * unordered `LIMIT 1` read. Otherwise through `listUsersPaginated`, which also
5191
+ * counts and sorts the whole table, so it is only the fallback.
5192
+ */
5193
+ async function isUserTableEmpty(repo) {
5194
+ if (repo.firstUserIds) return (await repo.firstUserIds(1)).length === 0;
5195
+ const { total } = await repo.listUsersPaginated({ limit: 1 });
5196
+ return total === 0;
5197
+ }
5198
+ /**
5199
+ * Whether the account just created is the only one there is. Two ids at most:
5200
+ * whether anyone else exists, and, when nobody does, whether it is this one.
5201
+ * Same fallback as {@link isUserTableEmpty}.
5202
+ */
5203
+ async function isOnlyUser(repo, uid) {
5204
+ if (repo.firstUserIds) {
5205
+ const ids = await repo.firstUserIds(2);
5206
+ return ids.length === 1 && ids[0] === uid;
5207
+ }
5208
+ const { users, total } = await repo.listUsersPaginated({ limit: 2 });
5209
+ return total === 1 && users[0]?.id === uid;
5210
+ }
5211
+ /**
4537
5212
  * What to tell a caller who hit an empty production database with the window
4538
5213
  * shut. Named here so the register route and the bootstrap route say the same
4539
5214
  * thing.
@@ -4885,7 +5560,7 @@ function mountSessionRoutes(opts) {
4885
5560
  if (!signedOutUid) {
4886
5561
  const accessToken = extractBearerToken(c.req.header("authorization"));
4887
5562
  if (accessToken !== void 0) {
4888
- const { verifyAccessToken } = await import("./jwt-R6bSPMjk.js").then((n) => n._);
5563
+ const { verifyAccessToken } = await import("./jwt-Bqcu7cGt.js").then((n) => n._);
4889
5564
  signedOutUid = (await verifyAccessToken(accessToken))?.uid;
4890
5565
  }
4891
5566
  }
@@ -5692,18 +6367,6 @@ function createAuthRoutes(config) {
5692
6367
  return dummyHash;
5693
6368
  }
5694
6369
  /**
5695
- * Is the account just created the only one there is?
5696
- *
5697
- * Two rows at most: the total says whether anyone else exists, and the
5698
- * one row left when nobody does says whether it is this account. It used
5699
- * to be `listUsers()` — the whole users table, password hashes included,
5700
- * on every sign-up, to compare its length with one.
5701
- */
5702
- async function isOnlyUser(uid) {
5703
- const { users, total } = await authRepo.listUsersPaginated({ limit: 2 });
5704
- return total === 1 && users[0]?.id === uid;
5705
- }
5706
- /**
5707
6370
  * Helper to generate and store session tokens.
5708
6371
  *
5709
6372
  * Every route that signs somebody in comes through here — password login,
@@ -5760,8 +6423,7 @@ function createAuthRoutes(config) {
5760
6423
  if (config.disableSelfRegistration) throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
5761
6424
  let bootstrapRegistration = false;
5762
6425
  if (!isRegistrationAllowed()) {
5763
- const { total } = await authRepo.listUsersPaginated({ limit: 1 });
5764
- bootstrapRegistration = total === 0;
6426
+ bootstrapRegistration = await isUserTableEmpty(authRepo);
5765
6427
  if (!bootstrapRegistration) throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
5766
6428
  if (!isBootstrapWindowOpen()) throw ApiError.forbidden(SETUP_REQUIRED_MESSAGE, "SETUP_REQUIRED");
5767
6429
  }
@@ -5788,7 +6450,7 @@ function createAuthRoutes(config) {
5788
6450
  };
5789
6451
  if (ops.beforeUserCreate) createData = await ops.beforeUserCreate(createData);
5790
6452
  const user = await authRepo.createUser(createData);
5791
- const isFirstUser = await isOnlyUser(user.id);
6453
+ const isFirstUser = await isOnlyUser(authRepo, user.id);
5792
6454
  if (bootstrapRegistration && !isFirstUser) {
5793
6455
  await authRepo.deleteUser(user.id);
5794
6456
  throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
@@ -5931,8 +6593,7 @@ function createAuthRoutes(config) {
5931
6593
  if (config.disableSelfRegistration) throw ApiError.forbidden(noSignupsMessage, "REGISTRATION_DISABLED");
5932
6594
  let bootstrapRegistration = false;
5933
6595
  if (!isRegistrationAllowed()) {
5934
- const { total } = await authRepo.listUsersPaginated({ limit: 1 });
5935
- bootstrapRegistration = total === 0;
6596
+ bootstrapRegistration = await isUserTableEmpty(authRepo);
5936
6597
  if (!bootstrapRegistration) throw ApiError.forbidden(noSignupsMessage, "REGISTRATION_DISABLED");
5937
6598
  if (!isBootstrapWindowOpen()) throw ApiError.forbidden(SETUP_REQUIRED_MESSAGE, "SETUP_REQUIRED");
5938
6599
  }
@@ -5950,7 +6611,7 @@ function createAuthRoutes(config) {
5950
6611
  } catch (err) {
5951
6612
  logger.error("[AuthHooks] afterUserCreate error", { error: err instanceof Error ? err.message : err });
5952
6613
  }
5953
- const isFirstUser = await isOnlyUser(user.id);
6614
+ const isFirstUser = await isOnlyUser(authRepo, user.id);
5954
6615
  if (bootstrapRegistration && !isFirstUser) {
5955
6616
  await authRepo.deleteUser(user.id);
5956
6617
  throw ApiError.forbidden(noSignupsMessage, "REGISTRATION_DISABLED");
@@ -6499,7 +7160,7 @@ function createResetPasswordRoute(config) {
6499
7160
  const chosenPassword = body.password ? body.password : void 0;
6500
7161
  if (chosenPassword) {
6501
7162
  const validation = ops.validatePasswordStrength(chosenPassword);
6502
- if (!validation.valid) throw ApiError.badRequest(`Password too weak: ${validation.errors.join(", ")}`);
7163
+ if (!validation.valid) throw ApiError.badRequest(`Password too weak: ${validation.errors.join(", ")}`, "WEAK_PASSWORD");
6503
7164
  } else if (collectionAuthConfig?.onResetPassword) {
6504
7165
  const isEmailConfigured = !!(emailService && emailService.isConfigured());
6505
7166
  const hookResult = await collectionAuthConfig.onResetPassword(existing.id, {
@@ -6595,12 +7256,101 @@ function createAdminRolesRoute(config) {
6595
7256
  return router;
6596
7257
  }
6597
7258
  //#endregion
7259
+ //#region src/auth/user-picking.ts
7260
+ var collectionsForPicking = () => [];
7261
+ /** Tell the picker which collections this backend serves. Called once at boot. */
7262
+ function configureUserPicking(collections) {
7263
+ collectionsForPicking = collections;
7264
+ }
7265
+ function propertyReferencesUsers(property) {
7266
+ if (!property) return false;
7267
+ if (property.type === "string" && property.userSelect === true) return true;
7268
+ if (property.type === "map") return propertiesReferenceUsers(property.properties);
7269
+ if (property.type === "array") {
7270
+ const of = property.of;
7271
+ if (Array.isArray(of)) return of.some(propertyReferencesUsers);
7272
+ if (of) return propertyReferencesUsers(of);
7273
+ return propertiesReferenceUsers(property.oneOf?.properties);
7274
+ }
7275
+ return false;
7276
+ }
7277
+ /** Whether any property, at any depth, is a `userSelect` field. */
7278
+ function propertiesReferenceUsers(properties) {
7279
+ return !!properties && Object.values(properties).some(propertyReferencesUsers);
7280
+ }
7281
+ /** Whether any part of a policy is raw SQL the evaluator cannot decide. */
7282
+ function containsRawSql(expr) {
7283
+ if (!expr) return false;
7284
+ switch (expr.kind) {
7285
+ case "raw": return true;
7286
+ case "and":
7287
+ case "or": return expr.operands.some(containsRawSql);
7288
+ case "not": return containsRawSql(expr.operand);
7289
+ case "existsIn": return containsRawSql(expr.where);
7290
+ default: return false;
7291
+ }
7292
+ }
7293
+ /**
7294
+ * The rule with each clause that holds raw SQL read as `false`, so an
7295
+ * undecidable rule never grants (permissive) and always refuses
7296
+ * (restrictive). The clauses are carried already desugared — roles folded
7297
+ * in — so nothing else in the rule is read again.
7298
+ */
7299
+ function failClosedOnRawSql(rule) {
7300
+ const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
7301
+ if (!containsRawSql(usingExpr) && !containsRawSql(withCheckExpr)) return rule;
7302
+ const clause = (expr) => expr === null || containsRawSql(expr) ? policy.false() : expr;
7303
+ return {
7304
+ operation: rule.operation,
7305
+ operations: rule.operations,
7306
+ mode: rule.mode,
7307
+ name: rule.name,
7308
+ condition: clause(usingExpr),
7309
+ check: clause(withCheckExpr)
7310
+ };
7311
+ }
7312
+ /** The signed-in person behind a request, as the policy evaluator reads one. */
7313
+ function personOf(c) {
7314
+ const user = c.get("user");
7315
+ if (typeof user !== "object" || user === null) return null;
7316
+ const uid = typeof user.uid === "string" ? user.uid : void 0;
7317
+ if (!uid || isAnonymousUid(uid)) return null;
7318
+ if ("isAnonymous" in user && user.isAnonymous === true) return null;
7319
+ return {
7320
+ uid,
7321
+ roles: Array.isArray(user.roles) ? user.roles.filter((role) => typeof role === "string") : [],
7322
+ isAnonymous: false,
7323
+ displayName: null,
7324
+ email: null,
7325
+ photoURL: null,
7326
+ providerId: "custom"
7327
+ };
7328
+ }
7329
+ /**
7330
+ * Whether this request's caller may read the user picker: they hold
7331
+ * `users:read`, or they may write a collection with a `userSelect` field.
7332
+ */
7333
+ function mayPickUsers(c) {
7334
+ if (hasScope(c, "users:read")) return true;
7335
+ const person = personOf(c);
7336
+ if (!person) return false;
7337
+ return collectionsForPicking().some((collection) => {
7338
+ if (!propertiesReferenceUsers(collection.properties) || !hasScope(c, "data:write", collection.slug)) return false;
7339
+ const judged = collection.securityRules ? {
7340
+ ...collection,
7341
+ securityRules: collection.securityRules.map(failClosedOnRawSql)
7342
+ } : collection;
7343
+ return checkOperation(judged, { user: person }, null, "insert") || checkOperation(judged, { user: person }, null, "update");
7344
+ });
7345
+ }
7346
+ //#endregion
6598
7347
  //#region src/auth/admin-users-route.ts
6599
7348
  /**
6600
7349
  * Standalone admin endpoint for user management.
6601
7350
  *
6602
7351
  * Mounts:
6603
7352
  * GET /users
7353
+ * GET /users/pick
6604
7354
  * GET /users/:uid
6605
7355
  * POST /users
6606
7356
  * PUT /users/:uid
@@ -6732,6 +7482,48 @@ function createAdminUsersRoute(config) {
6732
7482
  offset: result.offset
6733
7483
  });
6734
7484
  });
7485
+ /**
7486
+ * The user picker's read: who a `userSelect` field can name, as id, name,
7487
+ * email and avatar — nothing an administrator's list carries beyond that.
7488
+ * Open to anyone who may write a collection with such a field, as well as
7489
+ * to `users:read` ({@link mayPickUsers}). Registered before `/users/:uid`,
7490
+ * which would otherwise take `pick` for an id.
7491
+ *
7492
+ * `?ids=a,b` resolves the users a stored value names; otherwise a page of
7493
+ * `?search=`, `?limit=` (25 by default) and `?offset=`.
7494
+ */
7495
+ router.get("/users/pick", async (c) => {
7496
+ if (!mayPickUsers(c)) throw new ApiError(403, "SCOPE_MISSING", "Picking a user needs write access to a collection with a user field, or the \"users:read\" scope.", { requiredScope: "users:read" });
7497
+ const toPickedUser = (u) => ({
7498
+ uid: u.id,
7499
+ email: u.email,
7500
+ displayName: u.displayName ?? null,
7501
+ photoURL: u.photoUrl ?? null
7502
+ });
7503
+ const idsParam = c.req.query("ids");
7504
+ if (idsParam !== void 0) {
7505
+ const ids = [...new Set(idsParam.split(",").map((id) => id.trim()).filter(Boolean))].slice(0, MAX_USER_IDS_PER_LOOKUP);
7506
+ const users = (await Promise.all(ids.map((id) => authRepo.getUserById(id)))).filter((u) => !!u).map(toPickedUser);
7507
+ return c.json({
7508
+ users,
7509
+ limit: ids.length,
7510
+ offset: 0
7511
+ });
7512
+ }
7513
+ const limit = resolveListLimitParam(c.req.query("limit"), { defaultLimit: 25 });
7514
+ const offsetParam = c.req.query("offset");
7515
+ const offset = offsetParam ? Math.max(0, parseInt(offsetParam, 10) || 0) : 0;
7516
+ const result = await authRepo.listUsersPaginated({
7517
+ limit,
7518
+ offset,
7519
+ search: c.req.query("search") || void 0
7520
+ });
7521
+ return c.json({
7522
+ users: result.users.map(toPickedUser),
7523
+ limit: result.limit,
7524
+ offset: result.offset
7525
+ });
7526
+ });
6735
7527
  router.get("/users/:uid", requireScope("users:read"), async (c) => {
6736
7528
  const uid = c.req.param("uid");
6737
7529
  const result = await authRepo.getUserWithRoles(uid);
@@ -6751,7 +7543,7 @@ function createAdminUsersRoute(config) {
6751
7543
  emailConfig,
6752
7544
  resolvedHooks: ops,
6753
7545
  collectionAuthConfig
6754
- });
7546
+ }, { mayWaivePasswordPolicy: callerIsAdmin(c) });
6755
7547
  let createData = {
6756
7548
  email: prepResult.values.email,
6757
7549
  passwordHash: prepResult.values.passwordHash,
@@ -6803,7 +7595,7 @@ function createAdminUsersRoute(config) {
6803
7595
  let passwordHash;
6804
7596
  if (password) {
6805
7597
  const validation = ops.validatePasswordStrength(password);
6806
- if (!validation.valid) throw ApiError.badRequest(`Password too weak: ${validation.errors.join(". ")}`);
7598
+ if (!validation.valid) throw ApiError.badRequest(`Password too weak: ${validation.errors.join(". ")}`, "WEAK_PASSWORD");
6807
7599
  passwordHash = await ops.hashPassword(password);
6808
7600
  }
6809
7601
  if (Object.keys(updates).length > 0) await authRepo.updateUser(uid, updates);
@@ -7060,14 +7852,14 @@ function createBuiltinAuthAdapter(config) {
7060
7852
  router.route("/", adminUsersRoute);
7061
7853
  return router;
7062
7854
  },
7063
- async prepareUserCreation(values, collectionAuth) {
7855
+ async prepareUserCreation(values, collectionAuth, options) {
7064
7856
  return prepareAdminUserValues(values, {
7065
7857
  authRepo: authRepository,
7066
7858
  emailService,
7067
7859
  emailConfig,
7068
7860
  resolvedHooks: resolvedOps,
7069
7861
  collectionAuthConfig: collectionAuth ?? collectionAuthConfig
7070
- });
7862
+ }, options);
7071
7863
  },
7072
7864
  describeUserCreationContract(collectionAuth) {
7073
7865
  if ((collectionAuth ?? collectionAuthConfig)?.onCreateUser || resolvedOps.onAdminCreateUser) return {
@@ -7076,7 +7868,8 @@ function createBuiltinAuthAdapter(config) {
7076
7868
  };
7077
7869
  return {
7078
7870
  validate: true,
7079
- extraFields: ["password"]
7871
+ extraFields: ["password", SKIP_PASSWORD_POLICY_FIELD],
7872
+ independent: !authHooks?.hashPassword
7080
7873
  };
7081
7874
  },
7082
7875
  async finalizeUserCreation(entity, clearPassword) {
@@ -7095,7 +7888,7 @@ function createBuiltinAuthAdapter(config) {
7095
7888
  async getCapabilities() {
7096
7889
  let needsSetup = false;
7097
7890
  try {
7098
- needsSetup = (await authRepository.listUsersPaginated({ limit: 1 })).total === 0 && isBootstrapWindowOpen();
7891
+ needsSetup = isBootstrapWindowOpen() && await isUserTableEmpty(authRepository);
7099
7892
  } catch (error) {
7100
7893
  logger.warn("[Auth] Could not count users for `needsSetup`; reporting the backend as already set up.", { error: error instanceof Error ? error.message : String(error) });
7101
7894
  }
@@ -8500,6 +9293,78 @@ function scopesFromStoredPermissions(permissions, admin) {
8500
9293
  */
8501
9294
  var TABLE = "rebase.api_keys";
8502
9295
  /**
9296
+ * Drops `rebase.idx_api_keys_hash` — a plain btree on `key_hash`, next to the
9297
+ * UNIQUE (key_hash) constraint's own index — but only while a valid, unique,
9298
+ * non-partial btree on the table leads with the same column, so a lookup is
9299
+ * never left without one. Under a one-second `lock_timeout`, local to this
9300
+ * statement's transaction: `DROP INDEX` takes the table's ACCESS EXCLUSIVE lock.
9301
+ * A no-op read once the index is gone.
9302
+ */
9303
+ var DROP_DUPLICATE_KEY_HASH_INDEX_SQL = `
9304
+ DO $rebase_drop_hash_index$
9305
+ BEGIN
9306
+ IF EXISTS (
9307
+ SELECT 1
9308
+ FROM pg_index d
9309
+ JOIN pg_class ic ON ic.oid = d.indexrelid
9310
+ JOIN pg_namespace n ON n.oid = ic.relnamespace
9311
+ WHERE n.nspname = 'rebase' AND ic.relname = 'idx_api_keys_hash' AND d.indnatts = 1
9312
+ AND EXISTS (
9313
+ SELECT 1 FROM pg_index o
9314
+ JOIN pg_class oc ON oc.oid = o.indexrelid
9315
+ JOIN pg_am am ON am.oid = oc.relam
9316
+ WHERE o.indrelid = d.indrelid AND o.indexrelid <> d.indexrelid
9317
+ AND o.indisvalid AND o.indisunique AND o.indpred IS NULL AND o.indexprs IS NULL
9318
+ AND am.amname = 'btree' AND o.indkey[0] = d.indkey[0])
9319
+ ) THEN
9320
+ PERFORM set_config('lock_timeout', '1000ms', true);
9321
+ DROP INDEX IF EXISTS rebase.idx_api_keys_hash;
9322
+ END IF;
9323
+ END
9324
+ $rebase_drop_hash_index$`.trim();
9325
+ /** The relations `ensureTable` creates in `rebase`: the table and its two indexes. */
9326
+ var BOOT_RELATIONS = [
9327
+ "api_keys",
9328
+ "idx_api_keys_prefix",
9329
+ "idx_api_keys_owner"
9330
+ ];
9331
+ /** The columns `ensureTable` adds to a table created before scopes. */
9332
+ var BOOT_COLUMNS = [
9333
+ "kind",
9334
+ "scopes",
9335
+ "roles",
9336
+ "owner_uid"
9337
+ ];
9338
+ /**
9339
+ * One catalogue read answering whether `ensureTable` has anything to do. It
9340
+ * returns one row: whether the table, its indexes and the scope columns are
9341
+ * there, whether the duplicate hash index and the pre-scopes `permissions`
9342
+ * column are gone, and whether the end-user role still holds a privilege (the
9343
+ * same test `revokeInternalTableSql` makes before it revokes). `readable` is
9344
+ * the connection's own access, the question `isReadable` asks.
9345
+ */
9346
+ var BOOT_STATE_SQL = (() => {
9347
+ const list = (names) => names.map((name) => `'${name}'`).join(", ");
9348
+ const target = `(SELECT oid FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')`;
9349
+ const columnCount = (names) => `(SELECT count(*) FROM pg_attribute a WHERE a.attrelid = to_regclass('${TABLE}') AND a.attnum > 0 AND NOT a.attisdropped AND a.attname IN (${list(names)}))::int`;
9350
+ return `
9351
+ SELECT
9352
+ (SELECT count(*) FROM pg_class c WHERE c.relnamespace = to_regnamespace('rebase')
9353
+ AND c.relname IN (${list(BOOT_RELATIONS)}))::int AS present,
9354
+ (SELECT count(*) FROM pg_class c WHERE c.relnamespace = to_regnamespace('rebase')
9355
+ AND c.relname = 'idx_api_keys_hash')::int AS stale,
9356
+ ${columnCount(BOOT_COLUMNS)} AS columns,
9357
+ ${columnCount(["permissions"])} AS legacy,
9358
+ (SELECT count(*) FROM pg_class c
9359
+ WHERE c.oid = to_regclass('${TABLE}') AND NOT c.relrowsecurity AND ${target} IS NOT NULL
9360
+ AND (c.relacl IS NULL
9361
+ OR EXISTS (SELECT 1 FROM aclexplode(c.relacl) a WHERE a.grantee = ${target})
9362
+ OR EXISTS (SELECT 1 FROM pg_attribute att, aclexplode(att.attacl) a
9363
+ WHERE att.attrelid = c.oid AND att.attacl IS NOT NULL AND a.grantee = ${target})))::int AS granted,
9364
+ (has_schema_privilege('rebase', 'USAGE') AND has_table_privilege(to_regclass('${TABLE}'), 'SELECT')) AS readable
9365
+ WHERE to_regnamespace('rebase') IS NOT NULL`.trim();
9366
+ })();
9367
+ /**
8503
9368
  * Generate a plaintext API key with the `rk_live_` prefix.
8504
9369
  *
8505
9370
  * Format: `rk_live_` + 32 random hex characters.
@@ -8609,8 +9474,29 @@ function createApiKeyStore(driver) {
8609
9474
  }
8610
9475
  if (rows.length > 0) logger.info(`[api-key-store] Gave ${rows.length} stored API key(s) their scopes.`);
8611
9476
  }
9477
+ /**
9478
+ * Whether a boot has nothing to do, from {@link BOOT_STATE_SQL}. Every
9479
+ * statement below is a no-op on such a database, but not a free one: each
9480
+ * `ADD COLUMN IF NOT EXISTS` queues for the table's ACCESS EXCLUSIVE lock
9481
+ * before it finds the column there, each `CREATE INDEX IF NOT EXISTS` takes
9482
+ * a SHARE lock, and the `REVOKE` rewrites the table's catalogue row. Anything
9483
+ * missing, granted or legacy — or a read that fails, as it does on a driver
9484
+ * that is not Postgres — runs the full bootstrap, exactly as before.
9485
+ */
9486
+ async function bootIsCurrent() {
9487
+ try {
9488
+ const row = (await exec(BOOT_STATE_SQL))[0];
9489
+ return row !== void 0 && Number(row.present) === BOOT_RELATIONS.length && Number(row.stale) === 0 && Number(row.columns) === BOOT_COLUMNS.length && Number(row.legacy) === 0 && Number(row.granted) === 0 && row.readable === true;
9490
+ } catch {
9491
+ return false;
9492
+ }
9493
+ }
8612
9494
  return {
8613
9495
  async ensureTable() {
9496
+ if (await bootIsCurrent()) {
9497
+ logger.debug("✅ API keys table ready");
9498
+ return;
9499
+ }
8614
9500
  await ddl.ensureObject("Creating schema rebase", "CREATE SCHEMA IF NOT EXISTS rebase");
8615
9501
  await ddl.ensureObject(`Creating ${TABLE}`, `
8616
9502
  CREATE TABLE IF NOT EXISTS ${TABLE} (
@@ -8631,10 +9517,11 @@ function createApiKeyStore(driver) {
8631
9517
  revoked_at TIMESTAMPTZ
8632
9518
  )
8633
9519
  `);
8634
- await ddl.ensureObject("Creating idx_api_keys_hash", `
8635
- CREATE INDEX IF NOT EXISTS idx_api_keys_hash
8636
- ON ${TABLE}(key_hash)
8637
- `);
9520
+ try {
9521
+ await exec(DROP_DUPLICATE_KEY_HASH_INDEX_SQL);
9522
+ } catch (err) {
9523
+ logger.warn(`[api-key-store] Left the duplicate idx_api_keys_hash in place for now: ${err instanceof Error ? err.message : String(err)}`);
9524
+ }
8638
9525
  await ddl.ensureObject("Creating idx_api_keys_prefix", `
8639
9526
  CREATE INDEX IF NOT EXISTS idx_api_keys_prefix
8640
9527
  ON ${TABLE}(key_prefix)
@@ -9180,6 +10067,6 @@ var auth_exports = /* @__PURE__ */ __exportAll({
9180
10067
  verifyPassword: () => verifyPassword
9181
10068
  });
9182
10069
  //#endregion
9183
- export { queryTokenAuth as $, createDataRateLimiter as A, getEmailChangeNoticeTemplate as At, SMTPEmailService as B, escapeHtml as Bt, pkceTokenParams as C, scopeDataDriver as Ct, isBootstrapWindowOpen as D, generateSecurePassword as Dt, createBuiltinAuthAdapter as E, completeUserCreation as Et, activeDevEmailSink as F, getPasswordResetTemplate as Ft, verifyPassword as G, isPublicStorageKey as Gt, resolveAuthHooks as H, raw as Ht, clearActiveDevEmailSink as I, getUserInvitationTemplate as It, createRequireAuth as J, IMPERSONATE_HEADER as Jt, createAdapterAuthMiddleware as K, isPublicStoragePath as Kt, createDevEmailSink as L, getWelcomeEmailTemplate as Lt, defaultAuthLimiter as M, getEmailOtpTemplate as Mt, setSharedRateLimitStore as N, getEmailVerificationTemplate as Nt, DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT as O, assertEmailLinkBases as Ot, MemoryRateLimitStore as P, getMagicLinkTemplate as Pt, publicObjectAuth as Q, extractLinks as R, resolveEmailBranding as Rt, oauthCodeFlowSchema as S, SERVICE_IDENTITY as St, createJwksRoutes as T, isAccessTokenRevoked as Tt, hashPassword as U, httpMethodToOperation as Ut, createEmailService as V, html as Vt, validatePasswordStrength as W, PUBLIC_STORAGE_PREFIX as Wt, fileTokenAuth as X, extractUserFromToken as Y, optionalAuth as Z, tryVerifyOidcIdToken as _, hasScope as _t, createApiKeyStore as a, decideImpersonation as at, createLinkedinProvider as b, extractBearerToken as bt, createSpotifyProvider as c, createFunctionScopeGuard as ct, createGitLabProvider as d, resolveApiKey as dt, requireAdmin as et, createDiscordProvider as f, validateApiKey as ft, createMicrosoftProvider as g, getAccessModel as gt, createAppleProvider as h, configureAccess as ht, createPersonalKeyRoutes as i, storageRequestWildcard as it, createRateLimiter as j, getEmailChangeTemplate as jt, createDataRateLimitCheck as k, resolveEmailLinkBase as kt, createSlackProvider as l, createTusScopeGuard as lt, createFacebookProvider as m, callerScopes as mt, createCustomAuthAdapter as n, requestedStorageObject as nt, resolveRateLimitStoreKind as o, refuseUnhonouredImpersonation as ot, createTwitterProvider as p, accessModelFromCollections as pt, createAuthMiddleware as q, storageObjectPath as qt, createApiKeyRoutes as r, storageObjectOfPath as rt, createSqlRateLimitStore as s, createApiKeyPreAuth as st, auth_exports as t, requireAuth as tt, createBitbucketProvider as u, isApiKeyToken as ut, verifyOidcIdToken as v, requireScope as vt, providerVerifiedEmail as w, supportsRlsScoping as wt, createGoogleProvider as x, safeCompare as xt, createGitHubProvider as y, requireScopeByMethod as yt, registerDevEmailSink as z, RawHtml as zt };
10070
+ export { publicObjectAuth as $, storageObjectPath as $t, createDataRateLimitCheck as A, isAccessTokenRevoked as At, registerDevEmailSink as B, getPasswordResetTemplate as Bt, pkceTokenParams as C, requireScope as Ct, configureUserPicking as D, SERVICE_IDENTITY as Dt, createBuiltinAuthAdapter as E, safeCompare as Et, MemoryRateLimitStore as F, getEmailChangeNoticeTemplate as Ft, validatePasswordStrength as G, escapeHtml as Gt, createEmailService as H, getWelcomeEmailTemplate as Ht, activeDevEmailSink as I, getEmailChangeTemplate as It, createAuthMiddleware as J, httpMethodToOperation as Jt, verifyPassword as K, html as Kt, clearActiveDevEmailSink as L, getEmailOtpTemplate as Lt, createRateLimiter as M, generateSecurePassword as Mt, defaultAuthLimiter as N, assertEmailLinkBases as Nt, isBootstrapWindowOpen as O, scopeDataDriver as Ot, setSharedRateLimitStore as P, resolveEmailLinkBase as Pt, optionalAuth as Q, isPublicStoragePath as Qt, createDevEmailSink as R, getEmailVerificationTemplate as Rt, oauthCodeFlowSchema as S, keyOwnerResolverFor as St, createJwksRoutes as T, extractBearerToken as Tt, resolveAuthHooks as U, resolveEmailBranding as Ut, SMTPEmailService as V, getUserInvitationTemplate as Vt, hashPassword as W, RawHtml as Wt, extractUserFromToken as X, PUBLIC_STORAGE_PREFIX as Xt, createRequireAuth as Y, securityRuleToConditions as Yt, fileTokenAuth as Z, isPublicStorageKey as Zt, tryVerifyOidcIdToken as _, accessModelFromCollections as _t, createApiKeyStore as a, isPublicObjectGrant as at, createLinkedinProvider as b, getAccessModel as bt, createSpotifyProvider as c, storageRequestWildcard as ct, createGitLabProvider as d, createApiKeyPreAuth as dt, queryTokenAuth as et, createDiscordProvider as f, createFunctionScopeGuard as ft, createMicrosoftProvider as g, validateApiKey as gt, createAppleProvider as h, resolveApiKey as ht, createPersonalKeyRoutes as i, isDownloadTokenGrant as it, createDataRateLimiter as j, completeUserCreation as jt, DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT as k, supportsRlsScoping as kt, createSlackProvider as l, decideImpersonation as lt, createFacebookProvider as m, isApiKeyToken as mt, createCustomAuthAdapter as n, requireAuth as nt, resolveRateLimitStoreKind as o, requestedStorageObject as ot, createTwitterProvider as p, createTusScopeGuard as pt, createAdapterAuthMiddleware as q, raw as qt, createApiKeyRoutes as r, PUBLIC_OBJECT_PRINCIPAL as rt, createSqlRateLimitStore as s, storageObjectOfPath as st, auth_exports as t, requireAdmin as tt, createBitbucketProvider as u, refuseUnhonouredImpersonation as ut, verifyOidcIdToken as v, callerScopes as vt, providerVerifiedEmail as w, requireScopeByMethod as wt, createGoogleProvider as x, hasScope as xt, createGitHubProvider as y, configureAccess as yt, extractLinks as z, getMagicLinkTemplate as zt };
9184
10071
 
9185
- //# sourceMappingURL=auth-DMLngxn_.js.map
10072
+ //# sourceMappingURL=auth-OPq_FeMs.js.map