@rebasepro/server 0.24.0 → 0.24.1-canary.gcb918fc

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 (224) hide show
  1. package/README.md +27 -2
  2. package/dist/GCSStorageController-fv6JAZ87.js +402 -0
  3. package/dist/GCSStorageController-fv6JAZ87.js.map +1 -0
  4. package/dist/S3StorageController-BNg-pXiY.js +480 -0
  5. package/dist/S3StorageController-BNg-pXiY.js.map +1 -0
  6. package/dist/api/mount-path.d.ts +30 -0
  7. package/dist/api/mount.d.ts +6 -13
  8. package/dist/api/rest/api-generator.d.ts +53 -1
  9. package/dist/api/rest/auth-collection-writes.d.ts +28 -21
  10. package/dist/api/rest/field-access-query.d.ts +28 -8
  11. package/dist/api/rest/field-ops.d.ts +19 -7
  12. package/dist/api/rest/query-body.d.ts +10 -0
  13. package/dist/api/rest/write-validation.d.ts +7 -5
  14. package/dist/api/schema-editor-routes.d.ts +10 -1
  15. package/dist/api/sql-export-routes.d.ts +8 -0
  16. package/dist/api/types.d.ts +35 -0
  17. package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-mCeSC03x.js} +3 -3
  18. package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-mCeSC03x.js.map} +1 -1
  19. package/dist/auth/access.d.ts +57 -8
  20. package/dist/auth/admin-user-ops.d.ts +24 -2
  21. package/dist/auth/admin-users-route.d.ts +1 -0
  22. package/dist/auth/api-keys/api-key-middleware.d.ts +10 -3
  23. package/dist/auth/api-keys/api-key-routes.d.ts +4 -1
  24. package/dist/auth/builtin-auth-adapter.d.ts +13 -0
  25. package/dist/auth/email-change-routes.d.ts +3 -0
  26. package/dist/auth/expired-token-sweep.d.ts +5 -1
  27. package/dist/auth/interfaces.d.ts +10 -0
  28. package/dist/auth/jwks-routes.d.ts +5 -1
  29. package/dist/auth/jwt-crypto.d.ts +7 -0
  30. package/dist/auth/jwt-keys.d.ts +4 -0
  31. package/dist/auth/jwt.d.ts +282 -166
  32. package/dist/auth/magic-link-routes.d.ts +3 -0
  33. package/dist/auth/mfa-crypto.d.ts +9 -3
  34. package/dist/auth/mfa-gate.d.ts +4 -2
  35. package/dist/auth/mfa-routes.d.ts +6 -0
  36. package/dist/auth/middleware.d.ts +9 -2
  37. package/dist/auth/otp-routes.d.ts +3 -0
  38. package/dist/auth/rate-limiter.d.ts +61 -34
  39. package/dist/auth/registration-policy.d.ts +17 -0
  40. package/dist/auth/require-auth.d.ts +1 -1
  41. package/dist/auth/routes.d.ts +16 -0
  42. package/dist/auth/session-routes.d.ts +6 -0
  43. package/dist/auth/user-picking.d.ts +37 -0
  44. package/dist/auth/verify-credential.d.ts +25 -4
  45. package/dist/{auth-DMLngxn_.js → auth-GnONzteu.js} +2869 -1871
  46. package/dist/auth-GnONzteu.js.map +1 -0
  47. package/dist/auth_adapter-CIItCTc8.js +21 -0
  48. package/dist/auth_adapter-CIItCTc8.js.map +1 -0
  49. package/dist/backend/create-backend.d.ts +281 -0
  50. package/dist/backend/default-backend.d.ts +35 -0
  51. package/dist/backend/options.d.ts +912 -0
  52. package/dist/backend/scope.d.ts +23 -0
  53. package/dist/backend/services.d.ts +54 -0
  54. package/dist/backend-DTAOsLQc.js.map +1 -1
  55. package/dist/backup/backup-common.d.ts +14 -3
  56. package/dist/backup/backup-routes.d.ts +23 -2
  57. package/dist/backup/backup-storage.d.ts +3 -1
  58. package/dist/backup/index.d.ts +3 -1
  59. package/dist/{backup-CN0s50D2.js → backup-BnvkSLHi.js} +176 -35
  60. package/dist/backup-BnvkSLHi.js.map +1 -0
  61. package/dist/boot/boot.d.ts +2 -2
  62. package/dist/boot/driver.d.ts +3 -0
  63. package/dist/boot/env.d.ts +9 -2
  64. package/dist/boot/http-server.d.ts +22 -0
  65. package/dist/boot/options.d.ts +16 -1
  66. package/dist/boot/provision.d.ts +4 -2
  67. package/dist/boot/runtime-cors.d.ts +17 -0
  68. package/dist/boot/sources.d.ts +2 -0
  69. package/dist/boot/static-routing.d.ts +9 -0
  70. package/dist/{contract-routes-fz8i4pxs.js → contract-routes-Ey6ftnKH.js} +4 -20
  71. package/dist/contract-routes-Ey6ftnKH.js.map +1 -0
  72. package/dist/cron/cron-loader.d.ts +77 -18
  73. package/dist/cron/cron-routes.d.ts +1 -1
  74. package/dist/cron/cron-scheduler.d.ts +69 -3
  75. package/dist/cron/cron-store.d.ts +33 -5
  76. package/dist/cron/define-cron.d.ts +6 -2
  77. package/dist/cron/index.d.ts +3 -3
  78. package/dist/cron-loader-B-u0snqq.js +202 -0
  79. package/dist/cron-loader-B-u0snqq.js.map +1 -0
  80. package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-OD6BstUj.js} +27 -4
  81. package/dist/cron-routes-OD6BstUj.js.map +1 -0
  82. package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-idtLPoN-.js} +299 -33
  83. package/dist/cron-scheduler-idtLPoN-.js.map +1 -0
  84. package/dist/cron-store-CZ30ZBi-.js +577 -0
  85. package/dist/cron-store-CZ30ZBi-.js.map +1 -0
  86. package/dist/email/smtp-email-service.d.ts +23 -0
  87. package/dist/email/types.d.ts +18 -0
  88. package/dist/env.d.ts +0 -37
  89. package/dist/filter-dialect-O1HK6Nvr.js +1628 -0
  90. package/dist/filter-dialect-O1HK6Nvr.js.map +1 -0
  91. package/dist/{function-loader-D7o5Epjj.js → function-loader-iKhOZatS.js} +68 -38
  92. package/dist/function-loader-iKhOZatS.js.map +1 -0
  93. package/dist/{function-routes-CaNG4waN.js → function-routes-j9TwpvQe.js} +5 -4
  94. package/dist/function-routes-j9TwpvQe.js.map +1 -0
  95. package/dist/functions/context.d.ts +9 -0
  96. package/dist/functions/define-function.d.ts +10 -6
  97. package/dist/functions/function-loader.d.ts +75 -18
  98. package/dist/functions/function-routes.d.ts +2 -1
  99. package/dist/functions/index.d.ts +5 -4
  100. package/dist/functions/index.js +42 -13
  101. package/dist/functions/index.js.map +1 -1
  102. package/dist/functions/internal.d.ts +2 -2
  103. package/dist/functions/proxy.d.ts +6 -2
  104. package/dist/functions/request-timeout.d.ts +7 -5
  105. package/dist/history/history-routes.d.ts +9 -0
  106. package/dist/history/retention-sweep.d.ts +81 -0
  107. package/dist/{history-recorder-Nr8zLvoU.js → history-recorder-BfWRwmPy.js} +3 -3
  108. package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-BfWRwmPy.js.map} +1 -1
  109. package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
  110. package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
  111. package/dist/index.d.ts +23 -10
  112. package/dist/index.es.js +24423 -20038
  113. package/dist/index.es.js.map +1 -1
  114. package/dist/init/docs.d.ts +27 -10
  115. package/dist/init/process-safety.d.ts +7 -2
  116. package/dist/init/shutdown.d.ts +18 -3
  117. package/dist/init.d.ts +49 -865
  118. package/dist/internal-tables-L2t39RdT.js +136 -0
  119. package/dist/internal-tables-L2t39RdT.js.map +1 -0
  120. package/dist/jobs/job-queue.d.ts +8 -0
  121. package/dist/jobs/job-store.d.ts +24 -0
  122. package/dist/jobs/types.d.ts +25 -4
  123. package/dist/{jobs-DqYNfquG.js → jobs-BCbflZj2.js} +234 -24
  124. package/dist/jobs-BCbflZj2.js.map +1 -0
  125. package/dist/{jwt-R6bSPMjk.js → jwt-BlvuKoR6.js} +529 -454
  126. package/dist/jwt-BlvuKoR6.js.map +1 -0
  127. package/dist/{keys-GAVZqbqx.js → keys-COzQ2aE8.js} +2 -2
  128. package/dist/{keys-GAVZqbqx.js.map → keys-COzQ2aE8.js.map} +1 -1
  129. package/dist/load-failure-L5hvfDoG.js +80 -0
  130. package/dist/load-failure-L5hvfDoG.js.map +1 -0
  131. package/dist/mcp/mcp-routes.d.ts +6 -0
  132. package/dist/mcp/mcp-tools.d.ts +8 -0
  133. package/dist/mcp/oauth-routes.d.ts +7 -0
  134. package/dist/metrics/index.d.ts +18 -0
  135. package/dist/metrics/pool-wait.d.ts +50 -0
  136. package/dist/offline-QNQgsPOq-B2yGeXkn.js +3305 -0
  137. package/dist/offline-QNQgsPOq-B2yGeXkn.js.map +1 -0
  138. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js +1915 -0
  139. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js.map +1 -0
  140. package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-D9zUQmEp.js} +366 -82
  141. package/dist/openapi-generator-D9zUQmEp.js.map +1 -0
  142. package/dist/policy-PdzX-Opd.js +121 -0
  143. package/dist/policy-PdzX-Opd.js.map +1 -0
  144. package/dist/project_manifest-R6y_KHms.js +59 -0
  145. package/dist/project_manifest-R6y_KHms.js.map +1 -0
  146. package/dist/{proxy-qRlqeUmO.js → proxy-CkTlMGmh.js} +4 -3
  147. package/dist/proxy-CkTlMGmh.js.map +1 -0
  148. package/dist/{query-parser-BgiKJKvc.js → query-parser-4WkMoA_K.js} +510 -15
  149. package/dist/query-parser-4WkMoA_K.js.map +1 -0
  150. package/dist/{request-timeout-DgH7j8qO.js → request-timeout-Dnl2_OBc.js} +15 -8
  151. package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
  152. package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
  153. package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
  154. package/dist/schema-edit/apply-schema-change.d.ts +10 -0
  155. package/dist/schema-edit/local-source.d.ts +29 -0
  156. package/dist/schema-edit/remote-source.d.ts +13 -3
  157. package/dist/schema-edit/source-editor.d.ts +28 -0
  158. package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-C11Tumyd.js} +54 -10
  159. package/dist/schema-editor-routes-C11Tumyd.js.map +1 -0
  160. package/dist/services/outbound-post.d.ts +50 -0
  161. package/dist/services/outbound-url-guard.d.ts +34 -5
  162. package/dist/services/routed-realtime-service.d.ts +5 -0
  163. package/dist/services/webhook-service.d.ts +66 -14
  164. package/dist/singleton.d.ts +9 -5
  165. package/dist/soft-delete-params-DfV3vVN5.js +668 -0
  166. package/dist/soft-delete-params-DfV3vVN5.js.map +1 -0
  167. package/dist/sql-export-routes-ClRwzIsC.js +240 -0
  168. package/dist/sql-export-routes-ClRwzIsC.js.map +1 -0
  169. package/dist/{src-CatHFUym.js → src-D5Y3N-S4.js} +2 -1
  170. package/dist/{src-CatHFUym.js.map → src-D5Y3N-S4.js.map} +1 -1
  171. package/dist/storage/GCSStorageController.d.ts +46 -2
  172. package/dist/storage/LocalStorageController.d.ts +67 -2
  173. package/dist/storage/S3StorageController.d.ts +37 -3
  174. package/dist/storage/image-transform.d.ts +5 -0
  175. package/dist/storage/maintenance-routes.d.ts +23 -0
  176. package/dist/storage/maintenance.d.ts +126 -0
  177. package/dist/storage/rendition-cache.d.ts +9 -11
  178. package/dist/storage/request-principals.d.ts +30 -0
  179. package/dist/storage/routes.d.ts +20 -0
  180. package/dist/storage/source-variables.d.ts +10 -0
  181. package/dist/storage/streams.d.ts +42 -0
  182. package/dist/storage/tus-handler.d.ts +46 -2
  183. package/dist/storage/types.d.ts +82 -7
  184. package/dist/storage/upload-validation.d.ts +13 -0
  185. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
  186. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
  187. package/dist/strings.es-BeZWAodn.js +256 -0
  188. package/dist/strings.es-BeZWAodn.js.map +1 -0
  189. package/dist/upload-validation-Dd-fnsbA.js +172 -0
  190. package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
  191. package/dist/utils/byte-size.d.ts +10 -0
  192. package/dist/utils/compression.d.ts +6 -0
  193. package/dist/utils/load-failure.d.ts +14 -0
  194. package/package.json +5 -5
  195. package/dist/GCSStorageController-BSiP1c-f.js +0 -256
  196. package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
  197. package/dist/S3StorageController-CAwFRgjV.js +0 -276
  198. package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
  199. package/dist/auth-DMLngxn_.js.map +0 -1
  200. package/dist/backup-CN0s50D2.js.map +0 -1
  201. package/dist/contract-routes-fz8i4pxs.js.map +0 -1
  202. package/dist/cron-loader-CwaANlOG.js +0 -118
  203. package/dist/cron-loader-CwaANlOG.js.map +0 -1
  204. package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
  205. package/dist/cron-scheduler-CYQgco86.js.map +0 -1
  206. package/dist/cron-store-D2Q9-Aco.js +0 -300
  207. package/dist/cron-store-D2Q9-Aco.js.map +0 -1
  208. package/dist/dynamic-import-X40pTZUQ.js +0 -23
  209. package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
  210. package/dist/function-loader-D7o5Epjj.js.map +0 -1
  211. package/dist/function-routes-CaNG4waN.js.map +0 -1
  212. package/dist/jobs-DqYNfquG.js.map +0 -1
  213. package/dist/jwt-R6bSPMjk.js.map +0 -1
  214. package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
  215. package/dist/proxy-qRlqeUmO.js.map +0 -1
  216. package/dist/query-parser-BgiKJKvc.js.map +0 -1
  217. package/dist/request-timeout-DgH7j8qO.js.map +0 -1
  218. package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
  219. package/dist/soft-delete-params-BWPilMPF.js +0 -59
  220. package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
  221. package/dist/src-I3aG1PcY.js +0 -6870
  222. package/dist/src-I3aG1PcY.js.map +0 -1
  223. package/dist/types-BfKcm9do.js +0 -39
  224. package/dist/types-BfKcm9do.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strings.es-BeZWAodn.js","names":[],"sources":["../../types/src/errors.ts","../../types/src/controllers/data_driver.ts","../../utils/dist/strings.es.js"],"sourcesContent":["/**\n * The error codes every route can produce, as `RebaseApiError.code`.\n *\n * These are the defaults on `ApiError`'s static constructors server-side, so\n * any endpoint can answer with one. They are **not** the complete set: routes\n * pass their own more specific codes too (`EMAIL_EXISTS`, `TOKEN_EXPIRED`,\n * `INVALID_BULK_BODY`, …), and auth alone defines a couple of dozen.\n *\n * Hence the union is deliberately open rather than closed. It exists to give\n * autocomplete and to catch a typo in the common cases — `code` was a bare\n * `string`, so `e.code === \"NOT_FOUND\"` and `e.code === \"NOTFOUND\"` were\n * equally valid and only one of them worked. Closing it would be a lie that\n * broke the moment a route added a code.\n *\n * @example\n * if (e instanceof RebaseApiError) {\n * switch (e.code) {\n * case \"NOT_FOUND\": return null; // completed\n * case \"FORBIDDEN\": return redirect();\n * default: throw e; // routes' own codes land here\n * }\n * }\n *\n * @group Errors\n */\nexport type RebaseErrorCode =\n | \"BAD_REQUEST\"\n | \"UNAUTHORIZED\"\n | \"FORBIDDEN\"\n | \"NOT_FOUND\"\n | \"CONFLICT\"\n | \"INTERNAL_ERROR\"\n | \"SERVICE_UNAVAILABLE\"\n | \"NETWORK_ERROR\"\n | \"OFFLINE\"\n | \"DB_PERMISSION_DENIED\"\n | \"SCHEMA_DRIFT\"\n /**\n * Realtime: the socket closed after a request was written and before the\n * server answered — the request may or may not have run, and it is never\n * re-sent. On a live subscription's `onError`: the connection has been\n * down longer than a blip; the subscription is kept and resumes, with a\n * fresh `onUpdate`, when the socket is back.\n */\n | \"CONNECTION_LOST\"\n /**\n * Realtime: no answer within the request timeout of the call — either it\n * waited for a socket the whole time and was never sent, or it was written\n * and never answered.\n */\n | \"REQUEST_TIMEOUT\"\n // `string & {}` keeps the union open while preserving completion on the\n // literals above — a bare `| string` would collapse them and offer nothing.\n | (string & {});\n\n/**\n * Structured initializer for {@link RebaseApiError}.\n *\n * @group Errors\n */\nexport interface RebaseErrorInit {\n /**\n * HTTP status code, when the error originated from an HTTP response.\n *\n * Three states, and they mean different things:\n *\n * - a real status — the server answered, and this is what it said;\n * - **`0`** — the request never reached a server: DNS, a refused\n * connection, CORS, an abort. `XMLHttpRequest` has always spelled that\n * `0`, and a fabricated 5xx would be indistinguishable from one the\n * server actually sent. The original failure is on `cause`;\n * - `undefined` — nothing was sent at all: a realtime/WebSocket failure,\n * or a client-side logic error raised before any request.\n */\n status?: number;\n /** Stable, machine-readable error code. See {@link RebaseErrorCode}. */\n code?: RebaseErrorCode;\n /** Structured error payload returned by the server, when present. */\n details?: unknown;\n /**\n * The server's correlation id for the request that failed, when it sent\n * one — the `requestId` in the error envelope, which also comes back on the\n * `X-Request-ID` header.\n *\n * The envelope has carried it for a while; the client dropped it on the\n * floor, so a bug report from an app could never quote the one string that\n * finds the server-side line.\n */\n requestId?: string;\n /**\n * Seconds to wait before retrying, from the response's `Retry-After`\n * header. Present on a 429 and on some 503s.\n *\n * Also dropped. The offline queue's own backoff therefore ignored a server\n * that had said exactly how long to wait — the one number that turns a\n * retry storm into a queue that drains.\n */\n retryAfterSeconds?: number;\n /** The underlying error this one wraps, if any. */\n cause?: unknown;\n}\n\n/**\n * The single error type thrown across the entire Rebase client surface —\n * HTTP data/control-plane calls, realtime/WebSocket operations, and\n * client-side logic errors (e.g. an unknown collection accessor). A `catch`\n * block only ever needs to check for this one class:\n *\n * ```ts\n * import { RebaseApiError } from \"@rebasepro/client\"; // re-exported\n *\n * try {\n * await client.data.products.update(id, { price: 9 });\n * } catch (e) {\n * if (e instanceof RebaseApiError) {\n * if (e.status === 404) { ... } // HTTP failures carry a status\n * console.error(e.code, e.details);\n * }\n * }\n * ```\n *\n * `status` is present for HTTP failures and `undefined` otherwise, so its\n * presence distinguishes transport-level errors from realtime/logic errors.\n *\n * @group Errors\n */\nexport class RebaseApiError extends Error {\n /** HTTP status code, or `undefined` for non-HTTP errors. */\n readonly status?: number;\n /** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */\n readonly code?: RebaseErrorCode;\n /** Structured error payload from the server, when present. */\n readonly details?: unknown;\n /** See {@link RebaseErrorInit.requestId}. Quote it in a bug report. */\n readonly requestId?: string;\n /** See {@link RebaseErrorInit.retryAfterSeconds}. */\n readonly retryAfterSeconds?: number;\n\n constructor(message: string, init: RebaseErrorInit = {}) {\n super(message);\n this.name = \"RebaseApiError\";\n this.status = init.status;\n this.code = init.code;\n this.details = init.details;\n this.requestId = init.requestId;\n this.retryAfterSeconds = init.retryAfterSeconds;\n if (init.cause !== undefined) {\n // `cause` is standard on Error but not always in the lib target's type.\n (this as { cause?: unknown }).cause = init.cause;\n }\n }\n}\n\n/**\n * Client-side logic error — raised before any request is made (e.g. accessing\n * an unknown collection accessor when a typed dictionary is configured).\n *\n * A subclass of {@link RebaseApiError} (with no `status`), so a single\n * `catch (e) { if (e instanceof RebaseApiError) ... }` handles it too.\n *\n * @group Errors\n */\nexport class RebaseClientError extends RebaseApiError {\n /**\n * `init` is the same one {@link RebaseApiError} takes, and it is what makes\n * `code` reachable at all.\n *\n * The constructor used to accept a message and nothing else, so every\n * client-side failure — an undefined filter value, an unknown accessor,\n * `listen()` on a client built with `realtime: false`, a function name with\n * a `/` in it, `refreshSession()` while signed out — arrived with `code ===\n * undefined`. The documented `switch (e.code)` in this file's own example\n * fell to `default: throw e` for all of them, and the only client-side error\n * that *did* carry a code was `OFFLINE`, because that one path minted a\n * `RebaseApiError` instead.\n */\n constructor(message: string, init: RebaseErrorInit = {}) {\n super(message, init);\n this.name = \"RebaseClientError\";\n }\n}\n\n/**\n * Brand for a contract method a particular client cannot serve.\n *\n * `Symbol.for` rather than a fresh symbol: two copies of `@rebasepro/types` in\n * one tree — which happens, see `docs/dependency-duplication-traps.md` — must\n * agree about it, and a module-local symbol would not.\n */\nconst UNSUPPORTED_METHOD = Symbol.for(\"rebase.unsupportedMethod\");\n\n/**\n * Build the stub a client installs for a contract method it cannot serve.\n *\n * `listen`, `listenById` and `count` are part of `SDKCollectionClient`, not\n * optional extras — a caller should be able to write\n * `client.data.posts.count()` without asking first, and a transport that cannot\n * serve it should answer with a sentence naming the configuration that would,\n * rather than with `undefined is not a function` at the call site. Where the\n * transport genuinely cannot (a client built with `realtime: false`, a driver\n * with no `listenCollection`), it installs one of these instead of omitting the\n * method.\n *\n * @param message What to tell the caller, naming the fix.\n * @group Errors\n */\n/**\n * The tag {@link unsupportedMethod} puts on the stub it returns.\n *\n * Written as an optional member on a callable, which is what a tagged stub\n * actually is — so both the write and the read below are single casts and tsc\n * checks the symbol and the value type on each. Reading it as\n * `Record<symbol, boolean>` made every symbol key on the object a `boolean`\n * (which is not true of a function, and is why that needed `as unknown as` to\n * be written at all), and the write and the read were then free to disagree\n * about the tag.\n */\ntype UnsupportedTagged = ((...args: never[]) => unknown) & { [UNSUPPORTED_METHOD]?: boolean };\n\nexport function unsupportedMethod<F>(message: string): F {\n const stub = (): never => {\n // The two reasons a method is a stub — `realtime: false`, and a driver\n // with no `listenCollection` — are one thing to a caller: this client\n // cannot do realtime. One code covers both, and the message says which.\n throw new RebaseClientError(message, { code: \"REALTIME_DISABLED\" });\n };\n (stub as UnsupportedTagged)[UNSUPPORTED_METHOD] = true;\n return stub as F;\n}\n\n/**\n * Can this method actually do anything?\n *\n * `true` for a stub from {@link unsupportedMethod} **and** for a method that is\n * simply not there — a partial client, a hand-built test double, an\n * implementation written against an older shape of the interface. Both mean the\n * same thing to a caller, so both answer the same way, and an adapter that\n * checks this cannot be caught out by either.\n *\n * Ordinary code does not need it: calling the method and letting it throw is\n * the normal path. Adapters do — the admin panel chooses between subscribing\n * and a one-shot `find()` by asking whether the client can listen, and a UI\n * that subscribes into a throw is worse than one that polls. This is the\n * question `if (accessor.listen)` used to be asking, made explicit now that the\n * method is always there to call.\n *\n * @group Errors\n */\nexport function isUnsupported(method: unknown): boolean {\n if (typeof method !== \"function\") return true;\n return (method as UnsupportedTagged)[UNSUPPORTED_METHOD] === true;\n}\n","import { RebaseApiError } from \"../errors\";\nimport type { CollectionRegistryController } from \"./collection_registry\";\nimport type { EntityStatus, EntityValues } from \"../types/entities\";\nimport type { CollectionConfig, FilterValues } from \"../types/collections\";\nimport type { OrderByTuple } from \"../types/filter-operators\";\nimport type { RebaseCallContext } from \"../call_context\";\nimport type { IncludeSpec, LogicalCondition } from \"./data\";\nimport type { CollectionUpdateMeta } from \"../types/websockets\";\n\n\n/**\n * @internal\n */\nexport interface FetchOneProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n id: string | number;\n databaseId?: string;\n collection?: CollectionConfig<M>\n /**\n * See {@link FetchCollectionProps.withDeleted}. A soft-deleted row is a 404\n * here by default, so `findById` and `find` agree about which rows exist —\n * a row you cannot find in a listing and can still open by id is the kind\n * of inconsistency that makes a feature untrustworthy.\n */\n withDeleted?: boolean | \"only\";\n}\n\n/**\n * What a row listener subscribes to: the row, and the relations each frame\n * loads into it.\n *\n * @internal\n */\nexport type ListenOneTarget<M extends Record<string, unknown> = Record<string, unknown>> =\n FetchOneProps<M>\n & {\n /**\n * Relations to load into every frame, as {@link FetchCollectionProps.include}\n * loads them into each row of a list. Without it a to-many relation\n * is absent from each frame, and the admin's record form, opened by\n * URL, showed it empty — so a save from it unlinked every related row.\n */\n include?: IncludeSpec;\n }\n\n/**\n * @internal\n */\nexport type ListenOneProps<M extends Record<string, unknown> = Record<string, unknown>> =\n ListenOneTarget<M>\n & {\n onUpdate: (row: Record<string, unknown> | null) => void,\n onError?: (error: Error) => void,\n }\n\n/**\n * Configuration for vector similarity search queries.\n * Vector search applies an ORDER BY distance expression and optionally\n * filters results by a distance threshold.\n */\nexport interface VectorSearchParams {\n /** Property name containing the vector column */\n property: string;\n /** Query vector to compare against */\n vector: number[];\n /** Distance function (default: \"cosine\") */\n distance?: \"cosine\" | \"l2\" | \"inner_product\";\n /** Only return results within this distance threshold */\n threshold?: number;\n}\n\n// ── List pagination bounds ────────────────────────────────────────────────\n//\n// Client-driven list reads (REST `GET /<collection>` and the WebSocket\n// `subscribe_collection` message) accept a client-supplied `limit`. Without\n// bounds, an ABSENT limit streams the entire table into memory — a trivial\n// OOM/DoS — and `limit=100000000` (or `limit=0`, historically an unlimited\n// bypass) is honoured verbatim. `resolveClientListLimit` is the single shared\n// enforcement point so every untrusted ingress behaves identically. Trusted\n// server-side callers build fetch options directly and are intentionally NOT\n// bounded here (migrations, admin exports, and CDC refetches may need the full\n// set).\n//\n// A limit the platform will not serve is REFUSED, not quietly shrunk. Clamping\n// answers a request for 100 000 rows with 1 000 of them, and a short page is\n// indistinguishable from \"that is all the data there is\" — which is how a CSV\n// export shipped 50 rows of a 100 000-row collection under a filename that read\n// like the whole thing. `meta.total`/`meta.hasMore` make truncation *detectable*\n// on the REST list response, but only for a caller who thinks to compare what it\n// asked for against what it got, and the WebSocket `collection_update` frame\n// carries neither — so signalling cannot be the answer on every surface and\n// rejecting is. An ABSENT limit still defaults: naming no window is not the same\n// as asking for one that cannot be served.\n\n/** Rows returned for a plain / text-search list read when the client sends no `limit`. */\nexport const DEFAULT_LIST_LIMIT = 50;\n/** Rows returned for a vector-search list read when the client sends no `limit`. */\nexport const DEFAULT_VECTOR_LIST_LIMIT = 10;\n/** Largest `limit` a client may ask for on any surface. Above it, the read is refused. */\nexport const MAX_LIST_LIMIT = 1000;\n\n/** Overridable bounds for {@link resolveClientListLimit}. */\nexport interface ListLimitBounds {\n /** Default page size for plain and text-search reads. */\n defaultLimit?: number;\n /** Default page size for vector-search reads. */\n vectorDefaultLimit?: number;\n /** Largest limit a client may ask for. A larger one is rejected, not clamped. */\n maxLimit?: number;\n}\n\n/**\n * Thrown by {@link resolveClientListLimit} for a `limit` the platform will not\n * serve. Carries an HTTP status so an ingress that speaks HTTP can forward it\n * verbatim, and `maxLimit` so one can be built without re-deriving the ceiling.\n *\n * @group Errors\n */\nexport class ListLimitError extends RebaseApiError {\n /** The ceiling that was exceeded — what the caller should page by instead. */\n readonly maxLimit: number;\n\n constructor(message: string, maxLimit: number) {\n super(message, { status: 400, code: \"INVALID_LIMIT\" });\n this.name = \"ListLimitError\";\n this.maxLimit = maxLimit;\n // Keeps `instanceof` working when this is compiled down for an older\n // target, where extending a builtin otherwise loses the prototype.\n Object.setPrototypeOf(this, ListLimitError.prototype);\n }\n}\n\n/**\n * Resolve a client-supplied list `limit` into a safe, always-defined value.\n *\n * - An absent / blank limit falls back to the mode default:\n * `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.\n * - A limit that is present must be an integer in `[1, maxLimit]`. Anything\n * else — `0`, a negative, `1.5`, `abc`, `100000000` — throws\n * {@link ListLimitError} rather than being coerced into range, because every\n * coercion answers a question the caller did not ask with a page it cannot\n * tell apart from the whole collection.\n *\n * The return is never `undefined` — no ingress that routes its client limit\n * through this can produce an unbounded read.\n *\n * @throws {ListLimitError} when a present `limit` is not an integer in range.\n */\nexport function resolveClientListLimit(\n rawLimit: number | string | null | undefined,\n opts: ListLimitBounds & { vectorSearch?: boolean } = {}\n): number {\n const maxLimit = opts.maxLimit ?? MAX_LIST_LIMIT;\n if (rawLimit != null && String(rawLimit).trim() !== \"\") {\n // `Number`, not `parseInt`: `parseInt(\"50rows\")` is 50, which silently\n // reads a typo as a window the caller never wrote.\n const parsed = typeof rawLimit === \"number\" ? rawLimit : Number(String(rawLimit).trim());\n if (!Number.isInteger(parsed) || parsed < 1) {\n throw new ListLimitError(\n `Invalid \\`limit\\`: ${String(rawLimit)}. Expected a whole number between 1 and ${maxLimit}.`,\n maxLimit\n );\n }\n if (parsed > maxLimit) {\n throw new ListLimitError(\n `\\`limit\\` ${parsed} is above the maximum of ${maxLimit}. Ask for at most ${maxLimit} rows ` +\n \"per read and page through the rest with `offset` — answering with a smaller page would be \" +\n \"indistinguishable from there being no more rows.\",\n maxLimit\n );\n }\n return parsed;\n }\n return opts.vectorSearch\n ? (opts.vectorDefaultLimit ?? DEFAULT_VECTOR_LIST_LIMIT)\n : (opts.defaultLimit ?? DEFAULT_LIST_LIMIT);\n}\n\n/**\n * @internal\n */\nexport interface FetchCollectionProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n collection?: CollectionConfig<M>;\n filter?: FilterValues<Extract<keyof M, string>>,\n /**\n * An `or(...)`/`and(...)` group, applied alongside `filter`.\n *\n * The REST layer parsed `?or=` into this and then had nowhere to put it, so\n * the group was dropped and the read ran unfiltered — returning every row\n * the caller's policies allowed rather than the ones they asked for.\n */\n logical?: LogicalCondition;\n limit?: number;\n offset?: number;\n startAfter?: unknown;\n /**\n * The sort, in either of two spellings:\n *\n * - a field name, whose direction is the separate `order` below — the\n * original single-column contract, which every existing driver reads;\n * - a list of `[field, direction]` tuples applied in order of significance,\n * which carries a multi-column sort and ignores `order` entirely.\n *\n * `normalizeDriverOrderBy` in `@rebasepro/common` collapses the pair to the\n * list form. A driver that has not been taught the list form should read it\n * through that helper rather than assume a string: handed an array, `String()`\n * would produce a field name like `roles,asc` and the sort would 400 (or,\n * with unknown-field warnings on, silently vanish).\n */\n orderBy?: string | OrderByTuple[];\n searchString?: string;\n /** Ask each row which declared search field matched — populates `_matches`. */\n searchExplain?: boolean;\n /** Direction for the string form of `orderBy`. Ignored when `orderBy` is a list. */\n order?: \"desc\" | \"asc\";\n /** Vector similarity search configuration */\n vectorSearch?: VectorSearchParams;\n /**\n * What to do about rows a soft delete has stamped.\n *\n * Unset (the default) hides them, which is the whole point of the feature:\n * a deleted row is deleted as far as the application is concerned. `true`\n * includes them alongside the live ones — a trash view, an admin audit.\n * `\"only\"` returns nothing but them, which is the trash view proper and is\n * not expressible as a filter, because the field is not part of the\n * caller's vocabulary.\n *\n * Ignored by collections that do not declare {@link\n * PostgresCollectionConfig.softDelete}: there is no stamp to look at, and\n * silently returning nothing for `\"only\"` on such a collection would be a\n * worse answer than ignoring it.\n */\n withDeleted?: boolean | \"only\";\n /**\n * Relations to load — see {@link IncludeSpec}.\n *\n * Absent means *no* relations, the same as it does over REST. It used to be\n * absent from this contract entirely, and the driver's own fetch then loaded\n * every relation of every row unconditionally: `find()` returned a row with\n * a foreign key and `listen()` returned the same row with a nested object\n * where that key was, for the same query.\n */\n include?: IncludeSpec;\n /** Columns to read, as a projection. See `FindParams.fields`. */\n fields?: string[];\n /** `SELECT DISTINCT` over the projection. See `FindParams.distinct`. */\n distinct?: boolean;\n}\n\n/**\n * @internal\n */\nexport type ListenCollectionProps<M extends Record<string, unknown> = Record<string, unknown>> =\n FetchCollectionProps<M> &\n {\n /**\n * Page number (1-indexed), as `FindParams.page`.\n *\n * A subscription could name a `limit` and an `offset` but not a `page`,\n * so a live list on page three had to compute the offset itself — and\n * the two spellings then disagreed about what a page was.\n */\n page?: number;\n /**\n * Subscribe to the count alone: the server reads no rows, and each\n * frame carries `rows: []` and `meta.total`. A server that predates it\n * ignores the key and answers with the rows asked for (so ask for\n * `limit: 1`) and the same `meta`. See `listenCount`.\n */\n countOnly?: boolean;\n onUpdate: (rows: Record<string, unknown>[], meta?: CollectionUpdateMeta) => void;\n onError?: (error: Error) => void;\n };\n\n/**\n * @internal\n */\nexport interface SaveProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n values: Partial<EntityValues<M>>;\n id?: string | number; // can be empty for new entities\n previousValues?: Partial<EntityValues<M>>;\n collection?: CollectionConfig<M>;\n status: EntityStatus;\n /**\n * Write the row with INSERT ... ON CONFLICT DO UPDATE on the primary key\n * instead of choosing between insert and update up front.\n *\n * One statement, so it does not lose the race a read-then-write can, and it\n * succeeds whether or not the row is already there — what a re-runnable\n * import needs. Requires every primary key column to be present; without\n * them there is no conflict target and the row is inserted normally.\n */\n upsert?: boolean;\n\n /**\n * The columns the upsert matches a conflict on, instead of the primary key.\n *\n * The key is the only target that always exists, and it is the wrong one\n * for the write an upsert is usually reached for: \"this user, identified by\n * their email, exists with these values\". Keyed on the primary key that is\n * an insert, because the caller does not know the serial id — so the row is\n * duplicated on every run.\n *\n * Only column sets carrying a uniqueness guarantee are legal here; Postgres\n * refuses anything else with 42P10, from inside a transaction. The REST\n * layer checks the target against the collection's declarations first (see\n * `resolveConflictTarget`), so the answer is a 400 naming the available\n * targets rather than a 500 naming a constraint the caller never wrote.\n */\n onConflict?: readonly string[];\n\n /**\n * A check on the stored row an update addresses, run by the driver inside\n * the write's own transaction, before any hook runs and before anything is\n * written. See {@link RowPrecondition}. Honoured only by a driver that\n * declares {@link DataDriver.checksRowPreconditions}; ignored on a create.\n */\n precondition?: RowPrecondition;\n}\n\n/**\n * A check a caller makes on the row a write addresses, inside that write.\n *\n * The REST routes used to read the row in a transaction of their own before\n * writing it, to answer a missing row with their 404 and to compare\n * `If-Match`. Two transactions: the read loaded every declared relation of the\n * row, and two writes carrying the same tag could both pass the comparison,\n * because neither saw the other's write before making its own.\n *\n * @internal\n */\nexport interface RowPrecondition {\n /**\n * Lock the row before it is read, so no other write to it can land between\n * this check and this write. A conditional write (`If-Match`) sets it; a\n * plain existence check does not need it and does not pay for it.\n */\n lock?: boolean;\n /**\n * Judge the row as this caller reads it at the address written (their\n * policies, their `beforeQuery`, the trash hidden unless the write is a\n * restore or a purge) — `undefined` when there is none. Throw to refuse the\n * write; nothing has been written when this runs.\n */\n check(stored: Record<string, unknown> | undefined): void | Promise<void>;\n}\n\n/**\n * @internal\n */\nexport interface SaveManyProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n /**\n * The rows to write. A row carrying its primary key updates (or, with\n * `upsert`, inserts-or-updates) that row; one without inserts.\n */\n rows: Partial<EntityValues<M>>[];\n collection?: CollectionConfig<M>;\n /** Apply every row as INSERT ... ON CONFLICT DO UPDATE. See {@link SaveProps.upsert}. */\n upsert?: boolean;\n /** The conflict target for those upserts. See {@link SaveProps.onConflict}. */\n onConflict?: readonly string[];\n}\n\n/**\n * @internal\n */\nexport interface UpdateManyProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n /**\n * The rows to update, each named by its address.\n *\n * Distinct from {@link SaveManyProps.rows}, which carries keys *inside* the\n * values and is insert-shaped — `saveMany` passes `status: \"new\"` and no\n * `id`, so it cannot express \"update exactly this row\". This can, and it is\n * why bulk update is a separate driver method rather than a flag on that one.\n */\n updates: { id: string | number; values: Partial<EntityValues<M>> }[];\n collection?: CollectionConfig<M>;\n}\n\n/**\n * @internal\n */\nexport interface DeleteProps<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * The row's address, and nothing about its contents.\n *\n * The driver reads the row itself, under the caller's own scope, and that\n * read is what `beforeDelete` and `afterDelete` receive and what history\n * records. This used to carry `values`, and the driver took them as the\n * row — so over the WebSocket, which forwarded the client's frame, the\n * caller wrote the audit record of their own deletion and could get past a\n * `beforeDelete` that refused on a column by sending `values: {}`.\n */\n row: { id: string | number; path: string };\n collection?: CollectionConfig<M>;\n /**\n * Issue a real `DELETE` on a collection that declares\n * {@link PostgresCollectionConfig.softDelete}.\n *\n * The row and every cascade behind it go. It needs the same permission an\n * ordinary delete does and nothing more: it is the same verb, and a second\n * access-control surface for one operation is a second thing to get wrong.\n * No effect on a collection without soft delete, where every delete is\n * already this one.\n */\n hard?: boolean;\n\n /**\n * A check on the stored row, inside the delete's own transaction, before\n * any hook runs. See {@link RowPrecondition}. Honoured only by a driver\n * that declares {@link DataDriver.checksRowPreconditions}.\n */\n precondition?: RowPrecondition;\n}\n\n/**\n * @internal\n */\nexport interface DeleteManyProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n ids: (string | number)[];\n collection?: CollectionConfig<M>;\n /** See {@link DeleteProps.hard}. */\n hard?: boolean;\n}\n\n/**\n * Addressing ONE link of a many-to-many, to set the columns it carries.\n *\n * `path` is the relation on a row — `posts/1/tags` — and `targetId` the row on\n * the far side, so the pair names exactly one junction row. Not a `SaveProps`,\n * because a save at that address means \"write the target row\", and a link's own\n * columns are not the target's: two posts sharing a tag see one tag and two\n * different links.\n *\n * @internal\n */\nexport interface UpdateRelationPivotProps {\n /** The nested relation path, e.g. `posts/1/tags`. */\n path: string;\n /** The far side's key. */\n targetId: string | number;\n /** The junction columns to set, keyed by the property key `through.properties` declares. */\n pivot: Record<string, unknown>;\n}\n\n/**\n * One operation of a {@link DataDriver.batchWrite}.\n *\n * `path` rather than a slug, because a batch entry addresses rows exactly as\n * the single-row props do and a nested path is a legal address there.\n *\n * @internal\n */\nexport interface BatchWriteOperation<M extends Record<string, unknown> = Record<string, unknown>> {\n op: \"create\" | \"update\" | \"upsert\" | \"delete\";\n path: string;\n /** Required for `update` and `delete`. May be a `$ref` marker; see `batchWrite`. */\n id?: unknown;\n values?: Partial<EntityValues<M>>;\n collection?: CollectionConfig<M>;\n /** See {@link SaveProps.onConflict}. `upsert` only. */\n onConflict?: readonly string[];\n /** Names this operation's result, for a later `$ref`. */\n ref?: string;\n}\n\n/**\n * @internal\n */\nexport interface BatchWriteProps<M extends Record<string, unknown> = Record<string, unknown>> {\n operations: BatchWriteOperation<M>[];\n}\n\nexport type FilterCombinationValidProps = {\n path: string;\n databaseId?: string;\n collection: CollectionConfig;\n filterValues: FilterValues<string>;\n sortBy?: [string, \"asc\" | \"desc\"];\n};\n\n/**\n * The integration SPI for plugging a data backend into Rebase.\n *\n * Implement this interface to connect a custom backend (or use a built-in\n * driver such as the Firestore one) and register it on\n * `<Rebase dataSources>`. Rebase wraps drivers via `buildRebaseData` and\n * routes collections to them by their `dataSource` key.\n *\n * For *consuming* data in application code, use `RebaseData` /\n * `context.data` instead — this interface is only for providing it.\n *\n * @group Datasource\n */\nexport interface DataDriver {\n\n /**\n * Key that identifies this driver\n */\n key?: string;\n\n /**\n * If the driver has been initialised\n */\n initialised?: boolean;\n\n /**\n * Fetch data from a collection\n * @param props\n * @return Promise of flat rows\n */\n fetchCollection<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]>;\n\n /**\n * Listen to a collection in a given path. If you don't implement this method\n * `fetchCollection` will be used instead, with no real time updates.\n * @param props\n * @return Function to cancel subscription\n */\n listenCollection?<M extends Record<string, unknown> = Record<string, unknown>>(props: ListenCollectionProps<M>): () => void;\n\n /**\n * Retrieve a single row given a path and a collection\n * @param props\n */\n fetchOne<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchOneProps<M>): Promise<Record<string, unknown> | undefined>;\n\n /**\n * Get realtime updates on one row.\n * @param props\n * @return Function to cancel subscription\n */\n listenOne?<M extends Record<string, unknown> = Record<string, unknown>>(props: ListenOneProps<M>): () => void;\n\n /**\n * Save a row to the specified path\n * @param props\n */\n save<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveProps<M>): Promise<Record<string, unknown>>;\n\n /**\n * Save many rows as one unit of work.\n *\n * Every row runs the same pipeline as {@link save} — callbacks, relations\n * and row-level security all still apply — but they share a single\n * transaction, so the batch either lands whole or not at all. That, and the\n * single round trip, is what makes importing tens of thousands of rows\n * viable without dropping to raw SQL.\n *\n * Optional: drivers that cannot do this leave it undefined and callers fall\n * back to `save` per row.\n */\n saveMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveManyProps<M>): Promise<Record<string, unknown>[]>;\n\n /**\n * Update many rows in one transaction, each addressed by id.\n *\n * Optional for the same reason `saveMany` is: a driver that cannot make the\n * batch atomic should not pretend to. The REST layer reports\n * `BULK_UNSUPPORTED` rather than silently falling back to a loop of single\n * writes, which would be neither atomic nor one round trip — the two things\n * a caller reaches for a batch to get.\n */\n updateMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: UpdateManyProps<M>): Promise<Record<string, unknown>[]>;\n\n /**\n * Delete the row `props.row` addresses.\n *\n * **Resolving means the row is gone because this call removed it.** A\n * delete that matched nothing must reject with a not-found error\n * (`ApiError.notFound`, `statusCode: 404`) rather than resolving quietly.\n *\n * The rule is here rather than in each driver because the two\n * implementations answered differently and each had a test pinning its own\n * habit: Postgres threw, Mongo logged a warning and resolved. Three things\n * decide it in favour of rejecting.\n *\n * The REST layer already says 404 — `DELETE /api/data/<c>/<id>` reads the\n * row before removing it — so a quiet resolve made the driver API disagree\n * with the HTTP API about the same operation, and only in-process\n * `rebase.data` callers could see the difference.\n *\n * A caller cannot tell \"deleted\" from \"there was nothing there\" without it,\n * and those are different facts: one means the caller's model of the data\n * was right, the other that it was stale. Silence hands back the wrong one\n * and the caller carries on.\n *\n * And on a driver with row-level security, \"matched nothing\" is *also* how\n * a policy refusal arrives — Postgres filters `DELETE` through `USING`\n * rather than raising. A driver that resolves on zero rows therefore\n * reports a refused delete as a completed one, which is the defect\n * `explainZeroRowWrite` exists to prevent (see `write-denial.ts`).\n *\n * Conformance for both server drivers lives in\n * `packages/server/test/contract/delete-contract.ts`, run by each driver's\n * own suite against its own database. `packages/firebase`'s Firestore\n * driver does not honour it: `deleteDoc` resolves for a missing document\n * and reporting otherwise would cost a read on every delete. It runs in the\n * browser against Firestore's own semantics rather than behind\n * `rebase.data`, and that exception is stated here rather than left to be\n * discovered.\n */\n delete<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteProps<M>): Promise<void>;\n\n /**\n * Delete all entities from a collection.\n * @param path Collection path\n */\n deleteAll?(path: string): Promise<void>;\n\n /**\n * Set the columns ONE many-to-many link carries, leaving the membership\n * alone — `manyToMany`'s `through.properties`.\n *\n * `PATCH /api/data/<c>/<id>/<relation>/<targetId>` with a `_pivot` body\n * reaches this. The membership array cannot express it: sending one element\n * would unlink everything else, and re-sending the whole set to change one\n * value reintroduces the lost update the membership diff exists to avoid.\n *\n * Optional. A driver whose junctions carry nothing but the two keys leaves\n * it undefined, and the REST layer answers `RELATION_PIVOT_UNSUPPORTED`\n * rather than pretending the write landed.\n */\n updateRelationPivot?(props: UpdateRelationPivotProps): Promise<void>;\n\n /**\n * Delete many rows in one transaction, addressed by id.\n *\n * Ids rather than a filter, deliberately — see\n * {@link SDKCollectionClient.deleteMany}. Optional, as `saveMany` is.\n */\n deleteMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteManyProps<M>): Promise<void>;\n\n /**\n * Apply a mixed list of writes across collections as one unit of work.\n *\n * The capability `saveMany` and `deleteMany` cannot express between them: a\n * batch that touches two tables. Sent as two requests those can\n * half-succeed, and the recovery — read back, work out which half landed,\n * undo it — is code nobody writes.\n *\n * Every operation runs the pipeline its single-row equivalent runs, in\n * order, in one transaction, under the caller's own role. Operations may\n * carry `{ \"$ref\": \"<name>.<field>\" }` markers in `values` or `id`, which\n * the driver resolves against the rows earlier operations wrote — the\n * driver, because inside the transaction is the only place those rows\n * exist. `@rebasepro/server` exports `resolveBatchRefs` so the resolution\n * is one implementation rather than one per driver.\n *\n * Resolves to one entry per operation, aligned to the input: the written\n * row for a create, update or upsert, and `null` for a delete.\n *\n * Optional for the same reason `saveMany` is: a driver that cannot make it\n * atomic must not pretend to. The REST layer answers `BATCH_UNSUPPORTED`\n * rather than falling back to a loop, which would be the non-atomic\n * sequence the caller reached for this to avoid.\n */\n batchWrite?<M extends Record<string, unknown> = Record<string, unknown>>(\n props: BatchWriteProps<M>\n ): Promise<(Record<string, unknown> | null)[]>;\n\n /**\n * Check if the given property is unique in the given collection\n * @param path Collection path\n * @param name of the property\n * @param value\n * @param id\n * @param collection\n * @return `true` if there are no other fields besides the given entity\n */\n checkUniqueField(\n path: string,\n name: string,\n value: unknown,\n id?: string | number,\n collection?: CollectionConfig\n ): Promise<boolean>;\n\n /**\n * Count the number of entities in a collection\n */\n count?<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<number>;\n\n /**\n * Run several reads as one: `work` is handed a driver whose reads all go\n * through a single read-only transaction that reads one snapshot, under\n * this driver's identity and row-level security, and its result is\n * returned.\n *\n * A list page is a read and a count, and each was a transaction of its\n * own — the second checkout, `BEGIN`, identity setup and `COMMIT` cost as\n * many round trips as the count itself. On one snapshot the two also\n * agree: `meta.total` describes the rows the page was taken from, and an\n * `ETag` the row the body was rendered from.\n *\n * Optional: a driver without it is called directly, read by read.\n */\n readTogether?<T>(work: (driver: DataDriver) => Promise<T>): Promise<T>;\n\n /**\n * Check if the given filter combination is valid\n * @param props\n */\n isFilterCombinationValid?(props: Omit<FilterCombinationValidProps, \"collection\"> & {\n databaseId?: string\n }): boolean;\n\n /**\n * Get the object to generate the current time in the driver\n */\n currentTime?: () => unknown;\n\n delegateToCMSModel?: (data: unknown) => unknown;\n\n cmsToDelegateModel?: (data: unknown) => unknown;\n\n initTextSearch?: (props: {\n context: RebaseCallContext,\n path: string,\n databaseId?: string,\n collection: CollectionConfig,\n parentCollectionSlugs?: string[];\n parentEntityIds?: string[];\n }) => Promise<boolean>;\n\n /**\n * Flag to indicate if the driver has requested the initialization of the text search index\n */\n needsInitTextSearch?: boolean;\n\n // ── REST fetch capabilities ─────────────────────────────────────────\n\n /**\n * Optional REST-optimised fetch service. When present, the REST API\n * generator uses these methods instead of the generic `fetchOne` /\n * `fetchCollection` pipeline, enabling include-aware eager-loading.\n */\n restFetchService?: RestFetchService;\n\n /**\n * This driver reads the row an update or a delete addresses inside that\n * write's own transaction, and runs {@link SaveProps.precondition} /\n * {@link DeleteProps.precondition} on it there. A caller may then leave its\n * own read of the row out. A driver without it ignores the precondition,\n * so a caller must read the row itself.\n */\n readonly checksRowPreconditions?: boolean;\n\n // ── Admin capabilities ─────────────────────────────────────────────\n //\n // Admin operations are now modelled as capability-specific interfaces\n // (SQLAdmin, DocumentAdmin, SchemaAdmin) in `@rebasepro/types/backend`.\n //\n // Drivers that support admin features should expose them here.\n // Consumers should use the `isSQLAdmin()`, `isSchemaAdmin()` etc.\n // type guards to safely narrow the type before calling methods.\n\n /**\n * Return the admin capabilities of this driver.\n * @see SQLAdmin\n * @see DocumentAdmin\n * @see SchemaAdmin\n */\n admin?: import(\"../types/backend\").DatabaseAdmin;\n\n}\n\n/**\n * REST-optimised fetch service exposed by drivers that support\n * eager-loading of relations via `include`.\n *\n * The methods return flattened rows — exactly the table's columns, under their\n * own names and with the types the database returned — and included relations\n * inlined as plain nested rows. This is the shape served to app developers\n * through the REST API / SDK client.\n *\n * No synthesized `id`: identity is a primary key, which may be named anything\n * and span several columns, so an address is derived by whoever needs one (see\n * `buildCompositeId`) rather than written into the row on top of the data.\n *\n * @group DataDriver\n */\nexport interface RestFetchService {\n /**\n * Fetch a collection of flattened entities with optional relation includes.\n */\n fetchCollectionForRest(\n collectionPath: string,\n options?: {\n filter?: FilterValues<string>;\n /** An `or(...)`/`and(...)` group, applied alongside `filter`. */\n logical?: LogicalCondition;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: Record<string, unknown>;\n searchString?: string;\n /** Ask each row which declared search fields matched — populates `_matches`. */\n searchExplain?: boolean;\n databaseId?: string;\n vectorSearch?: VectorSearchParams;\n /** See {@link FetchCollectionProps.withDeleted}. */\n withDeleted?: boolean | \"only\";\n /**\n * Columns to read. A projection pushed into the SELECT, not a trim\n * of the response — `excludeFromApi` still applies on top, and the\n * primary key is always read whether or not it is named.\n */\n fields?: string[];\n /** `SELECT DISTINCT` over the projection. See `FindParams.distinct`. */\n distinct?: boolean;\n },\n include?: IncludeSpec\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * The opaque cursor that continues a listing after `row`.\n *\n * On the driver rather than the route because deriving it needs the\n * collection's primary key — which may be named anything and span several\n * columns — and that is the driver's knowledge. The route holds the last\n * row and the sort keys and asks for the string.\n *\n * `undefined` where no cursor can describe the page: an ordering with no\n * stored value to compare against (relevance), or a row missing a value for\n * one of the sort keys. The listing then reports no `nextCursor` and the\n * caller pages by offset, which is what it did before cursors existed.\n *\n * Optional: a driver that cannot seek simply never issues one, and\n * `meta.nextCursor` is absent for every read it serves.\n */\n cursorFor?(\n collectionPath: string,\n row: Record<string, unknown>,\n orderBy?: OrderByTuple[]\n ): string | undefined;\n\n /**\n * `count`/`sum`/`avg`/`min`/`max` over the rows a filter selects,\n * optionally grouped.\n *\n * Optional, and the REST route answers 501 where a driver does not\n * implement it — an aggregate is not a thing to approximate, and an empty\n * result set would read as \"nothing matched\".\n *\n * Any implementation **must apply the same row-level authorization as a\n * read**. An aggregate is an efficient way to learn about rows you cannot\n * select, and `count(*)` over a table whose policies would return nothing\n * has to be zero.\n */\n aggregate?(\n collectionPath: string,\n options: {\n aggregates: { fn: \"count\" | \"sum\" | \"avg\" | \"min\" | \"max\"; field?: string; alias: string }[];\n groupBy?: string[];\n filter?: FilterValues<string>;\n logical?: LogicalCondition;\n searchString?: string;\n /** Groups to return. Applies to a grouped aggregate; one without `groupBy` is one row. */\n limit?: number;\n /** Groups to skip, for paging past `limit`. Grouped aggregates only. */\n offset?: number;\n /**\n * Sort for the groups, by a `groupBy` field or an aggregate's alias\n * (`count`, `sum_total`). Whatever it leaves tied is ordered by the\n * group keys, so the order is total and a page boundary falls in\n * the same place every time. Grouped aggregates only.\n */\n orderBy?: OrderByTuple[];\n /** See {@link FetchCollectionProps.withDeleted}. */\n withDeleted?: boolean | \"only\";\n }\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch a single flattened entity with optional relation includes.\n */\n fetchOneForRest(\n collectionPath: string,\n id: string | number,\n include?: IncludeSpec,\n databaseId?: string,\n options?: {\n /** See `FetchCollectionProps.fields`. */\n fields?: string[];\n /** See {@link FetchOneProps.withDeleted}. */\n withDeleted?: boolean | \"only\";\n }\n ): Promise<Record<string, unknown> | null>;\n}\n","//#region src/strings.ts\nvar tokenizeRegex = /[A-Z]{2,}(?=[A-Z][a-z]|\\b)|[A-Z]?[a-z]+|[0-9]+(?:[a-z](?![a-z]))?|[A-Z]/g;\nvar toKebabCase = (str) => {\n\tif (!str || typeof str !== \"string\") return \"\";\n\tconst regExpMatchArray = str.match(tokenizeRegex);\n\tif (!regExpMatchArray) return \"\";\n\treturn regExpMatchArray.map((x) => x.toLowerCase()).join(\"-\");\n};\nvar snakeCaseRegex = tokenizeRegex;\nvar toSnakeCase = (str) => {\n\tif (!str || typeof str !== \"string\") return \"\";\n\tconst regExpMatchArray = str.match(snakeCaseRegex);\n\tif (!regExpMatchArray) return \"\";\n\treturn regExpMatchArray.map((x) => x.toLowerCase()).join(\"_\");\n};\nfunction camelCase(str) {\n\tif (!str) return \"\";\n\tif (str.length === 1) return str.toLowerCase();\n\tconst parts = str.split(/[-_ ]+/).filter(Boolean);\n\tif (parts.length === 0) return \"\";\n\treturn parts[0].toLowerCase() + parts.slice(1).map((part) => part.charAt(0).toUpperCase() + part.substring(1).toLowerCase()).join(\"\");\n}\n/**\n* A random base-36 string of exactly `strLength` characters.\n*\n* Not `Math.random().toString(36).slice(2, 2 + strLength)`: that has no\n* guaranteed length. Base-36 of a double drops trailing zeros, so the source\n* string is short about once in 36 calls and the slice quietly returns fewer\n* characters than asked for — `randomString(10)` returning 9. These values\n* prefix uploaded filenames to keep them apart, so a short one is a likelier\n* collision, and it fails at the rate that makes a test look flaky.\n*/\nfunction randomString(strLength = 5) {\n\tconst alphabet = \"0123456789abcdefghijklmnopqrstuvwxyz\";\n\tlet result = \"\";\n\tfor (let i = 0; i < strLength; i++) result += alphabet.charAt(Math.floor(Math.random() * 36));\n\treturn result;\n}\nfunction randomColor() {\n\treturn Math.floor(Math.random() * 16777215).toString(16);\n}\nfunction slugify(text, separator = \"_\", lowercase = true) {\n\tif (!text) return \"\";\n\tconst from = \"ãàáäâẽèéëêìíïîõòóöôùúüûñç·/_,:;-\";\n\tconst to = `aaaaaeeeeeiiiiooooouuuunc${separator}${separator}${separator}${separator}${separator}${separator}${separator}`;\n\tfor (let i = 0, l = 32; i < l; i++) text = text.replace(new RegExp(from.charAt(i), \"g\"), to.charAt(i));\n\ttext = text.toString().trim().replace(/^\\s+|\\s+$/g, \"\").replace(/\\s+/g, separator).replace(/&/g, separator).replace(/[^\\w\\\\-]+/g, \"\").replace(new RegExp(\"\\\\\" + separator + \"\\\\\" + separator + \"+\", \"g\"), separator);\n\treturn lowercase ? text.toLowerCase() : text;\n}\nfunction unslugify(slug) {\n\tif (!slug) return \"\";\n\tif (slug.includes(\"-\") || slug.includes(\"_\") || !slug.includes(\" \")) return slug.replace(/[-_]/g, \" \").replace(/\\w\\S*/g, function(txt) {\n\t\treturn txt.charAt(0).toUpperCase() + txt.substring(1);\n\t}).trim();\n\telse return slug.trim();\n}\n/**\n* Are these two identifiers one edit apart, ignoring case?\n*\n* Tight on purpose. A suggester that fires on two edits offers `columnWidth`\n* for `colWith` and `readOnly` for `required`, and a \"did you mean\" that is\n* usually wrong teaches people to stop reading them. One edit covers what\n* actually happens — a dropped letter (`multilne`), a doubled one, a transposed\n* pair (`validaton`), a wrong case — and nothing else.\n*\n* Lives here rather than beside either caller because both the CLI's manifest\n* reader and the server's collection validator answer the same question about\n* an unknown key, and two copies of a similarity rule is two rules the moment\n* one of them is tuned.\n*/\nfunction isNearMiss(a, b) {\n\tconst x = a.toLowerCase();\n\tconst y = b.toLowerCase();\n\tif (x === y) return true;\n\tif (Math.abs(x.length - y.length) > 1) return false;\n\tconst [shorter, longer] = x.length <= y.length ? [x, y] : [y, x];\n\tlet i = 0;\n\tlet j = 0;\n\tlet edits = 0;\n\twhile (i < shorter.length && j < longer.length) {\n\t\tif (shorter[i] === longer[j]) {\n\t\t\ti++;\n\t\t\tj++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (++edits > 1) return false;\n\t\tif (shorter.length === longer.length) i++;\n\t\tj++;\n\t}\n\treturn edits + (longer.length - j) + (shorter.length - i) <= 1;\n}\n/**\n* The key in `known` that `key` was probably meant to be, if any.\n*\n* `undefined` when nothing is close — which is the common case for a key that\n* is simply from a newer version, and the reason the caller must be able to say\n* \"unknown\" without saying \"wrong\".\n*/\nfunction suggestNearMiss(known, key) {\n\treturn known.find((candidate) => isNearMiss(candidate, key));\n}\nfunction prettifyIdentifier(input) {\n\tif (!input) return \"\";\n\tlet text = input;\n\ttext = text.replace(/([a-z])([A-Z])|([A-Z])([A-Z][a-z])/g, \"$1$3 $2$4\");\n\ttext = text.replace(/[_-]+/g, \" \");\n\treturn text.trim().replace(/\\b\\w/g, (char) => char.toUpperCase());\n}\n/**\n* How many terms one search string is allowed to contribute.\n*\n* Every term multiplies the work: the substring fallback compiles to one\n* `ILIKE '%term%'` per term *per searchable column*, evaluated per row on a\n* sequential scan. Eight is far past what a person types into a search box and\n* still bounds a pasted paragraph to a fixed number of predicates. Terms past\n* the cap are dropped rather than rejected — dropping only widens the result\n* set, and a search box that errors on a long paste is worse than one that\n* matches on its first eight words.\n*/\nvar MAX_SEARCH_TERMS = 8;\n/**\n* The terms a user meant when they typed into a search box.\n*\n* A search box is not a substring box. Typing `sebastian melendez` on a\n* collection whose name is split across `first_name` and `last_name` has to\n* find the row, and matching the whole string against each field separately\n* cannot: no single column holds both words. Splitting on whitespace and\n* requiring *every* term to match *some* field is what does, and it is what\n* `websearch_to_tsquery` already does for collections that declared a `search`\n* block — so this is also what keeps the two search paths answering the same\n* question.\n*\n* It fixes the trailing space too: `sebastian ` matched nothing at all, because\n* the space was part of the pattern.\n*\n* A double-quoted run is one term, again matching the full-text path, so a\n* caller who really does want the phrase can ask for it. An unbalanced quote is\n* stripped rather than honoured — the alternative is that everything after a\n* stray `\"` silently becomes one long term that matches nothing.\n*\n* Returns an empty array for a string with no terms in it at all (whitespace,\n* or nothing but quotes); the caller decides what an empty search means, since\n* only it knows whether \"no constraint\" or \"no rows\" is the safe reading.\n*/\nfunction splitSearchTerms(searchString, maxTerms = MAX_SEARCH_TERMS) {\n\tconst terms = [];\n\tconst pattern = /\"([^\"]*)\"|(\\S+)/g;\n\tlet match;\n\twhile ((match = pattern.exec(searchString)) !== null) {\n\t\tconst term = (match[1] ?? match[2].replace(/\"/g, \"\")).trim();\n\t\tif (!term) continue;\n\t\tterms.push(term);\n\t\tif (terms.length >= maxTerms) break;\n\t}\n\treturn terms;\n}\n//#endregion\nexport { MAX_SEARCH_TERMS, camelCase, isNearMiss, prettifyIdentifier, randomColor, randomString, slugify, splitSearchTerms, suggestNearMiss, toKebabCase, toSnakeCase, unslugify };\n\n//# sourceMappingURL=strings.es.js.map"],"mappings":"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"}
@@ -0,0 +1,172 @@
1
+ import { createRequire as __rebaseCreateRequire } from "module";
2
+ import __rebaseProcess from "process";
3
+ globalThis.process ??= __rebaseProcess;
4
+ __rebaseCreateRequire(import.meta.url);
5
+ import { t as ApiError } from "./errors-D6_y86c5.js";
6
+ import fs from "node:fs";
7
+ //#region src/storage/types.ts
8
+ /**
9
+ * Default maximum file size (50MB)
10
+ */
11
+ var DEFAULT_MAX_FILE_SIZE = 50 * 1024 * 1024;
12
+ /**
13
+ * Common image MIME types
14
+ */
15
+ var IMAGE_MIME_TYPES = [
16
+ "image/jpeg",
17
+ "image/png",
18
+ "image/gif",
19
+ "image/webp",
20
+ "image/svg+xml",
21
+ "image/bmp",
22
+ "image/tiff"
23
+ ];
24
+ /**
25
+ * Common document MIME types
26
+ */
27
+ var DOCUMENT_MIME_TYPES = [
28
+ "application/pdf",
29
+ "application/msword",
30
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
31
+ "application/vnd.ms-excel",
32
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
33
+ "application/vnd.ms-powerpoint",
34
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
35
+ "text/plain",
36
+ "text/csv"
37
+ ];
38
+ //#endregion
39
+ //#region src/storage/streams.ts
40
+ /**
41
+ * Moving object bytes without holding them.
42
+ *
43
+ * A stored object can be as large as the deployment's `maxFileSize` allows — a
44
+ * video, a database dump — and the paths that used to read one whole (a
45
+ * `File`, an `arrayBuffer()`, a `Buffer.concat`) held it three or four times
46
+ * over per request. These helpers are what the streamed paths share instead.
47
+ */
48
+ /**
49
+ * A byte source as a web stream that reads a chunk only when the consumer asks
50
+ * for one, and releases the source when the consumer stops early.
51
+ *
52
+ * Pull-based with a zero high-water mark, so nothing is read ahead of the
53
+ * reader: a slow client holds one chunk, not a queue of them. A source given
54
+ * as a function is opened on the first read, which keeps a stream nobody reads
55
+ * — a `HEAD`, a 304 decided late — from holding a file descriptor or a
56
+ * connection open. Cancelling (a client that disconnects mid-download)
57
+ * returns the iterator, which destroys a Node stream and so closes its file or
58
+ * socket.
59
+ */
60
+ function webStreamFrom(source) {
61
+ let iterator;
62
+ const open = () => {
63
+ iterator ??= (typeof source === "function" ? source() : source)[Symbol.asyncIterator]();
64
+ return iterator;
65
+ };
66
+ return new ReadableStream({
67
+ async pull(controller) {
68
+ try {
69
+ const next = await open().next();
70
+ if (next.done) controller.close();
71
+ else controller.enqueue(next.value);
72
+ } catch (err) {
73
+ controller.error(err);
74
+ }
75
+ },
76
+ async cancel() {
77
+ if (!iterator && typeof source === "function") return;
78
+ try {
79
+ await open().return?.();
80
+ } catch {}
81
+ }
82
+ }, { highWaterMark: 0 });
83
+ }
84
+ /**
85
+ * A file on disk, or an inclusive byte range of it, as a web stream. The file
86
+ * is opened on the first read; see {@link webStreamFrom}.
87
+ */
88
+ function fileWebStream(filePath, range) {
89
+ return webStreamFrom(() => fs.createReadStream(filePath, range ? {
90
+ start: range.start,
91
+ end: range.end
92
+ } : {}));
93
+ }
94
+ /** A buffer already in memory as a web stream of views onto it, never copies. */
95
+ function bufferWebStream(bytes, chunkSize = 64 * 1024) {
96
+ let offset = 0;
97
+ return new ReadableStream({ pull(controller) {
98
+ if (offset >= bytes.byteLength) {
99
+ controller.close();
100
+ return;
101
+ }
102
+ const end = Math.min(offset + chunkSize, bytes.byteLength);
103
+ controller.enqueue(bytes.subarray(offset, end));
104
+ offset = end;
105
+ } }, { highWaterMark: 0 });
106
+ }
107
+ /**
108
+ * Read a stream into one buffer.
109
+ *
110
+ * For the one consumer that needs the whole object at once — an image decode.
111
+ * With the size known the bytes are copied once, into a buffer of that size,
112
+ * rather than collected and then concatenated (which holds them twice); a
113
+ * stream that turns out longer or shorter than announced still comes back
114
+ * whole and exact.
115
+ */
116
+ async function readStreamFully(stream, expectedSize) {
117
+ const reader = stream.getReader();
118
+ const target = expectedSize !== void 0 && expectedSize > 0 ? Buffer.allocUnsafe(expectedSize) : void 0;
119
+ let offset = 0;
120
+ let overflow;
121
+ try {
122
+ for (;;) {
123
+ const { done, value } = await reader.read();
124
+ if (done) break;
125
+ if (target && !overflow && offset + value.byteLength <= target.byteLength) target.set(value, offset);
126
+ else {
127
+ overflow ??= target ? [target.subarray(0, offset)] : [];
128
+ overflow.push(value);
129
+ }
130
+ offset += value.byteLength;
131
+ }
132
+ } finally {
133
+ reader.releaseLock();
134
+ }
135
+ if (overflow) return Buffer.concat(overflow, offset);
136
+ if (!target) return Buffer.alloc(0);
137
+ return offset === target.byteLength ? target : target.subarray(0, offset);
138
+ }
139
+ /**
140
+ * The bytes of a buffer as a `Uint8Array` the response APIs accept, without
141
+ * copying them. `new Uint8Array(buffer)` copies; a view does not. Only a buffer
142
+ * over shared memory — which nothing here produces — is copied.
143
+ */
144
+ function bytesView(bytes) {
145
+ const backing = bytes.buffer;
146
+ return backing instanceof ArrayBuffer ? new Uint8Array(backing, bytes.byteOffset, bytes.byteLength) : new Uint8Array(bytes);
147
+ }
148
+ //#endregion
149
+ //#region src/storage/upload-validation.ts
150
+ /**
151
+ * What every storage controller checks about a file before it stores it: the
152
+ * source's own size cap and, when configured, its allowed content types.
153
+ *
154
+ * One function for the three controllers, which carried a copy each. Both
155
+ * refusals are `ApiError`s, so they reach the caller as what they are. A plain
156
+ * `Error` was a 500 "Internal Server Error" on `/upload` and a 502 on TUS —
157
+ * after the client had sent every byte.
158
+ */
159
+ function validateUploadedFile(file, maxSize, allowedMimeTypes) {
160
+ if (file.size > maxSize) throw new ApiError(413, "PAYLOAD_TOO_LARGE", `File size ${file.size} exceeds this storage source's maximum of ${maxSize} bytes.`, {
161
+ size: file.size,
162
+ maxFileSize: maxSize
163
+ });
164
+ if (allowedMimeTypes && allowedMimeTypes.length > 0 && !allowedMimeTypes.includes(file.type)) throw ApiError.badRequest(`File type ${file.type || "(none)"} is not allowed on this storage source. Allowed types: ${allowedMimeTypes.join(", ")}.`, "STORAGE_FILE_TYPE_REFUSED", {
165
+ allowedMimeTypes,
166
+ contentType: file.type
167
+ });
168
+ }
169
+ //#endregion
170
+ export { readStreamFully as a, DOCUMENT_MIME_TYPES as c, fileWebStream as i, IMAGE_MIME_TYPES as l, bufferWebStream as n, webStreamFrom as o, bytesView as r, DEFAULT_MAX_FILE_SIZE as s, validateUploadedFile as t };
171
+
172
+ //# sourceMappingURL=upload-validation-Dd-fnsbA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-validation-Dd-fnsbA.js","names":[],"sources":["../src/storage/types.ts","../src/storage/streams.ts","../src/storage/upload-validation.ts"],"sourcesContent":["/**\n * Storage configuration and types for Rebase backend\n */\n\nimport { StorageSource, UploadFileProps, UploadFileResult, DownloadConfig, DownloadMetadata, StorageListResult, StorageReference } from \"@rebasepro/types\";\n\n/**\n * Local filesystem storage configuration\n */\nexport interface LocalStorageConfig {\n type: \"local\";\n /** Base directory for file storage (e.g., './uploads') */\n basePath: string;\n /** Maximum file size in bytes (default: 50MB) */\n maxFileSize?: number;\n /** Allowed MIME types (if not set, all types allowed) */\n allowedMimeTypes?: string[];\n /** Base URL for generating download URLs (default: auto-detected from request) */\n baseUrl?: string;\n /**\n * Set when this local backend stands in for an object store the source\n * declared and nothing bound — `bucket(\"media\", { engine: \"s3\" })` with no\n * `S3_BUCKET__MEDIA` in a development process. Names the engine it stands\n * in for, so boot can say so and `rebase status` can show it. Never set in\n * production, where an unbound object store stays unbound.\n */\n standsInFor?: string;\n /**\n * Set when nothing chose this backend: no `STORAGE_TYPE` and no declared\n * engine, so local disk is only the default a source falls back to.\n * Production drops a local backend either way. This only decides how that\n * is reported: an implicit default being dropped means the project uses no\n * storage, which is not a misconfiguration.\n */\n implicit?: boolean;\n}\n\n/**\n * S3-compatible storage configuration (works with AWS S3 and MinIO)\n */\nexport interface S3StorageConfig {\n type: \"s3\";\n /** S3 bucket name */\n bucket: string;\n /** AWS region (e.g., 'us-east-1') */\n region?: string;\n /** Custom endpoint URL (required for MinIO, Cloudflare R2, Hetzner Object Storage) */\n endpoint?: string;\n /** AWS access key ID */\n accessKeyId: string;\n /** AWS secret access key */\n secretAccessKey: string;\n /** Use path-style URLs (required for MinIO) */\n forcePathStyle?: boolean;\n /** Maximum file size in bytes (default: 50MB) */\n maxFileSize?: number;\n /** Allowed MIME types (if not set, all types allowed) */\n allowedMimeTypes?: string[];\n}\n\n/**\n * Google Cloud Storage configuration (works with GCS and Firebase Storage)\n */\nexport interface GCSStorageConfig {\n type: \"gcs\";\n /** GCS bucket name (e.g. \"my-project.appspot.com\" for Firebase Storage) */\n bucket: string;\n /** GCP project ID (optional, auto-detected from credentials) */\n projectId?: string;\n /** Path to service account JSON key file */\n keyFilename?: string;\n /** Service account credentials object (alternative to keyFilename) */\n credentials?: { client_email: string; private_key: string; project_id?: string };\n /** Maximum file size in bytes (default: 50MB) */\n maxFileSize?: number;\n /** Allowed MIME types */\n allowedMimeTypes?: string[];\n}\n\n/**\n * Storage configuration — local filesystem, S3-compatible, or Google Cloud Storage.\n *\n * **Built-in providers:**\n * - `local` — Zero-config filesystem storage. Great for dev and single-server deployments (Hetzner, bare metal).\n * - `s3` — Any S3-compatible provider. AWS S3, Cloudflare R2, MinIO, Hetzner Object Storage,\n * Backblaze B2, DigitalOcean Spaces, and even GCS (via its S3-compatible interoperability API).\n * - `gcs` — Native Google Cloud Storage / Firebase Storage via `@google-cloud/storage`.\n *\n * **Custom providers:**\n * For other cloud storage (Azure Blob, etc.), implement the `StorageController`\n * interface and pass the instance directly to the `storage` config.\n */\nexport type BackendStorageConfig = LocalStorageConfig | S3StorageConfig | GCSStorageConfig;\n\n/** What a storage request is trying to do to an object. */\nexport type {\n StorageOperation,\n StorageAuthorizeUser,\n StorageAuthorizeContext,\n StorageAuthorizeData,\n StorageAuthorize\n} from \"@rebasepro/types\";\n\n/**\n * How the server writes an object. See {@link StorageController.putObject}.\n */\nexport interface PutObjectOptions {\n /**\n * Hold this write to the controller's upload limits: `maxFileSize` and\n * `allowedMimeTypes`. Defaults to `true`.\n *\n * Those limits say what a user may upload. A write the server makes for\n * itself, such as a database backup, passes `false`. A dump is neither of\n * an allowed type nor, past a small database, under 50 MB. The upload\n * routes never pass it.\n */\n uploadLimits?: boolean;\n}\n\n/**\n * Storage controller interface for backend implementations\n */\nexport interface StorageController {\n /**\n * Upload an object.\n *\n * A write is held to the controller's upload limits — `maxFileSize` and\n * `allowedMimeTypes` — unless `options.uploadLimits` is `false`. Only the\n * server's own writes pass that; nothing a request sends reaches it.\n */\n putObject(props: UploadFileProps, options?: PutObjectOptions): Promise<UploadFileResult>;\n\n /**\n * Get a download URL (signed URL equivalent) for an object.\n *\n * For server-side code only: no route hands this URL to a client. A\n * client reads a private object through `/file/*` with the download\n * token `GET /metadata/*` mints. The built-in S3 and GCS controllers sign\n * it for an hour.\n */\n getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig>;\n\n /**\n * Describe an object — size, type, custom metadata — without producing a\n * URL for it, or `null` when there is no such object.\n *\n * `GET /metadata/*` wants only this: the client builds its own `/file/*`\n * URL and spends the download token the route mints. Asking\n * `getSignedUrl` for it made every metadata read sign a URL nobody used,\n * and on GCS signing is not free — without a key file the client asks the\n * IAM Credentials API to sign, which needs `iam.serviceAccounts.signBlob`\n * on the runtime account itself. Cloud Run does not grant that, so every\n * private object answered 500 there.\n *\n * A controller that does not implement this keeps the old behaviour: the\n * route falls back to `getSignedUrl` and reads its `metadata`.\n */\n getMetadata?(key: string, bucket?: string): Promise<DownloadMetadata | null>;\n\n /**\n * Get object as a File\n */\n getObject(key: string, bucket?: string): Promise<File | null>;\n\n /**\n * Describe an object for serving it — its size, stored type and version —\n * without reading its bytes, or `null` when there is no such object.\n *\n * Optional, together with {@link getObjectStream}. A controller that has\n * both is served streamed: `GET /file/*` answers a `304`, a `416`, a `HEAD`\n * and a cached image rendition from this alone, and otherwise reads only\n * the bytes it sends — the requested range, a chunk at a time. One without\n * them is served through {@link getObject}, which holds the whole object in\n * memory for every request, a `Range` request for one megabyte included.\n */\n statObject?(key: string, bucket?: string): Promise<StoredObjectInfo | null>;\n\n /**\n * Read an object, or one byte range of it, as a stream, or `null` when\n * there is no such object. See {@link statObject}.\n *\n * The stream holds a file or a connection until it is read to the end or\n * cancelled, so a caller that decides not to read it must cancel it.\n */\n getObjectStream?(key: string, options?: { bucket?: string; range?: StoredObjectRange }): Promise<StoredObjectStream | null>;\n\n /**\n * Delete an object\n */\n deleteObject(key: string, bucket?: string): Promise<void>;\n\n /**\n * List objects in a prefix\n */\n listObjects(prefix: string, options?: {\n bucket?: string;\n maxResults?: number;\n pageToken?: string;\n }): Promise<StorageListResult>;\n\n /**\n * Get the storage provider identifier.\n *\n * Built-in values are `'local'` and `'s3'`. Custom implementations\n * should return their own identifier (e.g. `'gcs'`, `'azure'`).\n */\n getType(): string;\n\n /**\n * The buckets this controller serves, if it knows.\n *\n * The `bucket` a request may name is not a free-text field: on S3 and GCS it\n * goes straight to the provider, so an unvalidated one addresses *any*\n * bucket the deployment's credentials can reach, and locally it names a\n * directory the framework would create on demand. Either way, a caller who\n * mistypes it gets \"file not found\" — the same answer as a missing object —\n * with nothing to say the bucket was the problem.\n *\n * So a controller that knows its buckets says so, and the routes refuse\n * anything else with `UNKNOWN_STORAGE_SOURCE` naming what is served. A\n * controller that does not implement this keeps the old behaviour and is\n * handed whatever the caller wrote — a custom implementation may address\n * buckets this framework has no way to enumerate.\n *\n * `\"default\"` is always among them: it is the logical name every route uses\n * when the caller names none, and each provider maps it to its own.\n */\n knownBuckets?(): string[];\n\n /**\n * The largest file this controller accepts, in bytes.\n *\n * Every door that takes a file reads it, so the number a client is told is\n * the number it is held to: the upload route checks it before anything is\n * stored, the resumable route checks it at creation — before the first\n * chunk rather than after the last — and advertises it as `Tus-Max-Size`,\n * and the server's upload body limit is the largest of them. A controller\n * that does not implement this is held to the deployment-wide limit alone.\n */\n maxFileSize?(): number;\n}\n\n/**\n * What serving an object needs to know before deciding whether to read it.\n * See {@link StorageController.statObject}.\n */\nexport interface StoredObjectInfo {\n /** The whole object's size in bytes. */\n size: number;\n /** The stored content type — the uploader's claim, not a sniffed one. */\n contentType: string;\n /**\n * The object's own modification time, in milliseconds since the epoch.\n * The routes build `ETag`, `Last-Modified` and the transform cache key from\n * it, so it must be the object's version, not the time of the read.\n */\n lastModified: number;\n}\n\n/** An inclusive byte range, as `Range: bytes=start-end` names one. */\nexport interface StoredObjectRange {\n start: number;\n end: number;\n}\n\n/** An object's bytes as a stream. See {@link StorageController.getObjectStream}. */\nexport interface StoredObjectStream extends StoredObjectInfo {\n /** The bytes: the range when {@link range} is set, the whole object otherwise. */\n stream: ReadableStream<Uint8Array>;\n /**\n * The range {@link stream} holds — the one asked for — or absent when it\n * holds the whole object. A controller may answer a range with the whole\n * object, as HTTP lets a server do; the caller then serves it whole.\n */\n range?: StoredObjectRange;\n}\n\n/**\n * Default maximum file size (50MB)\n */\nexport const DEFAULT_MAX_FILE_SIZE = 50 * 1024 * 1024;\n\n/**\n * Common image MIME types\n */\nexport const IMAGE_MIME_TYPES = [\n \"image/jpeg\",\n \"image/png\",\n \"image/gif\",\n \"image/webp\",\n \"image/svg+xml\",\n \"image/bmp\",\n \"image/tiff\"\n];\n\n/**\n * Common document MIME types\n */\nexport const DOCUMENT_MIME_TYPES = [\n \"application/pdf\",\n \"application/msword\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n \"application/vnd.ms-excel\",\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n \"application/vnd.ms-powerpoint\",\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n \"text/plain\",\n \"text/csv\"\n];\n","/**\n * Moving object bytes without holding them.\n *\n * A stored object can be as large as the deployment's `maxFileSize` allows — a\n * video, a database dump — and the paths that used to read one whole (a\n * `File`, an `arrayBuffer()`, a `Buffer.concat`) held it three or four times\n * over per request. These helpers are what the streamed paths share instead.\n */\nimport fs from \"node:fs\";\n\n/** Anything that hands over bytes a chunk at a time: a Node readable, an SDK body. */\ntype ByteSource = AsyncIterable<Uint8Array>;\n\n/**\n * A byte source as a web stream that reads a chunk only when the consumer asks\n * for one, and releases the source when the consumer stops early.\n *\n * Pull-based with a zero high-water mark, so nothing is read ahead of the\n * reader: a slow client holds one chunk, not a queue of them. A source given\n * as a function is opened on the first read, which keeps a stream nobody reads\n * — a `HEAD`, a 304 decided late — from holding a file descriptor or a\n * connection open. Cancelling (a client that disconnects mid-download)\n * returns the iterator, which destroys a Node stream and so closes its file or\n * socket.\n */\nexport function webStreamFrom(source: ByteSource | (() => ByteSource)): ReadableStream<Uint8Array> {\n let iterator: AsyncIterator<Uint8Array> | undefined;\n const open = (): AsyncIterator<Uint8Array> => {\n iterator ??= (typeof source === \"function\" ? source() : source)[Symbol.asyncIterator]();\n return iterator;\n };\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const next = await open().next();\n if (next.done) controller.close();\n else controller.enqueue(next.value);\n } catch (err) {\n controller.error(err);\n }\n },\n async cancel() {\n // A source never opened has nothing to release — and opening it\n // now only to close it would be the cost this avoids.\n if (!iterator && typeof source === \"function\") return;\n try {\n await open().return?.();\n } catch {\n // Already failed or closed; either way it is released.\n }\n }\n }, { highWaterMark: 0 });\n}\n\n/**\n * A file on disk, or an inclusive byte range of it, as a web stream. The file\n * is opened on the first read; see {@link webStreamFrom}.\n */\nexport function fileWebStream(filePath: string, range?: { start: number; end: number }): ReadableStream<Uint8Array> {\n return webStreamFrom(() => fs.createReadStream(filePath, range ? { start: range.start, end: range.end } : {}));\n}\n\n/** A buffer already in memory as a web stream of views onto it, never copies. */\nexport function bufferWebStream(bytes: Uint8Array, chunkSize = 64 * 1024): ReadableStream<Uint8Array> {\n let offset = 0;\n return new ReadableStream<Uint8Array>({\n pull(controller) {\n if (offset >= bytes.byteLength) {\n controller.close();\n return;\n }\n const end = Math.min(offset + chunkSize, bytes.byteLength);\n controller.enqueue(bytes.subarray(offset, end));\n offset = end;\n }\n }, { highWaterMark: 0 });\n}\n\n/**\n * Read a stream into one buffer.\n *\n * For the one consumer that needs the whole object at once — an image decode.\n * With the size known the bytes are copied once, into a buffer of that size,\n * rather than collected and then concatenated (which holds them twice); a\n * stream that turns out longer or shorter than announced still comes back\n * whole and exact.\n */\nexport async function readStreamFully(stream: ReadableStream<Uint8Array>, expectedSize?: number): Promise<Buffer> {\n const reader = stream.getReader();\n const target = expectedSize !== undefined && expectedSize > 0 ? Buffer.allocUnsafe(expectedSize) : undefined;\n let offset = 0;\n let overflow: Uint8Array[] | undefined;\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (target && !overflow && offset + value.byteLength <= target.byteLength) {\n target.set(value, offset);\n } else {\n overflow ??= target ? [target.subarray(0, offset)] : [];\n overflow.push(value);\n }\n offset += value.byteLength;\n }\n } finally {\n reader.releaseLock();\n }\n if (overflow) return Buffer.concat(overflow, offset);\n if (!target) return Buffer.alloc(0);\n return offset === target.byteLength ? target : target.subarray(0, offset);\n}\n\n/**\n * The bytes of a buffer as a `Uint8Array` the response APIs accept, without\n * copying them. `new Uint8Array(buffer)` copies; a view does not. Only a buffer\n * over shared memory — which nothing here produces — is copied.\n */\nexport function bytesView(bytes: Uint8Array): Uint8Array<ArrayBuffer> {\n const backing = bytes.buffer;\n return backing instanceof ArrayBuffer\n ? new Uint8Array(backing, bytes.byteOffset, bytes.byteLength)\n : new Uint8Array(bytes);\n}\n","/**\n * What every storage controller checks about a file before it stores it: the\n * source's own size cap and, when configured, its allowed content types.\n *\n * One function for the three controllers, which carried a copy each. Both\n * refusals are `ApiError`s, so they reach the caller as what they are. A plain\n * `Error` was a 500 \"Internal Server Error\" on `/upload` and a 502 on TUS —\n * after the client had sent every byte.\n */\n\nimport { ApiError } from \"../api/errors\";\n\nexport function validateUploadedFile(\n file: { size: number; type: string },\n maxSize: number,\n allowedMimeTypes: string[] | undefined\n): void {\n if (file.size > maxSize) {\n throw new ApiError(\n 413,\n \"PAYLOAD_TOO_LARGE\",\n `File size ${file.size} exceeds this storage source's maximum of ${maxSize} bytes.`,\n { size: file.size, maxFileSize: maxSize }\n );\n }\n\n if (allowedMimeTypes && allowedMimeTypes.length > 0 && !allowedMimeTypes.includes(file.type)) {\n // The code a property's `acceptedFiles` refusal answers with: to the\n // caller, \"this source does not take this type\" is the same mistake.\n throw ApiError.badRequest(\n `File type ${file.type || \"(none)\"} is not allowed on this storage source. ` +\n `Allowed types: ${allowedMimeTypes.join(\", \")}.`,\n \"STORAGE_FILE_TYPE_REFUSED\",\n { allowedMimeTypes, contentType: file.type }\n );\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A byte count as an operator reads it in an error message: `10MB`, `1.5MB`,
3
+ * `512KB`, `1000 bytes`.
4
+ *
5
+ * The body-limit messages used to round to whole megabytes, so a limit of
6
+ * 1000 bytes said "Maximum size is 0MB" and one of 512 KiB said "1MB" — the
7
+ * one number the caller needed was wrong in both. Units are binary (1 MB =
8
+ * 1024 × 1024 bytes), as the limits themselves are configured.
9
+ */
10
+ export declare function formatByteSize(bytes: number): string;
@@ -10,6 +10,12 @@ import type { MiddlewareHandler } from "hono";
10
10
  * the identity body, so compressing a 206 desyncs the framing from the bytes
11
11
  * actually sent.
12
12
  *
13
+ * - **Bodies under 1 KB are sent as they are.** Hono's `compress` has that
14
+ * threshold but reads it from `Content-Length`, which `c.json()` never sets,
15
+ * so every row, every 401 and every `{ ok: true }` went through
16
+ * `CompressionStream` — 4.7× the CPU of the request itself, and a 37-byte
17
+ * body came out at 57. {@link measureSmallBody} supplies the length.
18
+ *
13
19
  * No brotli: `CompressionStream` has no "br", so a br-only client would fall
14
20
  * back to identity — every real client sends gzip too.
15
21
  */
@@ -0,0 +1,14 @@
1
+ /**
2
+ * How a function or cron file that failed to import is listed in the boot
3
+ * log: `"<file> (syntax error at <file>:<line>: <message>)"` for a syntax
4
+ * error, `"<file> (threw: <message>)"` for anything else.
5
+ *
6
+ * The two need different remedies. A file that *threw* usually read an
7
+ * undefined env var at module scope; a file with a *syntax error* needs its
8
+ * syntax fixed, and the loaders' advice is keyed on which one this is. Node
9
+ * puts a syntax error's location only in the first line of its stack, so that
10
+ * is where the line number comes from.
11
+ */
12
+ export declare function describeLoadFailure(file: string, err: unknown): string;
13
+ /** The advice for {@link describeLoadFailure}'s syntax-error entries. */
14
+ export declare const SYNTAX_ERROR_ADVICE: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/server",
3
- "version": "0.24.0",
3
+ "version": "0.24.1-canary.gcb918fc",
4
4
  "description": "Database-Agnostic Backend Core for Rebase",
5
5
  "keywords": [
6
6
  "rebase",
@@ -54,10 +54,10 @@
54
54
  "jsonwebtoken": "^9.0.3",
55
55
  "ws": "^8.21.1",
56
56
  "zod": "^4.4.3",
57
- "@rebasepro/client": "0.24.0",
58
- "@rebasepro/common": "0.24.0",
59
- "@rebasepro/utils": "0.24.0",
60
- "@rebasepro/types": "0.24.0"
57
+ "@rebasepro/client": "0.24.1-canary.gcb918fc",
58
+ "@rebasepro/common": "0.24.1-canary.gcb918fc",
59
+ "@rebasepro/types": "0.24.1-canary.gcb918fc",
60
+ "@rebasepro/utils": "0.24.1-canary.gcb918fc"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@jest/globals": "^30.4.1",