@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
@@ -3,15 +3,16 @@ import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
- import "./src-CatHFUym.js";
7
- import { a as parseEnvBoolean, r as isAlreadyExecutingSkip } from "./cron-scheduler-CYQgco86.js";
6
+ import "./src-D5Y3N-S4.js";
7
+ import { a as parseEnvBoolean, r as isAlreadyExecutingSkip } from "./cron-scheduler-idtLPoN-.js";
8
8
  import { r as logger } from "./logger-D-S-hO5e.js";
9
9
  import { r as errorHandler, t as ApiError } from "./errors-D6_y86c5.js";
10
- import { t as S3StorageController } from "./S3StorageController-CAwFRgjV.js";
11
- import { t as GCSStorageController } from "./GCSStorageController-BSiP1c-f.js";
10
+ import { m as jwtOf } from "./jwt-BlvuKoR6.js";
11
+ import { t as S3StorageController } from "./S3StorageController-BNg-pXiY.js";
12
+ import { t as GCSStorageController } from "./GCSStorageController-fv6JAZ87.js";
13
+ import { Hono } from "hono";
12
14
  import fs from "fs";
13
15
  import path from "path";
14
- import { Hono } from "hono";
15
16
  //#region src/backup/backup-common.ts
16
17
  /**
17
18
  * Storage-generic backup helpers used by the admin backup routes.
@@ -125,14 +126,16 @@ function isObjectBackupKey(dest, key) {
125
126
  */
126
127
  async function listBackupObjects(dest, storage) {
127
128
  if (dest.kind === "local") {
128
- if (!fs.existsSync(dest.path)) return [];
129
- const dir = fs.statSync(dest.path).isDirectory() ? dest.path : path.dirname(dest.path);
130
- if (!fs.existsSync(dir)) return [];
131
- const files = fs.readdirSync(dir);
129
+ const stat = await fs.promises.stat(dest.path).catch(() => null);
130
+ if (!stat) return [];
131
+ const dir = stat.isDirectory() ? dest.path : path.dirname(dest.path);
132
+ const files = await fs.promises.readdir(dir);
132
133
  const present = new Set(files);
133
- return files.filter((f) => f.endsWith(DUMP_SUFFIX)).map((f) => {
134
+ const dumps = files.filter((f) => f.endsWith(DUMP_SUFFIX));
135
+ const stats = await mapBounded(dumps, LOCAL_STAT_CONCURRENCY, (f) => fs.promises.stat(path.join(dir, f)));
136
+ return dumps.map((f, i) => {
134
137
  const full = path.join(dir, f);
135
- const st = fs.statSync(full);
138
+ const st = stats[i];
136
139
  const createdAt = parseBackupTimestamp(f) ?? st.mtime;
137
140
  const globals = globalsKeyFor(f);
138
141
  return {
@@ -160,6 +163,21 @@ async function listBackupObjects(dest, storage) {
160
163
  };
161
164
  }).sort(byNewest);
162
165
  }
166
+ /** Stats in flight at once while listing a local backup folder. */
167
+ var LOCAL_STAT_CONCURRENCY = 32;
168
+ /** `fn` over `items`, at most `limit` at a time; results in input order. */
169
+ async function mapBounded(items, limit, fn) {
170
+ const results = new Array(items.length);
171
+ let next = 0;
172
+ const worker = async () => {
173
+ while (next < items.length) {
174
+ const i = next++;
175
+ results[i] = await fn(items[i]);
176
+ }
177
+ };
178
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
179
+ return results;
180
+ }
163
181
  function noControllerFor(dest) {
164
182
  return `No storage controller for ${dest.kind === "s3" ? "s3" : "gs"}://${dest.bucket}${dest.prefix ? `/${dest.prefix}` : ""}, so its backups cannot be read.`;
165
183
  }
@@ -199,6 +217,22 @@ async function readBackupBytes(dest, key, storage) {
199
217
  };
200
218
  }
201
219
  /**
220
+ * Whether `key` names a backup the download route would serve, without
221
+ * opening it: the same guards as {@link openBackupStream}, and the file or
222
+ * object is there. An object store without `statObject` is taken at its key —
223
+ * reading the object to find out would be the download itself.
224
+ */
225
+ async function findBackup(dest, key, storage) {
226
+ if (dest.kind === "local") {
227
+ const resolved = resolveLocalBackupFile(dest, key);
228
+ return resolved ? { name: path.basename(resolved) } : null;
229
+ }
230
+ if (!storage) throw new Error(noControllerFor(dest));
231
+ if (!isObjectBackupKey(dest, key)) return null;
232
+ if (storage.statObject && !await storage.statObject(key, dest.bucket)) return null;
233
+ return { name: key.split("/").pop() || key };
234
+ }
235
+ /**
202
236
  * A single backup, opened for streaming, with its size. Same guards as
203
237
  * {@link readBackupBytes}.
204
238
  *
@@ -208,9 +242,11 @@ async function readBackupBytes(dest, key, storage) {
208
242
  * buffer for object storage — and a file over 2 GiB never downloaded at all:
209
243
  * `readFileSync` throws `ERR_FS_FILE_TOO_LARGE` there.
210
244
  *
211
- * A local file is read from disk as the response is written. Object storage
212
- * still arrives through {@link StorageController.getObject}, which is a
213
- * buffered `File` by contract; streaming that avoids the second copy.
245
+ * A local file is read from disk as the response is written, and so is an
246
+ * object through a controller with `getObjectStream` (the built-in S3 and GCS
247
+ * ones). A custom controller without it still arrives through
248
+ * {@link StorageController.getObject}, a buffered `File` by contract;
249
+ * streaming that avoids the second copy.
214
250
  */
215
251
  async function openBackupStream(dest, key, storage) {
216
252
  if (dest.kind === "local") {
@@ -225,12 +261,21 @@ async function openBackupStream(dest, key, storage) {
225
261
  }
226
262
  if (!storage) throw new Error(noControllerFor(dest));
227
263
  if (!isObjectBackupKey(dest, key)) return null;
264
+ const name = key.split("/").pop() || key;
265
+ if (storage.getObjectStream) {
266
+ const object = await storage.getObjectStream(key, { bucket: dest.bucket });
267
+ return object ? {
268
+ stream: object.stream,
269
+ size: object.size,
270
+ name
271
+ } : null;
272
+ }
228
273
  const file = await storage.getObject(key, dest.bucket);
229
274
  if (!file) return null;
230
275
  return {
231
276
  stream: file.stream(),
232
277
  size: file.size,
233
- name: key.split("/").pop() || key
278
+ name
234
279
  };
235
280
  }
236
281
  /** A file on disk as a web stream, read a chunk at a time as the response is written. */
@@ -261,6 +306,11 @@ function fileStream(filePath) {
261
306
  * uploads bucket, it is a local directory in development, and in production
262
307
  * without an uploads bucket there is none at all.
263
308
  */
309
+ /** The destination `BACKUP_DESTINATION` names, or `null` when it is unset. */
310
+ function backupDestinationFromEnv(env) {
311
+ const out = env.BACKUP_DESTINATION?.trim();
312
+ return out ? parseBackupDestination(out) : null;
313
+ }
264
314
  /** The destination as `BACKUP_DESTINATION` spells it. */
265
315
  function describeBackupDestination(dest) {
266
316
  if (dest.kind === "local") return dest.path;
@@ -276,7 +326,7 @@ function backupStorageFromEnv(dest, env) {
276
326
  type: "gcs",
277
327
  bucket: dest.bucket
278
328
  });
279
- if (!env.S3_ACCESS_KEY_ID || !env.S3_SECRET_ACCESS_KEY) throw new Error("S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY are not set in this server's environment. Set the same S3_* variables the backup cron uses.");
329
+ if (!env.S3_ACCESS_KEY_ID || !env.S3_SECRET_ACCESS_KEY) throw new Error("An S3 backup destination needs S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY in the environment — the same S3_* variables the backup cron uses.");
280
330
  return new S3StorageController({
281
331
  type: "s3",
282
332
  bucket: dest.bucket,
@@ -295,18 +345,38 @@ function unreadable(dest, err) {
295
345
  return ApiError.serviceUnavailable(`Cannot read the backups at ${describeBackupDestination(dest)}: ${reason}`);
296
346
  }
297
347
  /**
298
- * Admin REST routes for the Backups panel.
299
- *
300
- * Routes (mounted under `/admin/backups`, admin-guarded by the caller):
301
- * GET / → list available backups, and the scheduled job's last run
302
- * GET /download → download a backup's bytes (?key=…)
348
+ * The `purpose` of a download link's token. `verifyAccessToken` refuses
349
+ * any token with a purpose, and the storage, MFA and MCP verifiers each demand
350
+ * their own, so a link is never a session or any other credential, and none of
351
+ * those is a link.
303
352
  */
304
- function createBackupRoutes(config) {
305
- const router = new Hono();
306
- router.onError(errorHandler);
353
+ var BACKUP_DOWNLOAD_PURPOSE = "backup-download";
354
+ /** The token's audience: the one route that redeems it. */
355
+ var BACKUP_DOWNLOAD_AUDIENCE = "rebase:admin/backups/download";
356
+ /** What a link grants: one backup, at one destination. */
357
+ function linkScope(dest, key) {
358
+ return `backup:${describeBackupDestination(dest)}:${key}`;
359
+ }
360
+ /**
361
+ * Whether `token` is a live link for `key` at `dest`, signed by `jwt` — the
362
+ * backend answering. Never throws: anything else is a no.
363
+ */
364
+ async function linkGrants(jwt, token, dest, key) {
365
+ if (!jwt.isConfigured()) return false;
366
+ const claims = await jwt.verifyPurposeToken(BACKUP_DOWNLOAD_PURPOSE, token).catch(() => null);
367
+ return claims?.aud === BACKUP_DOWNLOAD_AUDIENCE && claims.scope === linkScope(dest, key);
368
+ }
369
+ /** The configured destination, or the 400 every backup route answers without one. */
370
+ function destinationOf(config) {
371
+ const dest = config.getDestination();
372
+ if (!dest) throw ApiError.badRequest("Backups are not configured (set BACKUP_DESTINATION).");
373
+ return dest;
374
+ }
375
+ /** {@link BackupRoutesConfig.storageFor}, with its default, for a destination that has a controller. */
376
+ function storageResolver(config) {
307
377
  const storageFor = config.storageFor ?? ((dest) => backupStorageFromEnv(dest, process.env));
308
378
  /** The controller for `dest`, or none for a local one. A destination it cannot read is an error, never `[]`. */
309
- const storageOf = (dest) => {
379
+ return (dest) => {
310
380
  if (dest.kind === "local") return void 0;
311
381
  try {
312
382
  return storageFor(dest);
@@ -314,6 +384,60 @@ function createBackupRoutes(config) {
314
384
  throw unreadable(dest, err);
315
385
  }
316
386
  };
387
+ }
388
+ /** One backup's bytes as the response, streamed: a dump is the size of the database. See `openBackupStream`. */
389
+ async function serveBackup(c, dest, key, storage) {
390
+ const result = await openBackupStream(dest, key, storage);
391
+ if (!result) throw ApiError.notFound(`Backup not found: ${key}`);
392
+ c.header("Content-Type", "application/octet-stream");
393
+ c.header("Content-Length", String(result.size));
394
+ c.header("Content-Disposition", `attachment; filename="${result.name}"`);
395
+ c.header("Cache-Control", "no-store");
396
+ return c.body(result.stream);
397
+ }
398
+ /**
399
+ * `GET /download?key=…&token=…`: a download link spent in place of the bearer
400
+ * header, for the one backup it names.
401
+ *
402
+ * Mount it at the backups prefix AHEAD of every gate on the admin prefix: the
403
+ * auth adapter's own admin routers authenticate everything under `/admin`
404
+ * before the Backups gate is reached. A request with no `token` passes
405
+ * through untouched; one with a token is answered here — the backup, or a
406
+ * 401 — and never falls through to the header: a bad link is a bad link.
407
+ *
408
+ * A browser cannot put a header on a navigation, so before links the SDK
409
+ * fetched the dump and held it whole in the tab as a Blob. With a link the
410
+ * browser streams it to disk.
411
+ */
412
+ function createBackupLinkRoutes(config) {
413
+ const router = new Hono();
414
+ router.onError(errorHandler);
415
+ const storageOf = storageResolver(config);
416
+ router.get("/download", async (c, next) => {
417
+ const token = c.req.query("token");
418
+ if (token === void 0) return next();
419
+ const dest = destinationOf(config);
420
+ const key = c.req.query("key") ?? "";
421
+ if (!await linkGrants(jwtOf(c), token, dest, key)) throw ApiError.unauthenticated(`This download link is not valid for this backup, or it has expired. Links last 60 seconds: ask for a new one.`);
422
+ return serveBackup(c, dest, key, storageOf(dest));
423
+ });
424
+ return router;
425
+ }
426
+ /**
427
+ * Admin REST routes for the Backups panel.
428
+ *
429
+ * Routes (mounted under `/admin/backups`, admin-guarded by the caller):
430
+ * GET / → list available backups, and the scheduled job's last run
431
+ * GET /download → download a backup's bytes (?key=…)
432
+ * POST /download-url → a 60-second link to one backup, `{ key }` → {@link BackupDownloadLink}
433
+ *
434
+ * The link is spent at `GET /download?key=…&token=…`, which
435
+ * {@link createBackupLinkRoutes} answers ahead of the gate.
436
+ */
437
+ function createBackupRoutes(config) {
438
+ const router = new Hono();
439
+ router.onError(errorHandler);
440
+ const storageOf = storageResolver(config);
317
441
  router.get("/", async (c) => {
318
442
  const schedule = config.getSchedule ? await config.getSchedule() : null;
319
443
  const dest = config.getDestination();
@@ -336,16 +460,29 @@ function createBackupRoutes(config) {
336
460
  });
337
461
  });
338
462
  router.get("/download", async (c) => {
339
- const dest = config.getDestination();
340
- if (!dest) throw ApiError.badRequest("Backups are not configured (set BACKUP_DESTINATION).");
463
+ const dest = destinationOf(config);
341
464
  const key = c.req.query("key");
342
465
  if (!key) throw ApiError.badRequest("Missing 'key' query parameter.");
343
- const result = await openBackupStream(dest, key, storageOf(dest));
344
- if (!result) throw ApiError.notFound(`Backup not found: ${key}`);
345
- c.header("Content-Type", "application/octet-stream");
346
- c.header("Content-Length", String(result.size));
347
- c.header("Content-Disposition", `attachment; filename="${result.name}"`);
348
- return c.body(result.stream);
466
+ return serveBackup(c, dest, key, storageOf(dest));
467
+ });
468
+ router.post("/download-url", async (c) => {
469
+ const dest = destinationOf(config);
470
+ const body = await c.req.json().catch(() => null);
471
+ const key = typeof body === "object" && body !== null && "key" in body ? body.key : void 0;
472
+ if (typeof key !== "string" || !key) throw ApiError.badRequest("Send the backup's key: { \"key\": \"…\" }.");
473
+ if (!await findBackup(dest, key, storageOf(dest))) throw ApiError.notFound(`Backup not found: ${key}`);
474
+ const jwt = jwtOf(c);
475
+ if (!jwt.isConfigured()) throw ApiError.serviceUnavailable("Download links are signed with the JWT secret, and this server has none (auth.jwtSecret). Download through GET /download with the Authorization header instead.");
476
+ const token = await jwt.signPurposeToken(BACKUP_DOWNLOAD_PURPOSE, {
477
+ aud: BACKUP_DOWNLOAD_AUDIENCE,
478
+ scope: linkScope(dest, key)
479
+ }, 60);
480
+ const download = new URL("download", c.req.url).pathname;
481
+ return c.json({
482
+ url: `${download}?key=${encodeURIComponent(key)}&token=${encodeURIComponent(token)}`,
483
+ token,
484
+ expiresIn: 60
485
+ });
349
486
  });
350
487
  return router;
351
488
  }
@@ -429,7 +566,11 @@ async function readBackupSchedule(source) {
429
566
  //#endregion
430
567
  //#region src/backup/index.ts
431
568
  var backup_exports = /* @__PURE__ */ __exportAll({
569
+ BACKUP_DOWNLOAD_LINK_TTL_SECONDS: () => 60,
570
+ backupDestinationFromEnv: () => backupDestinationFromEnv,
571
+ createBackupLinkRoutes: () => createBackupLinkRoutes,
432
572
  createBackupRoutes: () => createBackupRoutes,
573
+ findBackup: () => findBackup,
433
574
  listAllObjectKeys: () => listAllObjectKeys,
434
575
  listBackupObjects: () => listBackupObjects,
435
576
  openBackupStream: () => openBackupStream,
@@ -439,6 +580,6 @@ var backup_exports = /* @__PURE__ */ __exportAll({
439
580
  readBackupSchedule: () => readBackupSchedule
440
581
  });
441
582
  //#endregion
442
- export { parseBackupTimestamp as a, parseBackupDestination as i, createBackupRoutes as n, readBackupBytes as o, listBackupObjects as r, backup_exports as t };
583
+ export { listBackupObjects as a, readBackupBytes as c, backupStorageFromEnv as i, createBackupLinkRoutes as n, parseBackupDestination as o, createBackupRoutes as r, parseBackupTimestamp as s, backup_exports as t };
443
584
 
444
- //# sourceMappingURL=backup-CN0s50D2.js.map
585
+ //# sourceMappingURL=backup-BnvkSLHi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup-BnvkSLHi.js","names":[],"sources":["../src/backup/backup-common.ts","../src/backup/backup-storage.ts","../src/backup/backup-routes.ts","../src/backup/backup-schedule.ts","../src/backup/index.ts"],"sourcesContent":["/**\n * Storage-generic backup helpers used by the admin backup routes.\n *\n * These live in server (which owns {@link StorageController}) so the\n * admin API can list backups without depending on the Postgres driver —\n * `server` must not import from `server-postgres`. The `pg_dump`-side\n * copies of the tiny pure parsers live in `@rebasepro/server-postgres`;\n * keep the two in sync if the naming scheme ever changes.\n */\nimport fs from \"fs\";\nimport path from \"path\";\nimport type { BackupInfo, BackupDestinationKind } from \"@rebasepro/types\";\nimport type { StorageController } from \"../storage\";\nimport { logger } from \"../utils/logger\";\n\nexport type BackupDestination =\n | { kind: \"local\"; path: string }\n | { kind: \"s3\"; bucket: string; prefix: string }\n | { kind: \"gcs\"; bucket: string; prefix: string };\n\n/** Parse a destination string (`s3://…`, `gs://…`, or a local path). */\nexport function parseBackupDestination(out: string): BackupDestination {\n const s3 = out.match(/^s3:\\/\\/([^/]+)\\/?(.*)$/);\n if (s3) return { kind: \"s3\", bucket: s3[1], prefix: s3[2].replace(/\\/+$/, \"\") };\n const gcs = out.match(/^gs:\\/\\/([^/]+)\\/?(.*)$/);\n if (gcs) return { kind: \"gcs\", bucket: gcs[1], prefix: gcs[2].replace(/\\/+$/, \"\") };\n return { kind: \"local\", path: out };\n}\n\n/**\n * Recover the UTC creation time encoded in a `rebase-<db>-<ts>.dump` name.\n * Returns `null` for names that don't match, so foreign objects are ignored.\n */\nexport function parseBackupTimestamp(fileName: string): Date | null {\n const base = fileName.split(\"/\").pop() ?? fileName;\n const match = base.match(/-(\\d{8})T(\\d{6})Z\\.dump$/);\n if (!match) return null;\n const [, ymd, hms] = match;\n const iso =\n `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}` +\n `T${hms.slice(0, 2)}:${hms.slice(2, 4)}:${hms.slice(4, 6)}Z`;\n const date = new Date(iso);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * A backup is two files: the `.dump`, and beside it a `.globals.sql` holding\n * the cluster roles the dump's GRANTs and RLS policies name. Without that\n * sidecar a restore into a fresh cluster stops at the first GRANT to a role\n * that does not exist there. The CLI uploads, restores and prunes the two as\n * a pair. This listing used to know only the `.dump`, so a backup downloaded\n * from Studio arrived without its roles.\n *\n * The suffix mirrors `globalsFileForDump` in `@rebasepro/server-postgres`.\n */\nconst DUMP_SUFFIX = \".dump\";\nconst GLOBALS_SUFFIX = \".globals.sql\";\n\nfunction globalsKeyFor(dumpKey: string): string {\n return dumpKey.slice(0, -DUMP_SUFFIX.length) + GLOBALS_SUFFIX;\n}\n\n/** The only files the download route will serve. */\nfunction isBackupFile(key: string): boolean {\n return key.endsWith(DUMP_SUFFIX) || key.endsWith(GLOBALS_SUFFIX);\n}\n\n/**\n * How many pages of an object-storage listing to read before giving up — at\n * the providers' 1,000 keys a page, 100,000 objects, or 50,000 backups.\n */\nconst MAX_LIST_PAGES = 100;\n\n/**\n * Every key under a prefix, across pages.\n *\n * S3 and GCS answer a listing a page at a time, in ascending key order, and a\n * backup's key is `rebase-<db>-<UTC timestamp>`. Reading one page therefore\n * returned the OLDEST thousand objects: with retention unset and an hourly\n * schedule, the newest backup the panel and `rebase db backups list` showed\n * stopped advancing after about three weeks, which reads as \"backups stopped\".\n */\nexport async function listAllObjectKeys(\n storage: StorageController,\n prefix: string,\n bucket: string\n): Promise<string[]> {\n const keys: string[] = [];\n let pageToken: string | undefined;\n for (let page = 0; page < MAX_LIST_PAGES; page++) {\n const result = await storage.listObjects(prefix, { bucket, maxResults: 1000, pageToken });\n for (const item of result.items) keys.push(item.fullPath);\n pageToken = result.nextPageToken;\n if (!pageToken) return keys;\n }\n logger.warn(\n `[backups] Stopped listing ${bucket}/${prefix} after ${MAX_LIST_PAGES} pages (${keys.length} objects). ` +\n \"Set BACKUP_RETENTION_DAYS so old backups are pruned, or give the backups a prefix of their own.\"\n );\n return keys;\n}\n\n/**\n * Whether an object key is one the backup routes may serve: a backup file, at\n * the destination's prefix and nowhere else.\n *\n * The local branch has always been held to the backup directory; this branch\n * checked only the suffix, so any `.dump` in the bucket — `uploads/x.dump`, a\n * neighbouring `nightlyish/` prefix — was readable by naming it. A key is taken\n * as written, so a scheme (`s3://other-bucket/…`, which S3StorageController\n * would follow) or a `.`/`..`/empty segment (which a filesystem-backed or\n * S3-compatible store may resolve) is refused rather than interpreted.\n */\nfunction isObjectBackupKey(dest: { prefix: string }, key: string): boolean {\n if (!isBackupFile(key) || /^[a-z][a-z0-9+.-]*:\\/\\//i.test(key)) return false;\n const segments = key.split(\"/\");\n if (segments.some((segment) => segment === \"\" || segment === \".\" || segment === \"..\")) return false;\n return dest.prefix === \"\" || key.startsWith(`${dest.prefix}/`);\n}\n\n/**\n * List the backups at a destination as {@link BackupInfo}, newest first.\n * One entry per `.dump`, with its `.globals.sql` sidecar attached as\n * `globalsKey` when there is one.\n */\nexport async function listBackupObjects(\n dest: BackupDestination,\n storage?: StorageController\n): Promise<BackupInfo[]> {\n if (dest.kind === \"local\") {\n // Asynchronous throughout, a few stats at a time. This runs on the\n // Backups panel's request path, and a year of hourly backups is 8,760\n // dumps: stat'ed synchronously, the server answered nothing else\n // while it listed them.\n const stat = await fs.promises.stat(dest.path).catch(() => null);\n if (!stat) return [];\n const dir = stat.isDirectory() ? dest.path : path.dirname(dest.path);\n const files = await fs.promises.readdir(dir);\n const present = new Set(files);\n const dumps = files.filter((f) => f.endsWith(DUMP_SUFFIX));\n const stats = await mapBounded(dumps, LOCAL_STAT_CONCURRENCY, (f) => fs.promises.stat(path.join(dir, f)));\n return dumps\n .map((f, i): BackupInfo => {\n const full = path.join(dir, f);\n const st = stats[i];\n const createdAt = parseBackupTimestamp(f) ?? st.mtime;\n const globals = globalsKeyFor(f);\n return {\n key: full,\n name: f,\n sizeBytes: st.size,\n createdAt: createdAt.toISOString(),\n destinationKind: \"local\",\n ...(present.has(globals) ? { globalsKey: path.join(dir, globals) } : {})\n };\n })\n .sort(byNewest);\n }\n\n // An empty list is a claim about the bucket. Without a controller for it\n // nothing was read, and the panel said \"No backups yet.\" about a bucket\n // full of them.\n if (!storage) throw new Error(noControllerFor(dest));\n const keys = await listAllObjectKeys(storage, dest.prefix ? `${dest.prefix}/` : \"\", dest.bucket);\n const present = new Set(keys);\n return keys\n .filter((key) => key.endsWith(DUMP_SUFFIX))\n .map((key): BackupInfo => {\n const createdAt = parseBackupTimestamp(key);\n const globals = globalsKeyFor(key);\n return {\n key,\n name: key.split(\"/\").pop() || key,\n createdAt: createdAt ? createdAt.toISOString() : undefined,\n destinationKind: dest.kind as BackupDestinationKind,\n ...(present.has(globals) ? { globalsKey: globals } : {})\n };\n })\n .sort(byNewest);\n}\n\n/** Stats in flight at once while listing a local backup folder. */\nconst LOCAL_STAT_CONCURRENCY = 32;\n\n/** `fn` over `items`, at most `limit` at a time; results in input order. */\nasync function mapBounded<T, R>(items: readonly T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> {\n const results = new Array<R>(items.length);\n let next = 0;\n const worker = async () => {\n while (next < items.length) {\n const i = next++;\n results[i] = await fn(items[i]);\n }\n };\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));\n return results;\n}\n\nfunction noControllerFor(dest: { kind: \"s3\" | \"gcs\"; bucket: string; prefix: string }): string {\n const scheme = dest.kind === \"s3\" ? \"s3\" : \"gs\";\n return `No storage controller for ${scheme}://${dest.bucket}${dest.prefix ? `/${dest.prefix}` : \"\"}, so its backups cannot be read.`;\n}\n\nfunction byNewest(a: BackupInfo, b: BackupInfo): number {\n const ta = a.createdAt ? Date.parse(a.createdAt) : 0;\n const tb = b.createdAt ? Date.parse(b.createdAt) : 0;\n return tb - ta;\n}\n\n/**\n * The absolute path of a local backup file, or `null` for anything the download\n * route must not serve: a path outside the backup directory, a file that is not\n * a backup, or one that does not exist.\n */\nfunction resolveLocalBackupFile(dest: { path: string }, key: string): string | null {\n const stat = fs.existsSync(dest.path) ? fs.statSync(dest.path) : null;\n const dir = stat?.isDirectory() ? dest.path : path.dirname(dest.path);\n const resolvedDir = path.resolve(dir);\n const resolved = path.resolve(key);\n // Only allow reads inside the backup directory, and only backup files.\n if (!resolved.startsWith(resolvedDir + path.sep) || !isBackupFile(resolved)) {\n return null;\n }\n return fs.existsSync(resolved) ? resolved : null;\n}\n\n/** Read a single backup's bytes. Guards against path traversal for local. */\nexport async function readBackupBytes(\n dest: BackupDestination,\n key: string,\n storage?: StorageController\n): Promise<{ bytes: Uint8Array<ArrayBuffer>; name: string } | null> {\n if (dest.kind === \"local\") {\n const resolved = resolveLocalBackupFile(dest, key);\n if (!resolved) return null;\n return { bytes: new Uint8Array(fs.readFileSync(resolved)), name: path.basename(resolved) };\n }\n\n if (!storage) throw new Error(noControllerFor(dest));\n if (!isObjectBackupKey(dest, key)) return null;\n const file = await storage.getObject(key, dest.bucket);\n if (!file) return null;\n return { bytes: new Uint8Array(await file.arrayBuffer()), name: key.split(\"/\").pop() || key };\n}\n\n/**\n * Whether `key` names a backup the download route would serve, without\n * opening it: the same guards as {@link openBackupStream}, and the file or\n * object is there. An object store without `statObject` is taken at its key —\n * reading the object to find out would be the download itself.\n */\nexport async function findBackup(\n dest: BackupDestination,\n key: string,\n storage?: StorageController\n): Promise<{ name: string } | null> {\n if (dest.kind === \"local\") {\n const resolved = resolveLocalBackupFile(dest, key);\n return resolved ? { name: path.basename(resolved) } : null;\n }\n if (!storage) throw new Error(noControllerFor(dest));\n if (!isObjectBackupKey(dest, key)) return null;\n if (storage.statObject && !(await storage.statObject(key, dest.bucket))) return null;\n return { name: key.split(\"/\").pop() || key };\n}\n\n/**\n * A single backup, opened for streaming, with its size. Same guards as\n * {@link readBackupBytes}.\n *\n * What the download route serves. A dump is the size of the database, and\n * reading it whole put all of it on the API process's heap — twice for a local\n * destination (`readFileSync`, then a copy) and on top of the controller's own\n * buffer for object storage — and a file over 2 GiB never downloaded at all:\n * `readFileSync` throws `ERR_FS_FILE_TOO_LARGE` there.\n *\n * A local file is read from disk as the response is written, and so is an\n * object through a controller with `getObjectStream` (the built-in S3 and GCS\n * ones). A custom controller without it still arrives through\n * {@link StorageController.getObject}, a buffered `File` by contract;\n * streaming that avoids the second copy.\n */\nexport async function openBackupStream(\n dest: BackupDestination,\n key: string,\n storage?: StorageController\n): Promise<{ stream: ReadableStream<Uint8Array>; size: number; name: string } | null> {\n if (dest.kind === \"local\") {\n const resolved = resolveLocalBackupFile(dest, key);\n if (!resolved) return null;\n const { size } = await fs.promises.stat(resolved);\n return { stream: fileStream(resolved), size, name: path.basename(resolved) };\n }\n\n if (!storage) throw new Error(noControllerFor(dest));\n if (!isObjectBackupKey(dest, key)) return null;\n const name = key.split(\"/\").pop() || key;\n if (storage.getObjectStream) {\n const object = await storage.getObjectStream(key, { bucket: dest.bucket });\n return object ? { stream: object.stream, size: object.size, name } : null;\n }\n const file = await storage.getObject(key, dest.bucket);\n if (!file) return null;\n return { stream: file.stream(), size: file.size, name };\n}\n\n/** A file on disk as a web stream, read a chunk at a time as the response is written. */\nfunction fileStream(filePath: string): ReadableStream<Uint8Array> {\n const file = fs.createReadStream(filePath);\n const chunks: AsyncIterator<Buffer> = file[Symbol.asyncIterator]();\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n const next = await chunks.next();\n if (next.done) controller.close();\n else controller.enqueue(next.value);\n },\n cancel() {\n file.destroy();\n }\n });\n}\n","/**\n * The storage controller that reads a backup bucket.\n *\n * A backup destination is its own bucket, named by `BACKUP_DESTINATION`, and\n * the documented backup cron writes to it through a controller it builds from\n * the `S3_*` variables (GCS: application default credentials) — as\n * `rebase db backups list` does. The admin routes have to read it the same way.\n * The app's file storage is not that controller: it is configured for the\n * uploads bucket, it is a local directory in development, and in production\n * without an uploads bucket there is none at all.\n */\nimport { parseEnvBoolean } from \"@rebasepro/types\";\nimport { GCSStorageController } from \"../storage/GCSStorageController\";\nimport { S3StorageController } from \"../storage/S3StorageController\";\nimport type { StorageController } from \"../storage/types\";\nimport { parseBackupDestination, type BackupDestination } from \"./backup-common\";\n\n/** A destination in object storage — `s3://bucket/prefix` or `gs://bucket/prefix`. */\nexport type ObjectBackupDestination = Exclude<BackupDestination, { kind: \"local\" }>;\n\n/** The destination `BACKUP_DESTINATION` names, or `null` when it is unset. */\nexport function backupDestinationFromEnv(env: Record<string, string | undefined>): BackupDestination | null {\n const out = env.BACKUP_DESTINATION?.trim();\n return out ? parseBackupDestination(out) : null;\n}\n\n/** The destination as `BACKUP_DESTINATION` spells it. */\nexport function describeBackupDestination(dest: BackupDestination): string {\n if (dest.kind === \"local\") return dest.path;\n const scheme = dest.kind === \"s3\" ? \"s3\" : \"gs\";\n return `${scheme}://${dest.bucket}${dest.prefix ? `/${dest.prefix}` : \"\"}`;\n}\n\n/**\n * A controller for an object-storage destination's bucket, built from the\n * environment the way the backup cron and `rebase db backups list` build\n * theirs. Throws when an S3 destination has no credentials to read it with.\n */\nexport function backupStorageFromEnv(\n dest: ObjectBackupDestination,\n env: Record<string, string | undefined>\n): StorageController {\n if (dest.kind === \"gcs\") {\n return new GCSStorageController({ type: \"gcs\", bucket: dest.bucket });\n }\n // s3 also covers R2, MinIO and the other S3-compatible stores, through S3_ENDPOINT.\n if (!env.S3_ACCESS_KEY_ID || !env.S3_SECRET_ACCESS_KEY) {\n throw new Error(\n \"An S3 backup destination needs S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY in the environment \" +\n \"— the same S3_* variables the backup cron uses.\"\n );\n }\n return new S3StorageController({\n type: \"s3\",\n bucket: dest.bucket,\n region: env.S3_REGION || \"auto\",\n accessKeyId: env.S3_ACCESS_KEY_ID,\n secretAccessKey: env.S3_SECRET_ACCESS_KEY,\n endpoint: env.S3_ENDPOINT,\n // Unset stays unset, so the controller decides from the endpoint.\n forcePathStyle: parseEnvBoolean(env.S3_FORCE_PATH_STYLE)\n });\n}\n","import { Hono, type Context } from \"hono\";\nimport type { BackupDownloadLink, BackupListing, BackupScheduleStatus } from \"@rebasepro/types\";\nimport type { HonoEnv } from \"../api/types\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport { jwtOf, type JwtService } from \"../auth/jwt\";\nimport type { StorageController } from \"../storage/types\";\nimport { BackupDestination, findBackup, listBackupObjects, openBackupStream } from \"./backup-common\";\nimport { backupStorageFromEnv, describeBackupDestination, type ObjectBackupDestination } from \"./backup-storage\";\n\nexport interface BackupRoutesConfig {\n /**\n * Resolve the current backup destination, or `null` when backups are not\n * configured (`BACKUP_DESTINATION` unset). Read lazily so a restart isn't\n * required to pick up config.\n */\n getDestination: () => BackupDestination | null;\n /**\n * The controller that reads an object-storage destination. It has to\n * address the destination's own bucket — never the app's file storage,\n * which is another bucket, a local directory, or nothing. Defaults to\n * {@link backupStorageFromEnv} over `process.env`, which is how the backup\n * cron builds the one it writes with.\n */\n storageFor?: (dest: ObjectBackupDestination) => StorageController;\n /**\n * The scheduled backup job and its last run — see `readBackupSchedule`.\n * Omitted where this process has no cron scheduler; the listing then\n * answers `schedule: null`.\n */\n getSchedule?: () => Promise<BackupScheduleStatus | null>;\n /**\n * True when this process does not run the scheduled jobs (the `api` role\n * of a split deployment). A local destination read here is then this\n * process's disk, not the one the scheduled backup writes to.\n */\n scheduledElsewhere?: boolean;\n}\n\n/** Why a destination could not be read, as the answer says it. */\nfunction unreadable(dest: BackupDestination, err: unknown): ApiError {\n const reason = err instanceof Error ? err.message : String(err);\n return ApiError.serviceUnavailable(`Cannot read the backups at ${describeBackupDestination(dest)}: ${reason}`);\n}\n\n// ─── Download links ──────────────────────────────────────────────────\n\n/**\n * The `purpose` of a download link's token. `verifyAccessToken` refuses\n * any token with a purpose, and the storage, MFA and MCP verifiers each demand\n * their own, so a link is never a session or any other credential, and none of\n * those is a link.\n */\nconst BACKUP_DOWNLOAD_PURPOSE = \"backup-download\";\n/** The token's audience: the one route that redeems it. */\nconst BACKUP_DOWNLOAD_AUDIENCE = \"rebase:admin/backups/download\";\n/** How long a link works. Long enough to start the download, not to pass around. */\nexport const BACKUP_DOWNLOAD_LINK_TTL_SECONDS = 60;\n\n/** What a link grants: one backup, at one destination. */\nfunction linkScope(dest: BackupDestination, key: string): string {\n return `backup:${describeBackupDestination(dest)}:${key}`;\n}\n\n/**\n * Whether `token` is a live link for `key` at `dest`, signed by `jwt` — the\n * backend answering. Never throws: anything else is a no.\n */\nasync function linkGrants(jwt: JwtService, token: string, dest: BackupDestination, key: string): Promise<boolean> {\n if (!jwt.isConfigured()) return false;\n const claims = await jwt.verifyPurposeToken<{ aud?: unknown; scope?: unknown }>(BACKUP_DOWNLOAD_PURPOSE, token)\n .catch(() => null);\n return claims?.aud === BACKUP_DOWNLOAD_AUDIENCE && claims.scope === linkScope(dest, key);\n}\n\n/** The configured destination, or the 400 every backup route answers without one. */\nfunction destinationOf(config: Pick<BackupRoutesConfig, \"getDestination\">): BackupDestination {\n const dest = config.getDestination();\n if (!dest) throw ApiError.badRequest(\"Backups are not configured (set BACKUP_DESTINATION).\");\n return dest;\n}\n\n/** {@link BackupRoutesConfig.storageFor}, with its default, for a destination that has a controller. */\nfunction storageResolver(config: Pick<BackupRoutesConfig, \"storageFor\">): (dest: BackupDestination) => StorageController | undefined {\n const storageFor = config.storageFor ?? ((dest: ObjectBackupDestination) => backupStorageFromEnv(dest, process.env));\n /** The controller for `dest`, or none for a local one. A destination it cannot read is an error, never `[]`. */\n return (dest) => {\n if (dest.kind === \"local\") return undefined;\n try {\n return storageFor(dest);\n } catch (err) {\n throw unreadable(dest, err);\n }\n };\n}\n\n/** One backup's bytes as the response, streamed: a dump is the size of the database. See `openBackupStream`. */\nasync function serveBackup(c: Context<HonoEnv>, dest: BackupDestination, key: string, storage: StorageController | undefined): Promise<Response> {\n const result = await openBackupStream(dest, key, storage);\n if (!result) {\n throw ApiError.notFound(`Backup not found: ${key}`);\n }\n c.header(\"Content-Type\", \"application/octet-stream\");\n c.header(\"Content-Length\", String(result.size));\n c.header(\"Content-Disposition\", `attachment; filename=\"${result.name}\"`);\n // A whole database, and on a link a URL that grants it: nothing in between keeps a copy.\n c.header(\"Cache-Control\", \"no-store\");\n return c.body(result.stream);\n}\n\n/**\n * `GET /download?key=…&token=…`: a download link spent in place of the bearer\n * header, for the one backup it names.\n *\n * Mount it at the backups prefix AHEAD of every gate on the admin prefix: the\n * auth adapter's own admin routers authenticate everything under `/admin`\n * before the Backups gate is reached. A request with no `token` passes\n * through untouched; one with a token is answered here — the backup, or a\n * 401 — and never falls through to the header: a bad link is a bad link.\n *\n * A browser cannot put a header on a navigation, so before links the SDK\n * fetched the dump and held it whole in the tab as a Blob. With a link the\n * browser streams it to disk.\n */\nexport function createBackupLinkRoutes(config: Pick<BackupRoutesConfig, \"getDestination\" | \"storageFor\">): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n const storageOf = storageResolver(config);\n router.get(\"/download\", async (c, next) => {\n const token = c.req.query(\"token\");\n if (token === undefined) return next();\n const dest = destinationOf(config);\n const key = c.req.query(\"key\") ?? \"\";\n if (!(await linkGrants(jwtOf(c), token, dest, key))) {\n throw ApiError.unauthenticated(\n \"This download link is not valid for this backup, or it has expired. Links last \" +\n `${BACKUP_DOWNLOAD_LINK_TTL_SECONDS} seconds: ask for a new one.`\n );\n }\n return serveBackup(c, dest, key, storageOf(dest));\n });\n return router;\n}\n\n/**\n * Admin REST routes for the Backups panel.\n *\n * Routes (mounted under `/admin/backups`, admin-guarded by the caller):\n * GET / → list available backups, and the scheduled job's last run\n * GET /download → download a backup's bytes (?key=…)\n * POST /download-url → a 60-second link to one backup, `{ key }` → {@link BackupDownloadLink}\n *\n * The link is spent at `GET /download?key=…&token=…`, which\n * {@link createBackupLinkRoutes} answers ahead of the gate.\n */\nexport function createBackupRoutes(config: BackupRoutesConfig): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n\n const storageOf = storageResolver(config);\n\n router.get(\"/\", async (c) => {\n // Read whether or not a destination is configured: a backup cron with\n // no BACKUP_DESTINATION fails every run, and that is exactly what the\n // panel has to be able to say.\n const schedule = config.getSchedule ? await config.getSchedule() : null;\n const dest = config.getDestination();\n if (!dest) {\n return c.json({ backups: [], destinationKind: \"local\", configured: false, schedule } satisfies BackupListing);\n }\n const backups = await listBackupObjects(dest, storageOf(dest)).catch((err: unknown) => {\n throw unreadable(dest, err);\n });\n // Only where there is a scheduled backup to write elsewhere.\n const localDiskOfThisProcess = dest.kind === \"local\" && config.scheduledElsewhere === true && schedule !== null;\n return c.json({\n backups,\n destinationKind: dest.kind,\n configured: true,\n schedule,\n ...(localDiskOfThisProcess ? { localDiskOfThisProcess } : {})\n } satisfies BackupListing);\n });\n\n router.get(\"/download\", async (c) => {\n const dest = destinationOf(config);\n const key = c.req.query(\"key\");\n if (!key) {\n throw ApiError.badRequest(\"Missing 'key' query parameter.\");\n }\n return serveBackup(c, dest, key, storageOf(dest));\n });\n\n router.post(\"/download-url\", async (c) => {\n const dest = destinationOf(config);\n const body: unknown = await c.req.json().catch(() => null);\n const key = typeof body === \"object\" && body !== null && \"key\" in body ? body.key : undefined;\n if (typeof key !== \"string\" || !key) {\n throw ApiError.badRequest(\"Send the backup's key: { \\\"key\\\": \\\"…\\\" }.\");\n }\n // The download route's own guards, so a link is never minted for a\n // key it would refuse; and the backup is there, so the browser is not\n // sent to a 404 in place of a file.\n if (!(await findBackup(dest, key, storageOf(dest)))) {\n throw ApiError.notFound(`Backup not found: ${key}`);\n }\n const jwt = jwtOf(c);\n if (!jwt.isConfigured()) {\n throw ApiError.serviceUnavailable(\n \"Download links are signed with the JWT secret, and this server has none (auth.jwtSecret). \" +\n \"Download through GET /download with the Authorization header instead.\"\n );\n }\n const token = await jwt.signPurposeToken(\n BACKUP_DOWNLOAD_PURPOSE,\n { aud: BACKUP_DOWNLOAD_AUDIENCE, scope: linkScope(dest, key) },\n BACKUP_DOWNLOAD_LINK_TTL_SECONDS\n );\n const download = new URL(\"download\", c.req.url).pathname;\n return c.json({\n url: `${download}?key=${encodeURIComponent(key)}&token=${encodeURIComponent(token)}`,\n token,\n expiresIn: BACKUP_DOWNLOAD_LINK_TTL_SECONDS\n } satisfies BackupDownloadLink);\n });\n\n return router;\n}\n","/**\n * The scheduled backup job, as the Backups panel reports it.\n *\n * A backup that fails every night has to be visible where the backups are\n * listed. Before this the panel read the destination and nothing else, so a\n * cron that could not run `pg_dump` left an empty list under \"wait for the next\n * scheduled run\" — while each failure sat in `cron_logs`, one panel away.\n */\nimport type {\n BackupRunOutcome,\n BackupScheduleStatus,\n CronJobDefinition,\n CronJobLogEntry,\n CronJobStatus,\n RejectedCronJob\n} from \"@rebasepro/types\";\nimport { isAlreadyExecutingSkip } from \"../cron/cron-scheduler\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * The mark `createBackupCron` (in `@rebasepro/server-postgres`) puts on the\n * definition it returns.\n *\n * A registry symbol, so it is the same value across two installed copies of\n * either package, and neither package has to import the other: `server` must\n * not depend on `server-postgres`. The job cannot be recognised any other way —\n * its id is the cron file's name and its display name is configurable.\n */\nexport const BACKUP_CRON_MARK: unique symbol = Symbol.for(\"rebase.backupCron\");\n\n/** Whether a cron definition is the scheduled backup. */\nexport function isBackupCronDefinition(definition: CronJobDefinition): boolean {\n return Reflect.get(definition, BACKUP_CRON_MARK) === true;\n}\n\n/** The part of the cron scheduler this reads. */\nexport interface BackupScheduleSource {\n jobIdsWhere(predicate: (definition: CronJobDefinition) => boolean): string[];\n rejectedJobsWhere(predicate: (definition: CronJobDefinition) => boolean): RejectedCronJob[];\n fetchJob(id: string): Promise<CronJobStatus | undefined>;\n getJobLogsFromDb(id: string, limit?: number): Promise<CronJobLogEntry[]>;\n}\n\n/**\n * How many recent log entries to look through for the last scheduled run. A\n * skip — a slot that found the previous run still going — is logged as a\n * success with nothing done, and must not stand in for the run it skipped; nor\n * may a run someone started by hand, which is not the schedule's.\n */\nconst RECENT_RUNS = 50;\n\n/** A log entry as the panel reports a run. */\nfunction outcomeOf(entry: CronJobLogEntry): BackupRunOutcome {\n return {\n startedAt: entry.startedAt,\n finishedAt: entry.finishedAt,\n success: entry.success,\n ...(entry.success || !entry.error ? {} : { error: entry.error }),\n manual: entry.manual === true\n };\n}\n\n/**\n * The scheduled backup job and its last run, or `null` when the deployment\n * has none — registered or refused.\n */\nexport async function readBackupSchedule(source: BackupScheduleSource): Promise<BackupScheduleStatus | null> {\n const [jobId] = source.jobIdsWhere(isBackupCronDefinition);\n if (jobId === undefined) {\n // A refused job is not registered, and answered `null` here it read\n // as \"no backup cron\" under a hint to add the file the project has.\n const [refused] = source.rejectedJobsWhere(isBackupCronDefinition);\n return refused\n ? { jobId: refused.id, name: refused.name, schedule: refused.schedule, enabled: false, refused: refused.reason }\n : null;\n }\n\n const job = await source.fetchJob(jobId);\n if (!job) return null;\n // Off because its own definition says so — the documented cron file\n // exports `enabled: false` while BACKUP_SCHEDULE is unset — rather than\n // paused by someone. Resuming that in Cron Jobs would run the placeholder.\n const disabledInCode = !job.enabled &&\n source.jobIdsWhere(definition => isBackupCronDefinition(definition) && definition.enabled === false).includes(jobId);\n\n let recent: CronJobLogEntry[];\n let historyError: string | undefined;\n try {\n recent = await source.getJobLogsFromDb(jobId, RECENT_RUNS);\n } catch (err) {\n // Said, not read as \"has not run yet\". The listing itself still\n // answers: the backups at the destination are worth showing.\n logger.error(`[backups] Could not read the run history of the backup job \"${jobId}\"`, { error: err });\n recent = [];\n historyError = \"The backup job's run history could not be read. The server log has the reason.\";\n }\n const runs = recent.filter(entry => !isAlreadyExecutingSkip(entry));\n // \"Last scheduled backup\" is the schedule's: a later \"Run Now\" that worked\n // hid a failed nightly, and a failed test run read as a failed nightly.\n const lastScheduled = runs.find(entry => entry.manual !== true);\n // A run by hand since then is reported too, as what it is.\n const lastManual = runs.find(entry => entry.manual === true);\n const manualSince = lastManual && (!lastScheduled || lastManual.startedAt > lastScheduled.startedAt)\n ? lastManual\n : undefined;\n\n return {\n jobId,\n name: job.name,\n schedule: job.schedule,\n enabled: job.enabled,\n ...(disabledInCode ? { disabledInCode } : {}),\n ...(job.nextRunAt ? { nextRunAt: job.nextRunAt } : {}),\n ...(historyError ? { historyError } : {}),\n ...(lastScheduled ? { lastRun: outcomeOf(lastScheduled) } : {}),\n ...(manualSince ? { lastManualRun: outcomeOf(manualSince) } : {})\n };\n}\n","/**\n * Storage-generic backup helpers and admin routes for the Backups panel.\n * The `pg_dump`/`pg_restore` machinery lives in `@rebasepro/server-postgres`.\n */\nexport * from \"./backup-common\";\nexport { createBackupRoutes, createBackupLinkRoutes, BACKUP_DOWNLOAD_LINK_TTL_SECONDS } from \"./backup-routes\";\nexport { backupDestinationFromEnv, backupStorageFromEnv } from \"./backup-storage\";\nexport type { ObjectBackupDestination } from \"./backup-storage\";\nexport type { BackupRoutesConfig } from \"./backup-routes\";\nexport { readBackupSchedule, isBackupCronDefinition, BACKUP_CRON_MARK } from \"./backup-schedule\";\nexport type { BackupScheduleSource } from \"./backup-schedule\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,uBAAuB,KAAgC;CACnE,MAAM,KAAK,IAAI,MAAM,yBAAyB;CAC9C,IAAI,IAAI,OAAO;EAAE,MAAM;EAAM,QAAQ,GAAG;EAAI,QAAQ,GAAG,EAAE,CAAC,QAAQ,QAAQ,EAAE;CAAE;CAC9E,MAAM,MAAM,IAAI,MAAM,yBAAyB;CAC/C,IAAI,KAAK,OAAO;EAAE,MAAM;EAAO,QAAQ,IAAI;EAAI,QAAQ,IAAI,EAAE,CAAC,QAAQ,QAAQ,EAAE;CAAE;CAClF,OAAO;EAAE,MAAM;EAAS,MAAM;CAAI;AACtC;;;;;AAMA,SAAgB,qBAAqB,UAA+B;CAEhE,MAAM,SADO,SAAS,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,SAAA,CACvB,MAAM,0BAA0B;CACnD,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,GAAG,KAAK,OAAO;CACrB,MAAM,MACF,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,MAAM,GAAG,CAAC,EAAA,GACnD,IAAI,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE;CAC9D,MAAM,OAAO,IAAI,KAAK,GAAG;CACzB,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,IAAI,OAAO;AACjD;;;;;;;;;;;AAYA,IAAM,cAAc;AACpB,IAAM,iBAAiB;AAEvB,SAAS,cAAc,SAAyB;CAC5C,OAAO,QAAQ,MAAM,GAAG,EAAmB,IAAI;AACnD;;AAGA,SAAS,aAAa,KAAsB;CACxC,OAAO,IAAI,SAAS,WAAW,KAAK,IAAI,SAAS,cAAc;AACnE;;;;;AAMA,IAAM,iBAAiB;;;;;;;;;;AAWvB,eAAsB,kBAClB,SACA,QACA,QACiB;CACjB,MAAM,OAAiB,CAAC;CACxB,IAAI;CACJ,KAAK,IAAI,OAAO,GAAG,OAAO,gBAAgB,QAAQ;EAC9C,MAAM,SAAS,MAAM,QAAQ,YAAY,QAAQ;GAAE;GAAQ,YAAY;GAAM;EAAU,CAAC;EACxF,KAAK,MAAM,QAAQ,OAAO,OAAO,KAAK,KAAK,KAAK,QAAQ;EACxD,YAAY,OAAO;EACnB,IAAI,CAAC,WAAW,OAAO;CAC3B;CACA,OAAO,KACH,6BAA6B,OAAO,GAAG,OAAO,SAAS,eAAe,UAAU,KAAK,OAAO,2GAEhG;CACA,OAAO;AACX;;;;;;;;;;;;AAaA,SAAS,kBAAkB,MAA0B,KAAsB;CACvE,IAAI,CAAC,aAAa,GAAG,KAAK,2BAA2B,KAAK,GAAG,GAAG,OAAO;CAEvE,IADiB,IAAI,MAAM,GACvB,CAAA,CAAS,MAAM,YAAY,YAAY,MAAM,YAAY,OAAO,YAAY,IAAI,GAAG,OAAO;CAC9F,OAAO,KAAK,WAAW,MAAM,IAAI,WAAW,GAAG,KAAK,OAAO,EAAE;AACjE;;;;;;AAOA,eAAsB,kBAClB,MACA,SACqB;CACrB,IAAI,KAAK,SAAS,SAAS;EAKvB,MAAM,OAAO,MAAM,GAAG,SAAS,KAAK,KAAK,IAAI,CAAC,CAAC,YAAY,IAAI;EAC/D,IAAI,CAAC,MAAM,OAAO,CAAC;EACnB,MAAM,MAAM,KAAK,YAAY,IAAI,KAAK,OAAO,KAAK,QAAQ,KAAK,IAAI;EACnE,MAAM,QAAQ,MAAM,GAAG,SAAS,QAAQ,GAAG;EAC3C,MAAM,UAAU,IAAI,IAAI,KAAK;EAC7B,MAAM,QAAQ,MAAM,QAAQ,MAAM,EAAE,SAAS,WAAW,CAAC;EACzD,MAAM,QAAQ,MAAM,WAAW,OAAO,yBAAyB,MAAM,GAAG,SAAS,KAAK,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC;EACxG,OAAO,MACF,KAAK,GAAG,MAAkB;GACvB,MAAM,OAAO,KAAK,KAAK,KAAK,CAAC;GAC7B,MAAM,KAAK,MAAM;GACjB,MAAM,YAAY,qBAAqB,CAAC,KAAK,GAAG;GAChD,MAAM,UAAU,cAAc,CAAC;GAC/B,OAAO;IACH,KAAK;IACL,MAAM;IACN,WAAW,GAAG;IACd,WAAW,UAAU,YAAY;IACjC,iBAAiB;IACjB,GAAI,QAAQ,IAAI,OAAO,IAAI,EAAE,YAAY,KAAK,KAAK,KAAK,OAAO,EAAE,IAAI,CAAC;GAC1E;EACJ,CAAC,CAAC,CACD,KAAK,QAAQ;CACtB;CAKA,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,gBAAgB,IAAI,CAAC;CACnD,MAAM,OAAO,MAAM,kBAAkB,SAAS,KAAK,SAAS,GAAG,KAAK,OAAO,KAAK,IAAI,KAAK,MAAM;CAC/F,MAAM,UAAU,IAAI,IAAI,IAAI;CAC5B,OAAO,KACF,QAAQ,QAAQ,IAAI,SAAS,WAAW,CAAC,CAAC,CAC1C,KAAK,QAAoB;EACtB,MAAM,YAAY,qBAAqB,GAAG;EAC1C,MAAM,UAAU,cAAc,GAAG;EACjC,OAAO;GACH;GACA,MAAM,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;GAC9B,WAAW,YAAY,UAAU,YAAY,IAAI,KAAA;GACjD,iBAAiB,KAAK;GACtB,GAAI,QAAQ,IAAI,OAAO,IAAI,EAAE,YAAY,QAAQ,IAAI,CAAC;EAC1D;CACJ,CAAC,CAAC,CACD,KAAK,QAAQ;AACtB;;AAGA,IAAM,yBAAyB;;AAG/B,eAAe,WAAiB,OAAqB,OAAe,IAA2C;CAC3G,MAAM,UAAU,IAAI,MAAS,MAAM,MAAM;CACzC,IAAI,OAAO;CACX,MAAM,SAAS,YAAY;EACvB,OAAO,OAAO,MAAM,QAAQ;GACxB,MAAM,IAAI;GACV,QAAQ,KAAK,MAAM,GAAG,MAAM,EAAE;EAClC;CACJ;CACA,MAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,OAAO,MAAM,MAAM,EAAE,GAAG,MAAM,CAAC;CAC/E,OAAO;AACX;AAEA,SAAS,gBAAgB,MAAsE;CAE3F,OAAO,6BADQ,KAAK,SAAS,OAAO,OAAO,KACA,KAAK,KAAK,SAAS,KAAK,SAAS,IAAI,KAAK,WAAW,GAAG;AACvG;AAEA,SAAS,SAAS,GAAe,GAAuB;CACpD,MAAM,KAAK,EAAE,YAAY,KAAK,MAAM,EAAE,SAAS,IAAI;CAEnD,QADW,EAAE,YAAY,KAAK,MAAM,EAAE,SAAS,IAAI,KACvC;AAChB;;;;;;AAOA,SAAS,uBAAuB,MAAwB,KAA4B;CAEhF,MAAM,OADO,GAAG,WAAW,KAAK,IAAI,IAAI,GAAG,SAAS,KAAK,IAAI,IAAI,KAAA,EAC/C,YAAY,IAAI,KAAK,OAAO,KAAK,QAAQ,KAAK,IAAI;CACpE,MAAM,cAAc,KAAK,QAAQ,GAAG;CACpC,MAAM,WAAW,KAAK,QAAQ,GAAG;CAEjC,IAAI,CAAC,SAAS,WAAW,cAAc,KAAK,GAAG,KAAK,CAAC,aAAa,QAAQ,GACtE,OAAO;CAEX,OAAO,GAAG,WAAW,QAAQ,IAAI,WAAW;AAChD;;AAGA,eAAsB,gBAClB,MACA,KACA,SACgE;CAChE,IAAI,KAAK,SAAS,SAAS;EACvB,MAAM,WAAW,uBAAuB,MAAM,GAAG;EACjD,IAAI,CAAC,UAAU,OAAO;EACtB,OAAO;GAAE,OAAO,IAAI,WAAW,GAAG,aAAa,QAAQ,CAAC;GAAG,MAAM,KAAK,SAAS,QAAQ;EAAE;CAC7F;CAEA,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,gBAAgB,IAAI,CAAC;CACnD,IAAI,CAAC,kBAAkB,MAAM,GAAG,GAAG,OAAO;CAC1C,MAAM,OAAO,MAAM,QAAQ,UAAU,KAAK,KAAK,MAAM;CACrD,IAAI,CAAC,MAAM,OAAO;CAClB,OAAO;EAAE,OAAO,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC;EAAG,MAAM,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;CAAI;AAChG;;;;;;;AAQA,eAAsB,WAClB,MACA,KACA,SACgC;CAChC,IAAI,KAAK,SAAS,SAAS;EACvB,MAAM,WAAW,uBAAuB,MAAM,GAAG;EACjD,OAAO,WAAW,EAAE,MAAM,KAAK,SAAS,QAAQ,EAAE,IAAI;CAC1D;CACA,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,gBAAgB,IAAI,CAAC;CACnD,IAAI,CAAC,kBAAkB,MAAM,GAAG,GAAG,OAAO;CAC1C,IAAI,QAAQ,cAAc,CAAE,MAAM,QAAQ,WAAW,KAAK,KAAK,MAAM,GAAI,OAAO;CAChF,OAAO,EAAE,MAAM,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,IAAI;AAC/C;;;;;;;;;;;;;;;;;AAkBA,eAAsB,iBAClB,MACA,KACA,SACkF;CAClF,IAAI,KAAK,SAAS,SAAS;EACvB,MAAM,WAAW,uBAAuB,MAAM,GAAG;EACjD,IAAI,CAAC,UAAU,OAAO;EACtB,MAAM,EAAE,SAAS,MAAM,GAAG,SAAS,KAAK,QAAQ;EAChD,OAAO;GAAE,QAAQ,WAAW,QAAQ;GAAG;GAAM,MAAM,KAAK,SAAS,QAAQ;EAAE;CAC/E;CAEA,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,gBAAgB,IAAI,CAAC;CACnD,IAAI,CAAC,kBAAkB,MAAM,GAAG,GAAG,OAAO;CAC1C,MAAM,OAAO,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;CACrC,IAAI,QAAQ,iBAAiB;EACzB,MAAM,SAAS,MAAM,QAAQ,gBAAgB,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC;EACzE,OAAO,SAAS;GAAE,QAAQ,OAAO;GAAQ,MAAM,OAAO;GAAM;EAAK,IAAI;CACzE;CACA,MAAM,OAAO,MAAM,QAAQ,UAAU,KAAK,KAAK,MAAM;CACrD,IAAI,CAAC,MAAM,OAAO;CAClB,OAAO;EAAE,QAAQ,KAAK,OAAO;EAAG,MAAM,KAAK;EAAM;CAAK;AAC1D;;AAGA,SAAS,WAAW,UAA8C;CAC9D,MAAM,OAAO,GAAG,iBAAiB,QAAQ;CACzC,MAAM,SAAgC,KAAK,OAAO,cAAc,CAAC;CACjE,OAAO,IAAI,eAA2B;EAClC,MAAM,KAAK,YAAY;GACnB,MAAM,OAAO,MAAM,OAAO,KAAK;GAC/B,IAAI,KAAK,MAAM,WAAW,MAAM;QAC3B,WAAW,QAAQ,KAAK,KAAK;EACtC;EACA,SAAS;GACL,KAAK,QAAQ;EACjB;CACJ,CAAC;AACL;;;;;;;;;;;;;;;AC3SA,SAAgB,yBAAyB,KAAmE;CACxG,MAAM,MAAM,IAAI,oBAAoB,KAAK;CACzC,OAAO,MAAM,uBAAuB,GAAG,IAAI;AAC/C;;AAGA,SAAgB,0BAA0B,MAAiC;CACvE,IAAI,KAAK,SAAS,SAAS,OAAO,KAAK;CAEvC,OAAO,GADQ,KAAK,SAAS,OAAO,OAAO,KAC1B,KAAK,KAAK,SAAS,KAAK,SAAS,IAAI,KAAK,WAAW;AAC1E;;;;;;AAOA,SAAgB,qBACZ,MACA,KACiB;CACjB,IAAI,KAAK,SAAS,OACd,OAAO,IAAI,qBAAqB;EAAE,MAAM;EAAO,QAAQ,KAAK;CAAO,CAAC;CAGxE,IAAI,CAAC,IAAI,oBAAoB,CAAC,IAAI,sBAC9B,MAAM,IAAI,MACN,6IAEJ;CAEJ,OAAO,IAAI,oBAAoB;EAC3B,MAAM;EACN,QAAQ,KAAK;EACb,QAAQ,IAAI,aAAa;EACzB,aAAa,IAAI;EACjB,iBAAiB,IAAI;EACrB,UAAU,IAAI;EAEd,gBAAgB,gBAAgB,IAAI,mBAAmB;CAC3D,CAAC;AACL;;;;ACvBA,SAAS,WAAW,MAAyB,KAAwB;CACjE,MAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC9D,OAAO,SAAS,mBAAmB,8BAA8B,0BAA0B,IAAI,EAAE,IAAI,QAAQ;AACjH;;;;;;;AAUA,IAAM,0BAA0B;;AAEhC,IAAM,2BAA2B;;AAKjC,SAAS,UAAU,MAAyB,KAAqB;CAC7D,OAAO,UAAU,0BAA0B,IAAI,EAAE,GAAG;AACxD;;;;;AAMA,eAAe,WAAW,KAAiB,OAAe,MAAyB,KAA+B;CAC9G,IAAI,CAAC,IAAI,aAAa,GAAG,OAAO;CAChC,MAAM,SAAS,MAAM,IAAI,mBAAuD,yBAAyB,KAAK,CAAC,CAC1G,YAAY,IAAI;CACrB,OAAO,QAAQ,QAAQ,4BAA4B,OAAO,UAAU,UAAU,MAAM,GAAG;AAC3F;;AAGA,SAAS,cAAc,QAAuE;CAC1F,MAAM,OAAO,OAAO,eAAe;CACnC,IAAI,CAAC,MAAM,MAAM,SAAS,WAAW,sDAAsD;CAC3F,OAAO;AACX;;AAGA,SAAS,gBAAgB,QAA4G;CACjI,MAAM,aAAa,OAAO,gBAAgB,SAAkC,qBAAqB,MAAM,QAAQ,GAAG;;CAElH,QAAQ,SAAS;EACb,IAAI,KAAK,SAAS,SAAS,OAAO,KAAA;EAClC,IAAI;GACA,OAAO,WAAW,IAAI;EAC1B,SAAS,KAAK;GACV,MAAM,WAAW,MAAM,GAAG;EAC9B;CACJ;AACJ;;AAGA,eAAe,YAAY,GAAqB,MAAyB,KAAa,SAA2D;CAC7I,MAAM,SAAS,MAAM,iBAAiB,MAAM,KAAK,OAAO;CACxD,IAAI,CAAC,QACD,MAAM,SAAS,SAAS,qBAAqB,KAAK;CAEtD,EAAE,OAAO,gBAAgB,0BAA0B;CACnD,EAAE,OAAO,kBAAkB,OAAO,OAAO,IAAI,CAAC;CAC9C,EAAE,OAAO,uBAAuB,yBAAyB,OAAO,KAAK,EAAE;CAEvE,EAAE,OAAO,iBAAiB,UAAU;CACpC,OAAO,EAAE,KAAK,OAAO,MAAM;AAC/B;;;;;;;;;;;;;;;AAgBA,SAAgB,uBAAuB,QAAkF;CACrH,MAAM,SAAS,IAAI,KAAc;CACjC,OAAO,QAAQ,YAAY;CAC3B,MAAM,YAAY,gBAAgB,MAAM;CACxC,OAAO,IAAI,aAAa,OAAO,GAAG,SAAS;EACvC,MAAM,QAAQ,EAAE,IAAI,MAAM,OAAO;EACjC,IAAI,UAAU,KAAA,GAAW,OAAO,KAAK;EACrC,MAAM,OAAO,cAAc,MAAM;EACjC,MAAM,MAAM,EAAE,IAAI,MAAM,KAAK,KAAK;EAClC,IAAI,CAAE,MAAM,WAAW,MAAM,CAAC,GAAG,OAAO,MAAM,GAAG,GAC7C,MAAM,SAAS,gBACX,+GAEJ;EAEJ,OAAO,YAAY,GAAG,MAAM,KAAK,UAAU,IAAI,CAAC;CACpD,CAAC;CACD,OAAO;AACX;;;;;;;;;;;;AAaA,SAAgB,mBAAmB,QAA2C;CAC1E,MAAM,SAAS,IAAI,KAAc;CACjC,OAAO,QAAQ,YAAY;CAE3B,MAAM,YAAY,gBAAgB,MAAM;CAExC,OAAO,IAAI,KAAK,OAAO,MAAM;EAIzB,MAAM,WAAW,OAAO,cAAc,MAAM,OAAO,YAAY,IAAI;EACnE,MAAM,OAAO,OAAO,eAAe;EACnC,IAAI,CAAC,MACD,OAAO,EAAE,KAAK;GAAE,SAAS,CAAC;GAAG,iBAAiB;GAAS,YAAY;GAAO;EAAS,CAAyB;EAEhH,MAAM,UAAU,MAAM,kBAAkB,MAAM,UAAU,IAAI,CAAC,CAAC,CAAC,OAAO,QAAiB;GACnF,MAAM,WAAW,MAAM,GAAG;EAC9B,CAAC;EAED,MAAM,yBAAyB,KAAK,SAAS,WAAW,OAAO,uBAAuB,QAAQ,aAAa;EAC3G,OAAO,EAAE,KAAK;GACV;GACA,iBAAiB,KAAK;GACtB,YAAY;GACZ;GACA,GAAI,yBAAyB,EAAE,uBAAuB,IAAI,CAAC;EAC/D,CAAyB;CAC7B,CAAC;CAED,OAAO,IAAI,aAAa,OAAO,MAAM;EACjC,MAAM,OAAO,cAAc,MAAM;EACjC,MAAM,MAAM,EAAE,IAAI,MAAM,KAAK;EAC7B,IAAI,CAAC,KACD,MAAM,SAAS,WAAW,gCAAgC;EAE9D,OAAO,YAAY,GAAG,MAAM,KAAK,UAAU,IAAI,CAAC;CACpD,CAAC;CAED,OAAO,KAAK,iBAAiB,OAAO,MAAM;EACtC,MAAM,OAAO,cAAc,MAAM;EACjC,MAAM,OAAgB,MAAM,EAAE,IAAI,KAAK,CAAC,CAAC,YAAY,IAAI;EACzD,MAAM,MAAM,OAAO,SAAS,YAAY,SAAS,QAAQ,SAAS,OAAO,KAAK,MAAM,KAAA;EACpF,IAAI,OAAO,QAAQ,YAAY,CAAC,KAC5B,MAAM,SAAS,WAAW,4CAA4C;EAK1E,IAAI,CAAE,MAAM,WAAW,MAAM,KAAK,UAAU,IAAI,CAAC,GAC7C,MAAM,SAAS,SAAS,qBAAqB,KAAK;EAEtD,MAAM,MAAM,MAAM,CAAC;EACnB,IAAI,CAAC,IAAI,aAAa,GAClB,MAAM,SAAS,mBACX,iKAEJ;EAEJ,MAAM,QAAQ,MAAM,IAAI,iBACpB,yBACA;GAAE,KAAK;GAA0B,OAAO,UAAU,MAAM,GAAG;EAAE,GAAA,EAEjE;EACA,MAAM,WAAW,IAAI,IAAI,YAAY,EAAE,IAAI,GAAG,CAAC,CAAC;EAChD,OAAO,EAAE,KAAK;GACV,KAAK,GAAG,SAAS,OAAO,mBAAmB,GAAG,EAAE,SAAS,mBAAmB,KAAK;GACjF;GACA,WAAA;EACJ,CAA8B;CAClC,CAAC;CAED,OAAO;AACX;;;;;;;;;;;;ACtMA,IAAa,mBAAkC,OAAO,IAAI,mBAAmB;;AAG7E,SAAgB,uBAAuB,YAAwC;CAC3E,OAAO,QAAQ,IAAI,YAAY,gBAAgB,MAAM;AACzD;;;;;;;AAgBA,IAAM,cAAc;;AAGpB,SAAS,UAAU,OAA0C;CACzD,OAAO;EACH,WAAW,MAAM;EACjB,YAAY,MAAM;EAClB,SAAS,MAAM;EACf,GAAI,MAAM,WAAW,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,OAAO,MAAM,MAAM;EAC9D,QAAQ,MAAM,WAAW;CAC7B;AACJ;;;;;AAMA,eAAsB,mBAAmB,QAAoE;CACzG,MAAM,CAAC,SAAS,OAAO,YAAY,sBAAsB;CACzD,IAAI,UAAU,KAAA,GAAW;EAGrB,MAAM,CAAC,WAAW,OAAO,kBAAkB,sBAAsB;EACjE,OAAO,UACD;GAAE,OAAO,QAAQ;GAAI,MAAM,QAAQ;GAAM,UAAU,QAAQ;GAAU,SAAS;GAAO,SAAS,QAAQ;EAAO,IAC7G;CACV;CAEA,MAAM,MAAM,MAAM,OAAO,SAAS,KAAK;CACvC,IAAI,CAAC,KAAK,OAAO;CAIjB,MAAM,iBAAiB,CAAC,IAAI,WACxB,OAAO,aAAY,eAAc,uBAAuB,UAAU,KAAK,WAAW,YAAY,KAAK,CAAC,CAAC,SAAS,KAAK;CAEvH,IAAI;CACJ,IAAI;CACJ,IAAI;EACA,SAAS,MAAM,OAAO,iBAAiB,OAAO,WAAW;CAC7D,SAAS,KAAK;EAGV,OAAO,MAAM,+DAA+D,MAAM,IAAI,EAAE,OAAO,IAAI,CAAC;EACpG,SAAS,CAAC;EACV,eAAe;CACnB;CACA,MAAM,OAAO,OAAO,QAAO,UAAS,CAAC,uBAAuB,KAAK,CAAC;CAGlE,MAAM,gBAAgB,KAAK,MAAK,UAAS,MAAM,WAAW,IAAI;CAE9D,MAAM,aAAa,KAAK,MAAK,UAAS,MAAM,WAAW,IAAI;CAC3D,MAAM,cAAc,eAAe,CAAC,iBAAiB,WAAW,YAAY,cAAc,aACpF,aACA,KAAA;CAEN,OAAO;EACH;EACA,MAAM,IAAI;EACV,UAAU,IAAI;EACd,SAAS,IAAI;EACb,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC;EAC3C,GAAI,IAAI,YAAY,EAAE,WAAW,IAAI,UAAU,IAAI,CAAC;EACpD,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;EACvC,GAAI,gBAAgB,EAAE,SAAS,UAAU,aAAa,EAAE,IAAI,CAAC;EAC7D,GAAI,cAAc,EAAE,eAAe,UAAU,WAAW,EAAE,IAAI,CAAC;CACnE;AACJ"}
@@ -1,6 +1,6 @@
1
1
  import { type Server } from "http";
2
2
  import { Hono } from "hono";
3
- import { type RebaseBackendInstance } from "../init.js";
3
+ import type { RebaseBackendInstance } from "../init.js";
4
4
  import type { HonoEnv } from "../api/types.js";
5
5
  import { type Logger } from "../utils/logger.js";
6
6
  import { type RebaseBootEnv } from "./env.js";
@@ -166,7 +166,7 @@ export declare function warnOnDriverSkew(dataSources: InitializedDataSource[], r
166
166
  /**
167
167
  * Warn about a bundle whose shape makes provisioning impossible.
168
168
  *
169
- * `initializeRebaseBackend` provisions the collection schema for every boot
169
+ * `createRebaseBackend` provisions the collection schema for every boot
170
170
  * path, and it decides from what actually resolved: a project with no
171
171
  * collections reads them from the database and creates nothing. That rule is
172
172
  * right, but from inside the runtime it cannot tell "this project declares no
@@ -1,5 +1,6 @@
1
1
  import type { BackendBootstrapper, DatabaseAdapter } from "@rebasepro/types";
2
2
  import type { ResolvedDataSourceConfig } from "./sources.js";
3
+ import type { PoolConnect } from "../metrics/pool-wait.js";
3
4
  /**
4
5
  * The connection handle a driver hands back at boot: the client object, the
5
6
  * pool to close on shutdown, and how to probe it.
@@ -19,6 +20,8 @@ export interface DriverConnection {
19
20
  pool?: {
20
21
  end: () => Promise<void>;
21
22
  query?: (sql: string) => Promise<unknown>;
23
+ /** Checks a connection out. Timed, when metrics are on — see `metrics/pool-wait.ts`. */
24
+ connect?: PoolConnect;
22
25
  };
23
26
  /** Some drivers expose a query directly instead of via a pool. */
24
27
  query?: (sql: string) => Promise<unknown>;
@@ -167,10 +167,10 @@ export declare const bootEnvExtension: z.ZodObject<{
167
167
  REBASE_ROLE: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
168
168
  "": "";
169
169
  functions: "functions";
170
+ worker: "worker";
170
171
  all: "all";
171
172
  api: "api";
172
- worker: "worker";
173
- }>>, z.ZodTransform<"functions" | "all" | "api" | "worker", "" | "functions" | "all" | "api" | "worker">>;
173
+ }>>, z.ZodTransform<"functions" | "worker" | "all" | "api", "" | "functions" | "worker" | "all" | "api">>;
174
174
  REBASE_CRON_SCHEDULER: z.ZodPipe<z.ZodOptional<z.ZodEnum<{
175
175
  "": "";
176
176
  true: "true";
@@ -195,6 +195,13 @@ export declare const bootEnvExtension: z.ZodObject<{
195
195
  REBASE_FUNCTIONS_ONLY: z.ZodOptional<z.ZodString>;
196
196
  REBASE_FUNCTIONS_EXCLUDE: z.ZodOptional<z.ZodString>;
197
197
  REBASE_FUNCTIONS_UPSTREAM: z.ZodOptional<z.ZodString>;
198
+ REBASE_LIVE_SCHEMA_REPOSITORY: z.ZodOptional<z.ZodString>;
199
+ REBASE_LIVE_SCHEMA_BRANCH: z.ZodOptional<z.ZodString>;
200
+ REBASE_LIVE_SCHEMA_COLLECTIONS_PATH: z.ZodOptional<z.ZodString>;
201
+ REBASE_LIVE_SCHEMA_GITHUB_TOKEN: z.ZodOptional<z.ZodString>;
202
+ REBASE_LIVE_SCHEMA_GITHUB_APP_ID: z.ZodOptional<z.ZodString>;
203
+ REBASE_LIVE_SCHEMA_GITHUB_APP_INSTALLATION_ID: z.ZodOptional<z.ZodString>;
204
+ REBASE_LIVE_SCHEMA_GITHUB_APP_PRIVATE_KEY: z.ZodOptional<z.ZodString>;
198
205
  }, z.core.$strip>;
199
206
  export type RebaseBootEnv = RebaseEnv & z.infer<typeof bootEnvExtension>;
200
207
  /**
@@ -0,0 +1,22 @@
1
+ import { type RequestListener, type Server } from "http";
2
+ /**
3
+ * The largest request line plus headers the runtime accepts, in bytes.
4
+ *
5
+ * Node's default is 16 KB, and a request over it is answered 431 before any
6
+ * route sees it. The data API's own parameters reach that: a vector search
7
+ * carries its embedding as a JSON array in `?vector=`, and 768 dimensions of it
8
+ * are 16.7 KB of URL, 1,536 are 33 KB. 64 KB lets the common embedding sizes
9
+ * through up to 1,536 dimensions.
10
+ *
11
+ * A stopgap, not the fix. The proxy in front of most deployments has its own
12
+ * ceiling (nginx's is 8 KB) that no setting here moves, and 3,072 dimensions
13
+ * are 67 KB. `POST /api/data/<collection>/query` carries the same parameters in
14
+ * a body, and the SDK sends a vector search that way; this is for the URL a
15
+ * person or another client builds by hand.
16
+ *
17
+ * Not higher, because the bound is what it costs to hold a connection that is
18
+ * still sending its headers — the reason Node lowered it from 80 KB in 2018.
19
+ */
20
+ export declare const RUNTIME_MAX_HEADER_SIZE: number;
21
+ /** The runtime's HTTP server, with the header bound above. */
22
+ export declare function createRuntimeHttpServer(listener: RequestListener): Server;
@@ -1,5 +1,5 @@
1
1
  import type { CollectionConfig } from "@rebasepro/types";
2
- import type { RebaseAuthConfig } from "../init.js";
2
+ import type { LiveSchemaConfig, RebaseAuthConfig } from "../backend/options.js";
3
3
  import type { EmailConfig } from "../email/index.js";
4
4
  import type { CaptchaConfig } from "../auth/captcha.js";
5
5
  import type { RebaseBootEnv } from "./env.js";
@@ -34,3 +34,18 @@ export declare function resolveEmailOptions(env: RebaseBootEnv): EmailConfig | u
34
34
  */
35
35
  export declare function resolveCaptchaOptions(env: RebaseBootEnv): CaptchaConfig | undefined;
36
36
  export declare function resolveAuthOptions(env: RebaseBootEnv, usersCollection: CollectionConfig | undefined): RebaseAuthConfig;
37
+ /**
38
+ * Live schema editing for a built bundle, from the environment — or nothing.
39
+ *
40
+ * Opt-in: `undefined` unless `REBASE_LIVE_SCHEMA_REPOSITORY` names the
41
+ * repository to commit to, and then a bundle boots exactly as it did. A
42
+ * bundle has no code of its own to pass `liveSchema.repository` from, so this
43
+ * is the only way one can name a write target.
44
+ *
45
+ * A target that is named but cannot be reached — no credential, two
46
+ * credentials, half a GitHub App, a repository that is not `owner/name`, a
47
+ * collections path that leaves the repository — fails the boot. The other
48
+ * outcome is an "Edit collection" button that fails on press, on a deployment
49
+ * whose operator was sure they had configured it.
50
+ */
51
+ export declare function resolveLiveSchemaOptions(env: RebaseBootEnv): LiveSchemaConfig | undefined;
@@ -11,8 +11,10 @@ import type { BackendBootstrapper, CollectionConfig, InitializedDriver } from "@
11
11
  * a green deploy and no failing check anywhere. It stayed that way for weeks.
12
12
  *
13
13
  * The fix is structural rather than a second copy of the logic in the other
14
- * path: `initializeRebaseBackend` is the one function BOTH paths go through, so
15
- * provisioning belongs there, and this module is what it calls.
14
+ * path: `createRebaseBackend` is the one function every path goes through —
15
+ * `initializeRebaseBackend` and the bundle boot are compositions of it — so
16
+ * provisioning belongs there, under its `provision` option, and this module is
17
+ * what it calls.
16
18
  */
17
19
  /** How the process-wide provisioning attempt turned out. */
18
20
  export type ProvisionOutcome =
@@ -0,0 +1,17 @@
1
+ import type { MiddlewareHandler } from "hono";
2
+ import type { RebaseCorsOptions } from "./env.js";
3
+ /**
4
+ * Hono's `cors()`, answering only the requests that are CORS preflights.
5
+ *
6
+ * `cors()` answers every `OPTIONS` itself and never calls the route, preflight
7
+ * or not. A plain `OPTIONS` is also a request in its own right: it is how a TUS
8
+ * client asks `/api/storage/tus` for `Tus-Version`, `Tus-Extension` and
9
+ * `Tus-Max-Size`. With `cors()` mounted in front of every route that request
10
+ * got a bare 204 and the TUS handler's `OPTIONS` route was unreachable.
11
+ *
12
+ * A preflight is an `OPTIONS` carrying `Access-Control-Request-Method` — the
13
+ * one header a browser always sends on one. Those are still answered here.
14
+ * Any other `OPTIONS` is handled like a `GET`: `cors()` adds the allow-origin,
15
+ * credentials and exposed headers, then the route answers.
16
+ */
17
+ export declare function runtimeCors(options: RebaseCorsOptions): MiddlewareHandler;
@@ -66,6 +66,8 @@ export interface ResolvedDataSourceConfig {
66
66
  connectionString: string;
67
67
  adminConnectionString?: string;
68
68
  readConnectionString?: string;
69
+ /** The source's direct (non-pooled) URL, for the connections that hold a `LISTEN`. */
70
+ directConnectionString?: string;
69
71
  isDefault: boolean;
70
72
  poolConfig?: Record<string, number>;
71
73
  }
@@ -27,6 +27,15 @@ export declare function normalizeRequestHost(host: string): string;
27
27
  * deploy says is at an address is the app that answers there.
28
28
  */
29
29
  export declare function createStaticAppResolver(apps: readonly LoadedStaticApp[]): (request: StaticAppRequest) => LoadedStaticApp | undefined;
30
+ /**
31
+ * The static apps that sit under the API's base path, and so are never served.
32
+ *
33
+ * The API's routes are mounted first and every app's fallback declines the API
34
+ * base path, so an app at or under it answers nothing. The reserved-path rule
35
+ * the CLI applies names the default `/api`; only the runtime knows
36
+ * `REBASE_BASE_PATH`, so a moved API is checked here.
37
+ */
38
+ export declare function staticAppsShadowedByApi(apps: readonly LoadedStaticApp[], apiBasePath: string): LoadedStaticApp[];
30
39
  /** How one static app is mounted: what `serveSPA` needs beyond its own build. */
31
40
  export interface StaticAppMount {
32
41
  app: LoadedStaticApp;
@@ -3,26 +3,10 @@ import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
- import "./src-CatHFUym.js";
6
+ import "./src-D5Y3N-S4.js";
7
+ import { t as SCHEMA_VERSION_HEADER } from "./project_manifest-R6y_KHms.js";
7
8
  import { r as logger } from "./logger-D-S-hO5e.js";
8
9
  import { Hono } from "hono";
9
- //#region ../types/src/types/project_manifest.ts
10
- /**
11
- * Whether `host` is a public DNS name an app can be given: lowercase labels of
12
- * letters, digits and hyphens, at least two of them, not an IP address and not
13
- * `localhost`. Internationalised names arrive here already in punycode.
14
- */
15
- function isValidAppHost(host) {
16
- if (host.length === 0 || host.length > 253 || host !== host.toLowerCase()) return false;
17
- const labels = host.split(".");
18
- if (labels.length < 2) return false;
19
- const label = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
20
- if (!labels.every((part) => label.test(part))) return false;
21
- return !/^[0-9]+$/.test(labels[labels.length - 1]);
22
- }
23
- /** Header carrying the schema version an SDK was generated from. */
24
- var SCHEMA_VERSION_HEADER = "x-rebase-schema";
25
- //#endregion
26
10
  //#region ../types/src/types/collection_contract.ts
27
11
  /** Depth limit for the walk — deep enough for real configs, finite for cyclic ones. */
28
12
  var MAX_DEPTH = 64;
@@ -288,6 +272,6 @@ function createContractRoutes(config) {
288
272
  return router;
289
273
  }
290
274
  //#endregion
291
- export { isValidAppHost as a, SCHEMA_VERSION_HEADER as i, createContractRoutes as n, computeSchemaVersion as r, contract_routes_exports as t };
275
+ export { createContractRoutes as n, computeSchemaVersion as r, contract_routes_exports as t };
292
276
 
293
- //# sourceMappingURL=contract-routes-fz8i4pxs.js.map
277
+ //# sourceMappingURL=contract-routes-Ey6ftnKH.js.map