@rebasepro/server 0.23.0 → 0.24.1-canary.g56af4a4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/README.md +1 -1
  2. package/bin/rebase-server.js +4 -2
  3. package/dist/GCSStorageController-DivE9bAx.js +402 -0
  4. package/dist/GCSStorageController-DivE9bAx.js.map +1 -0
  5. package/dist/S3StorageController-D_vKs4kP.js +480 -0
  6. package/dist/S3StorageController-D_vKs4kP.js.map +1 -0
  7. package/dist/api/ast-schema-editor.d.ts +92 -1
  8. package/dist/api/errors.d.ts +9 -0
  9. package/dist/api/live-schema-routes.d.ts +38 -8
  10. package/dist/api/logs-routes.d.ts +39 -1
  11. package/dist/api/openapi-generator.d.ts +17 -0
  12. package/dist/api/rest/api-generator.d.ts +97 -11
  13. package/dist/api/rest/auth-collection-writes.d.ts +28 -21
  14. package/dist/api/rest/field-access-query.d.ts +28 -8
  15. package/dist/api/rest/field-ops.d.ts +19 -7
  16. package/dist/api/rest/query-body.d.ts +10 -0
  17. package/dist/api/rest/write-validation.d.ts +9 -7
  18. package/dist/api/schema-editor-routes.d.ts +10 -1
  19. package/dist/api/sql-export-routes.d.ts +8 -0
  20. package/dist/api/types.d.ts +28 -1
  21. package/dist/{ast-schema-editor-Mvr50v_S.js → ast-schema-editor-CSdL0iCk.js} +310 -12
  22. package/dist/ast-schema-editor-CSdL0iCk.js.map +1 -0
  23. package/dist/auth/access.d.ts +121 -0
  24. package/dist/auth/adapter-middleware.d.ts +2 -1
  25. package/dist/auth/address-ownership.d.ts +16 -1
  26. package/dist/auth/admin-roles-route.d.ts +4 -2
  27. package/dist/auth/admin-roles.d.ts +17 -20
  28. package/dist/auth/admin-user-ops.d.ts +24 -2
  29. package/dist/auth/admin-users-route.d.ts +2 -0
  30. package/dist/auth/api-keys/api-key-middleware.d.ts +56 -55
  31. package/dist/auth/api-keys/api-key-routes.d.ts +41 -11
  32. package/dist/auth/api-keys/api-key-store.d.ts +31 -8
  33. package/dist/auth/api-keys/api-key-types.d.ts +14 -16
  34. package/dist/auth/api-keys/http-operation.d.ts +19 -0
  35. package/dist/auth/api-keys/index.d.ts +11 -11
  36. package/dist/auth/api-keys/key-grant.d.ts +41 -0
  37. package/dist/auth/api-keys/legacy-permissions.d.ts +33 -0
  38. package/dist/auth/auth-hooks.d.ts +46 -7
  39. package/dist/auth/builtin-auth-adapter.d.ts +8 -0
  40. package/dist/auth/cookie-utils.d.ts +7 -0
  41. package/dist/auth/deliverable-address.d.ts +6 -0
  42. package/dist/auth/email-change-routes.d.ts +41 -0
  43. package/dist/auth/expired-token-sweep.d.ts +71 -0
  44. package/dist/auth/impersonation.d.ts +110 -0
  45. package/dist/auth/index.d.ts +4 -2
  46. package/dist/auth/interfaces.d.ts +120 -59
  47. package/dist/auth/jwt-crypto.d.ts +7 -0
  48. package/dist/auth/jwt-keys.d.ts +4 -0
  49. package/dist/auth/jwt.d.ts +54 -3
  50. package/dist/auth/magic-link-routes.d.ts +2 -6
  51. package/dist/auth/mfa-routes.d.ts +2 -9
  52. package/dist/auth/middleware.d.ts +17 -5
  53. package/dist/auth/otp-routes.d.ts +2 -6
  54. package/dist/auth/passwordless-signup.d.ts +27 -0
  55. package/dist/auth/platform-token.d.ts +122 -0
  56. package/dist/auth/rate-limiter.d.ts +54 -1
  57. package/dist/auth/registration-policy.d.ts +17 -0
  58. package/dist/auth/routes.d.ts +45 -0
  59. package/dist/auth/scope-routes.d.ts +22 -0
  60. package/dist/auth/session-routes.d.ts +11 -6
  61. package/dist/auth/token-revocation.d.ts +50 -1
  62. package/dist/auth/user-picking.d.ts +35 -0
  63. package/dist/auth/verify-credential.d.ts +28 -0
  64. package/dist/{auth-B-GIMpDG.js → auth-OPq_FeMs.js} +3115 -638
  65. package/dist/auth-OPq_FeMs.js.map +1 -0
  66. package/dist/auth_adapter-CIItCTc8.js +21 -0
  67. package/dist/auth_adapter-CIItCTc8.js.map +1 -0
  68. package/dist/backend-DTAOsLQc.js.map +1 -1
  69. package/dist/backup/backup-common.d.ts +24 -3
  70. package/dist/backup/backup-routes.d.ts +46 -5
  71. package/dist/backup/backup-schedule.d.ts +33 -0
  72. package/dist/backup/backup-storage.d.ts +16 -0
  73. package/dist/backup/index.d.ts +5 -1
  74. package/dist/backup-5d7XhLX3.js +581 -0
  75. package/dist/backup-5d7XhLX3.js.map +1 -0
  76. package/dist/boot/bundle.d.ts +19 -0
  77. package/dist/boot/driver.d.ts +3 -0
  78. package/dist/boot/env.d.ts +56 -4
  79. package/dist/boot/http-server.d.ts +22 -0
  80. package/dist/boot/options.d.ts +16 -1
  81. package/dist/boot/runtime-cors.d.ts +17 -0
  82. package/dist/boot/security-headers.d.ts +26 -0
  83. package/dist/boot/static-routing.d.ts +65 -0
  84. package/dist/collection_patch-BRu-BvDv.js +472 -0
  85. package/dist/collection_patch-BRu-BvDv.js.map +1 -0
  86. package/dist/{contract-routes-CbFjuBwa.js → contract-routes-02lASyJI.js} +5 -8
  87. package/dist/contract-routes-02lASyJI.js.map +1 -0
  88. package/dist/cron/cron-routes.d.ts +1 -1
  89. package/dist/cron/cron-scheduler.d.ts +94 -23
  90. package/dist/cron/cron-store.d.ts +31 -4
  91. package/dist/cron/define-cron.d.ts +6 -2
  92. package/dist/cron/index.d.ts +1 -1
  93. package/dist/{cron-loader-DfTj2Hbi.js → cron-loader-D7OmleAb.js} +11 -7
  94. package/dist/cron-loader-D7OmleAb.js.map +1 -0
  95. package/dist/{cron-routes-eE8nif_b.js → cron-routes-CBfiaqEL.js} +34 -8
  96. package/dist/cron-routes-CBfiaqEL.js.map +1 -0
  97. package/dist/{cron-scheduler-B0pLfAix.js → cron-scheduler-C2vbETMo.js} +349 -65
  98. package/dist/cron-scheduler-C2vbETMo.js.map +1 -0
  99. package/dist/cron-store-B2Ih0gCN.js +540 -0
  100. package/dist/cron-store-B2Ih0gCN.js.map +1 -0
  101. package/dist/{ddl-bootstrap-C6mo0Kmz.js → ddl-bootstrap-BaqMSa4Y.js} +2 -2
  102. package/dist/{ddl-bootstrap-C6mo0Kmz.js.map → ddl-bootstrap-BaqMSa4Y.js.map} +1 -1
  103. package/dist/email/index.d.ts +2 -2
  104. package/dist/email/smtp-email-service.d.ts +23 -0
  105. package/dist/email/templates.d.ts +22 -0
  106. package/dist/email/types.d.ts +44 -0
  107. package/dist/env.d.ts +1 -39
  108. package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
  109. package/dist/errors-D6_y86c5.js.map +1 -0
  110. package/dist/filter-dialect-REl-TjZr.js +1628 -0
  111. package/dist/filter-dialect-REl-TjZr.js.map +1 -0
  112. package/dist/{function-loader-xnbDAPfa.js → function-loader-BDKKiBmy.js} +5 -4
  113. package/dist/function-loader-BDKKiBmy.js.map +1 -0
  114. package/dist/{function-routes-Chet4-lB.js → function-routes-DWB7BBsi.js} +25 -13
  115. package/dist/function-routes-DWB7BBsi.js.map +1 -0
  116. package/dist/functions/context.d.ts +26 -6
  117. package/dist/functions/define-function.d.ts +7 -4
  118. package/dist/functions/guards.d.ts +22 -5
  119. package/dist/functions/index.d.ts +2 -2
  120. package/dist/functions/index.js +114 -37
  121. package/dist/functions/index.js.map +1 -1
  122. package/dist/functions/request-timeout.d.ts +7 -5
  123. package/dist/history/history-routes.d.ts +9 -0
  124. package/dist/history/retention-sweep.d.ts +81 -0
  125. package/dist/{history-recorder-B4MpJfJK.js → history-recorder-DHenr3lm.js} +4 -4
  126. package/dist/{history-recorder-B4MpJfJK.js.map → history-recorder-DHenr3lm.js.map} +1 -1
  127. package/dist/{history-store-BhxWOuz9.js → history-store-DPofGVDp.js} +2 -2
  128. package/dist/{history-store-BhxWOuz9.js.map → history-store-DPofGVDp.js.map} +1 -1
  129. package/dist/index.d.ts +14 -7
  130. package/dist/index.es.js +23363 -17552
  131. package/dist/index.es.js.map +1 -1
  132. package/dist/init/health.d.ts +17 -2
  133. package/dist/init/shutdown.d.ts +20 -0
  134. package/dist/init.d.ts +70 -6
  135. package/dist/internal-tables-L2t39RdT.js +136 -0
  136. package/dist/internal-tables-L2t39RdT.js.map +1 -0
  137. package/dist/jobs/job-queue.d.ts +8 -0
  138. package/dist/jobs/job-store.d.ts +24 -0
  139. package/dist/jobs/types.d.ts +25 -4
  140. package/dist/{jobs-CazMYhyy.js → jobs-BpD4Q0qR.js} +235 -25
  141. package/dist/jobs-BpD4Q0qR.js.map +1 -0
  142. package/dist/{jwt-DnQHNFCl.js → jwt-Bqcu7cGt.js} +207 -77
  143. package/dist/jwt-Bqcu7cGt.js.map +1 -0
  144. package/dist/{keys-CogCQpxG.js → keys-GAVZqbqx.js} +3 -17
  145. package/dist/{keys-CogCQpxG.js.map → keys-GAVZqbqx.js.map} +1 -1
  146. package/dist/load-failure-UvzZgGXT.js +48 -0
  147. package/dist/load-failure-UvzZgGXT.js.map +1 -0
  148. package/dist/{logger-DO2PZc4i.js → logger-D-S-hO5e.js} +26 -3
  149. package/dist/logger-D-S-hO5e.js.map +1 -0
  150. package/dist/{logs-routes-Bj4TYYUl.js → logs-routes-DAdv37GI.js} +48 -8
  151. package/dist/logs-routes-DAdv37GI.js.map +1 -0
  152. package/dist/mcp/consent-page.d.ts +1 -1
  153. package/dist/mcp/mcp-routes.d.ts +7 -0
  154. package/dist/mcp/mcp-tools.d.ts +23 -9
  155. package/dist/mcp/oauth-metadata.d.ts +21 -16
  156. package/dist/mcp/oauth-routes.d.ts +7 -1
  157. package/dist/metrics/index.d.ts +18 -0
  158. package/dist/metrics/pool-wait.d.ts +50 -0
  159. package/dist/offline-8Ks3yp9n-CadBKm2D.js +3305 -0
  160. package/dist/offline-8Ks3yp9n-CadBKm2D.js.map +1 -0
  161. package/dist/offline-store-D9ytNNaa-BCr2vsFB.js +1903 -0
  162. package/dist/offline-store-D9ytNNaa-BCr2vsFB.js.map +1 -0
  163. package/dist/{openapi-generator-O_O24MAT.js → openapi-generator-BqoUh4Ij.js} +455 -80
  164. package/dist/openapi-generator-BqoUh4Ij.js.map +1 -0
  165. package/dist/policy-PdzX-Opd.js +121 -0
  166. package/dist/policy-PdzX-Opd.js.map +1 -0
  167. package/dist/project_manifest-R6y_KHms.js +59 -0
  168. package/dist/project_manifest-R6y_KHms.js.map +1 -0
  169. package/dist/{proxy-Czngl3p9.js → proxy-qRlqeUmO.js} +2 -2
  170. package/dist/{proxy-Czngl3p9.js.map → proxy-qRlqeUmO.js.map} +1 -1
  171. package/dist/{query-parser-DGRVFNM3.js → query-parser-DK9eMkb9.js} +497 -52
  172. package/dist/query-parser-DK9eMkb9.js.map +1 -0
  173. package/dist/{request-timeout-C_4C2BeR.js → request-timeout-Dnl2_OBc.js} +17 -10
  174. package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
  175. package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
  176. package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
  177. package/dist/schema-edit/apply-schema-change.d.ts +73 -3
  178. package/dist/schema-edit/local-source.d.ts +29 -0
  179. package/dist/schema-edit/project-root.d.ts +3 -2
  180. package/dist/schema-edit/remote-source.d.ts +20 -5
  181. package/dist/schema-edit/source-editor.d.ts +28 -0
  182. package/dist/schema-editor-routes-BFf3i8jm.js +136 -0
  183. package/dist/schema-editor-routes-BFf3i8jm.js.map +1 -0
  184. package/dist/serve-spa.d.ts +58 -0
  185. package/dist/services/outbound-post.d.ts +50 -0
  186. package/dist/services/outbound-url-guard.d.ts +34 -5
  187. package/dist/services/routed-realtime-service.d.ts +16 -0
  188. package/dist/services/webhook-service.d.ts +66 -14
  189. package/dist/soft-delete-params-47yBGs3_.js +668 -0
  190. package/dist/soft-delete-params-47yBGs3_.js.map +1 -0
  191. package/dist/sql-export-routes-BNNuGGLP.js +240 -0
  192. package/dist/sql-export-routes-BNNuGGLP.js.map +1 -0
  193. package/dist/{src-vkcwKXbT.js → src-CatHFUym.js} +439 -20
  194. package/dist/src-CatHFUym.js.map +1 -0
  195. package/dist/storage/GCSStorageController.d.ts +48 -2
  196. package/dist/storage/LocalStorageController.d.ts +69 -2
  197. package/dist/storage/S3StorageController.d.ts +39 -3
  198. package/dist/storage/image-transform.d.ts +5 -0
  199. package/dist/storage/index.d.ts +2 -2
  200. package/dist/storage/maintenance-routes.d.ts +23 -0
  201. package/dist/storage/maintenance.d.ts +126 -0
  202. package/dist/storage/property-limits.d.ts +41 -6
  203. package/dist/storage/rendition-cache.d.ts +9 -11
  204. package/dist/storage/request-keys.d.ts +15 -0
  205. package/dist/storage/request-principals.d.ts +30 -0
  206. package/dist/storage/requested-object.d.ts +74 -0
  207. package/dist/storage/routes.d.ts +52 -14
  208. package/dist/storage/source-variables.d.ts +10 -0
  209. package/dist/storage/streams.d.ts +42 -0
  210. package/dist/storage/tus-handler.d.ts +76 -7
  211. package/dist/storage/types.d.ts +101 -7
  212. package/dist/storage/upload-validation.d.ts +13 -0
  213. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
  214. package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
  215. package/dist/strings.es-BeZWAodn.js +256 -0
  216. package/dist/strings.es-BeZWAodn.js.map +1 -0
  217. package/dist/upload-validation-Dd-fnsbA.js +172 -0
  218. package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
  219. package/dist/utils/byte-size.d.ts +10 -0
  220. package/dist/utils/compression.d.ts +6 -0
  221. package/dist/utils/load-failure.d.ts +14 -0
  222. package/dist/utils/logger.d.ts +12 -0
  223. package/package.json +5 -5
  224. package/dist/GCSStorageController-CjrA4PMo.js +0 -242
  225. package/dist/GCSStorageController-CjrA4PMo.js.map +0 -1
  226. package/dist/S3StorageController-B6pKDNVj.js +0 -264
  227. package/dist/S3StorageController-B6pKDNVj.js.map +0 -1
  228. package/dist/admin-roles-vYdp_Pil.js +0 -36
  229. package/dist/admin-roles-vYdp_Pil.js.map +0 -1
  230. package/dist/admin_block-DxKLmdiv.js +0 -206
  231. package/dist/admin_block-DxKLmdiv.js.map +0 -1
  232. package/dist/ast-schema-editor-Mvr50v_S.js.map +0 -1
  233. package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
  234. package/dist/auth-B-GIMpDG.js.map +0 -1
  235. package/dist/backup-D7YR94N3.js +0 -253
  236. package/dist/backup-D7YR94N3.js.map +0 -1
  237. package/dist/contract-routes-CbFjuBwa.js.map +0 -1
  238. package/dist/cron-loader-DfTj2Hbi.js.map +0 -1
  239. package/dist/cron-routes-eE8nif_b.js.map +0 -1
  240. package/dist/cron-scheduler-B0pLfAix.js.map +0 -1
  241. package/dist/cron-store-TcoGz-xS.js +0 -305
  242. package/dist/cron-store-TcoGz-xS.js.map +0 -1
  243. package/dist/dynamic-import-X40pTZUQ.js +0 -23
  244. package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
  245. package/dist/errors-DWsX4yTd.js.map +0 -1
  246. package/dist/function-loader-xnbDAPfa.js.map +0 -1
  247. package/dist/function-routes-Chet4-lB.js.map +0 -1
  248. package/dist/jobs-CazMYhyy.js.map +0 -1
  249. package/dist/jwt-DnQHNFCl.js.map +0 -1
  250. package/dist/logger-DO2PZc4i.js.map +0 -1
  251. package/dist/logs-routes-Bj4TYYUl.js.map +0 -1
  252. package/dist/openapi-generator-O_O24MAT.js.map +0 -1
  253. package/dist/query-parser-DGRVFNM3.js.map +0 -1
  254. package/dist/request-timeout-C_4C2BeR.js.map +0 -1
  255. package/dist/schema-editor-routes-C5-lh_jO.js +0 -87
  256. package/dist/schema-editor-routes-C5-lh_jO.js.map +0 -1
  257. package/dist/src-pmvW7BFx.js +0 -6688
  258. package/dist/src-pmvW7BFx.js.map +0 -1
  259. package/dist/src-vkcwKXbT.js.map +0 -1
  260. package/dist/types-BfKcm9do.js +0 -39
  261. package/dist/types-BfKcm9do.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup-5d7XhLX3.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 { isJwtConfigured, signPurposeToken, verifyPurposeToken } 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. {@link 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/** Whether `token` is a live link for `key` at `dest`. Never throws: anything else is a no. */\nasync function linkGrants(token: string, dest: BackupDestination, key: string): Promise<boolean> {\n if (!isJwtConfigured()) return false;\n const claims = await 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(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 if (!isJwtConfigured()) {\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 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":"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"}
@@ -37,11 +37,30 @@ export interface LoadedBundle {
37
37
  export interface LoadedStaticApp {
38
38
  /** Public base path, e.g. `/` or `/admin`. */
39
39
  path: string;
40
+ /**
41
+ * The one hostname this app answers on, lowercase — see
42
+ * {@link RebaseBundleStatic.host}. Absent means every hostname.
43
+ */
44
+ host?: string;
40
45
  /** Absolute path to the built assets. */
41
46
  dir: string;
42
47
  /** Serve `index.html` for unmatched paths under `path`. */
43
48
  spa: boolean;
44
49
  }
50
+ /**
51
+ * The order static apps are mounted in, which is also the order they are
52
+ * matched in: longest path first, and at an equal path the app that names a
53
+ * hostname before the one that does not.
54
+ *
55
+ * Longest first because the "/"-rooted app's catch-all would otherwise claim
56
+ * every sibling's URLs. Host first at an equal path because `admin.example.com`
57
+ * at "/" is a more specific claim than "every hostname" at "/" — the host-less
58
+ * app still answers on every other hostname, it just loses this one.
59
+ *
60
+ * The CLI orders `entry.static` the same way when it builds; this does not rely
61
+ * on that, because a bundle's order is whatever the program that wrote it chose.
62
+ */
63
+ export declare function compareStaticApps(a: Pick<LoadedStaticApp, "path" | "host">, b: Pick<LoadedStaticApp, "path" | "host">): number;
45
64
  /**
46
65
  * The file that makes a directory a bundle.
47
66
  *
@@ -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>;
@@ -9,7 +9,7 @@ import { type RebaseEnv } from "../env.js";
9
9
  * a set of environment variables, and everything either side needs to agree on
10
10
  * has to be part of the contract rather than a convention each project reinvents.
11
11
  */
12
- declare const bootEnvExtension: z.ZodObject<{
12
+ export declare const bootEnvExtension: z.ZodObject<{
13
13
  SMTP_HOST: z.ZodOptional<z.ZodString>;
14
14
  SMTP_PORT: z.ZodPreprocess<z.ZodDefault<z.ZodCoercedNumber<unknown>>>;
15
15
  SMTP_SECURE: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
@@ -34,19 +34,24 @@ declare const bootEnvExtension: z.ZodObject<{
34
34
  none: "none";
35
35
  ensure: "ensure";
36
36
  }>>;
37
+ REBASE_HSTS_INCLUDE_SUBDOMAINS: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
38
+ "": "";
39
+ true: "true";
40
+ false: "false";
41
+ }>>, z.ZodTransform<boolean, "" | "true" | "false">>;
37
42
  REBASE_METRICS: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
38
43
  "": "";
39
44
  true: "true";
40
45
  false: "false";
41
46
  }>>, z.ZodTransform<boolean, "" | "true" | "false">>;
42
47
  REBASE_METRICS_TOKEN: z.ZodOptional<z.ZodString>;
43
- LOG_LEVEL: z.ZodOptional<z.ZodEnum<{
48
+ LOG_LEVEL: z.ZodPreprocess<z.ZodOptional<z.ZodEnum<{
44
49
  "": "";
45
50
  debug: "debug";
46
51
  info: "info";
47
52
  warn: "warn";
48
53
  error: "error";
49
- }>>;
54
+ }>>>;
50
55
  STORAGE_PUBLIC_READ: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
51
56
  "": "";
52
57
  true: "true";
@@ -62,6 +67,7 @@ declare const bootEnvExtension: z.ZodObject<{
62
67
  true: "true";
63
68
  false: "false";
64
69
  }>>, z.ZodTransform<boolean, "" | "true" | "false">>;
70
+ STORAGE_DOWNLOAD_TOKEN_TTL: z.ZodPreprocess<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
65
71
  AUTH_REQUIRE: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
66
72
  "": "";
67
73
  true: "true";
@@ -82,6 +88,21 @@ declare const bootEnvExtension: z.ZodObject<{
82
88
  true: "true";
83
89
  false: "false";
84
90
  }>>, z.ZodTransform<boolean, "" | "true" | "false">>;
91
+ AUTH_REQUIRE_EMAIL_VERIFICATION: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
92
+ "": "";
93
+ true: "true";
94
+ false: "false";
95
+ }>>, z.ZodTransform<boolean, "" | "true" | "false">>;
96
+ AUTH_MAGIC_LINK_CREATES_USERS: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
97
+ "": "";
98
+ true: "true";
99
+ false: "false";
100
+ }>>, z.ZodTransform<boolean, "" | "true" | "false">>;
101
+ AUTH_REFRESH_TOKEN_REUSE: z.ZodPipe<z.ZodOptional<z.ZodEnum<{
102
+ "": "";
103
+ reject: "reject";
104
+ "revoke-session": "revoke-session";
105
+ }>>, z.ZodTransform<"reject" | "revoke-session" | undefined, "" | "reject" | "revoke-session" | undefined>>;
85
106
  CAPTCHA_PROVIDER: z.ZodOptional<z.ZodEnum<{
86
107
  "": "";
87
108
  turnstile: "turnstile";
@@ -174,6 +195,13 @@ declare const bootEnvExtension: z.ZodObject<{
174
195
  REBASE_FUNCTIONS_ONLY: z.ZodOptional<z.ZodString>;
175
196
  REBASE_FUNCTIONS_EXCLUDE: z.ZodOptional<z.ZodString>;
176
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>;
177
205
  }, z.core.$strip>;
178
206
  export type RebaseBootEnv = RebaseEnv & z.infer<typeof bootEnvExtension>;
179
207
  /**
@@ -223,4 +251,28 @@ export type CorsOriginResolver = (origin: string) => string | null;
223
251
  * adds localhost to it rather than replacing it.
224
252
  */
225
253
  export declare function resolveCorsOrigin(env: RebaseBootEnv): CorsOriginResolver;
226
- export {};
254
+ /**
255
+ * Response headers a browser app on another origin may read.
256
+ *
257
+ * Outside the CORS-safelisted few, a cross-origin `fetch` sees a header only
258
+ * when `Access-Control-Expose-Headers` names it — and the runtime named none.
259
+ * So in every cross-origin setup (every `rebase dev` project: a Vite frontend on
260
+ * its own port) the SDK read no `ETag`, `etagOf(row)` was always `undefined`,
261
+ * and an `ifMatch` update went out with no `If-Match` — a conditional write
262
+ * silently made unconditional. A 429's `Retry-After` and the `X-Request-ID` an
263
+ * error is reported by were lost the same way.
264
+ *
265
+ * These are the headers the SDK reads, plus the ones the API sends for a caller
266
+ * to act on (`X-RateLimit-*`, `Preference-Applied`). A backend that wires its
267
+ * own `cors()` wants the same list; `packages/server/test/cors-exposed-headers.test.ts`
268
+ * fails when the SDK reads a header this does not name.
269
+ */
270
+ export declare const CORS_EXPOSED_HEADERS: readonly string[];
271
+ /** What the runtime hands Hono's `cors()`. */
272
+ export interface RebaseCorsOptions {
273
+ origin: CorsOriginResolver;
274
+ credentials: true;
275
+ exposeHeaders: string[];
276
+ }
277
+ /** The runtime's whole CORS config, so a test can drive the same one boot does. */
278
+ export declare function resolveCorsOptions(env: RebaseBootEnv): RebaseCorsOptions;
@@ -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 "../init.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;
@@ -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;
@@ -0,0 +1,26 @@
1
+ import type { MiddlewareHandler } from "hono";
2
+ /**
3
+ * The `Strict-Transport-Security` value this runtime sends.
4
+ *
5
+ * Without `includeSubDomains` unless the operator asks for it. The default
6
+ * `secureHeaders()` sends it, and on a self-hosted custom domain that tells
7
+ * every browser to refuse plain HTTP on every sibling subdomain for six months —
8
+ * a decision about hosts this process does not serve, and not one it can undo
9
+ * once a browser has cached it.
10
+ */
11
+ export declare function strictTransportSecurity(includeSubDomains: boolean): string;
12
+ /**
13
+ * The Content-Security-Policy every static app is served with.
14
+ *
15
+ * Conservative on purpose: it says who may frame the app (this origin only,
16
+ * like the `X-Frame-Options: SAMEORIGIN` already sent), forbids plugins, and
17
+ * pins `<base>` to this origin. It restricts no script, style, worker, font or
18
+ * connection source, because an app's own build decides those — the CMS admin
19
+ * runs inline scripts, module workers and third-party sign-in, and a policy
20
+ * that broke any of them would be one every project had to turn off.
21
+ */
22
+ export declare const STATIC_APP_CSP = "frame-ancestors 'self'; object-src 'none'; base-uri 'self'";
23
+ /** The security headers both boot paths — backend and static-only — install. */
24
+ export declare function runtimeSecureHeaders(options: {
25
+ hstsIncludeSubDomains: boolean;
26
+ }): MiddlewareHandler;
@@ -0,0 +1,65 @@
1
+ import { type StaticAppRequest } from "../serve-spa.js";
2
+ import { type LoadedStaticApp } from "./bundle.js";
3
+ /**
4
+ * A request's hostname in the form an app's `host` is written in: lowercase,
5
+ * with no port and no trailing dot.
6
+ *
7
+ * All three are ways of naming the same host that a browser, a proxy or a
8
+ * person with curl will send. `Admin.Example.com`, `admin.example.com:443` and
9
+ * the fully-qualified `admin.example.com.` are one site, and an app that
10
+ * answered only the first spelling would be down for whoever typed another.
11
+ *
12
+ * An IPv6 literal comes back as it arrived: its colons are not a port, and no
13
+ * app is ever given one, so all it has to do here is not be mangled into
14
+ * something that could match.
15
+ */
16
+ export declare function normalizeRequestHost(host: string): string;
17
+ /**
18
+ * Which static app answers a request: the one rule, written once.
19
+ *
20
+ * An app is a candidate when it names no hostname or names this one, and the
21
+ * path is under its own. Of the candidates, the longest path wins, and at an
22
+ * equal path the app with a hostname beats the one without — which is
23
+ * {@link compareStaticApps}, so the first candidate in that order is the winner.
24
+ * `undefined` when no app claims the request, and then no app answers it.
25
+ *
26
+ * The CLI and the control plane order apps by the same rule, so the app a
27
+ * deploy says is at an address is the app that answers there.
28
+ */
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[];
39
+ /** How one static app is mounted: what `serveSPA` needs beyond its own build. */
40
+ export interface StaticAppMount {
41
+ app: LoadedStaticApp;
42
+ /** Sibling paths the app's SPA fallback must decline — the path-only rule. */
43
+ siblingPaths: string[];
44
+ /** Present when apps are told apart by hostname; see `ServeSPAConfig.owns`. */
45
+ owns?: (request: StaticAppRequest) => boolean;
46
+ }
47
+ /**
48
+ * Decide how each static app is mounted, in the order given.
49
+ *
50
+ * Two rules, chosen by whether any app names a hostname.
51
+ *
52
+ * **None does:** every app declines the paths of the apps nested beneath it
53
+ * (`nestedAppPaths`), exactly as a bundle has always been served. Not the
54
+ * resolver, even though it would agree on every ordinary request: in the
55
+ * corner it disagrees — a file in the root app's build under a sibling's
56
+ * prefix — those bundles are already deployed. A bundle that never mentions
57
+ * a hostname gets byte-for-byte what it got before hostnames existed.
58
+ *
59
+ * **Any does:** a path cannot say which app a request belongs to any more.
60
+ * Two apps can both sit at "/", and a host-less site must still answer
61
+ * `/cms` on its own domain while an admin on another hostname owns `/cms`
62
+ * there — excluding "/cms" by path would 404 the site's. So every mount asks
63
+ * the one resolver instead, and declines whatever another app wins.
64
+ */
65
+ export declare function planStaticAppMounts(apps: readonly LoadedStaticApp[]): StaticAppMount[];