@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,136 @@
1
+ import { createRequire as __rebaseCreateRequire } from "module";
2
+ import __rebaseProcess from "process";
3
+ globalThis.process ??= __rebaseProcess;
4
+ __rebaseCreateRequire(import.meta.url);
5
+ import "./src-CatHFUym.js";
6
+ import { i as isCollectionPatch, n as collectionPatchProblems } from "./collection_patch-BRu-BvDv.js";
7
+ import { r as errorHandler, t as ApiError } from "./errors-D6_y86c5.js";
8
+ import { AstSchemaEditor } from "./ast-schema-editor-CSdL0iCk.js";
9
+ import { Hono } from "hono";
10
+ import { z } from "zod";
11
+ //#region src/schema-edit/source-editor.ts
12
+ /**
13
+ * The process's editor of the collection source files, one edit at a time.
14
+ *
15
+ * Two routers rewrite those files: the source-only editor
16
+ * (`/api/schema-editor/*`) and live schema editing's `/apply`. Both go through
17
+ * this one {@link AstSchemaEditor}, so the ts-morph project over the
18
+ * collections directory is parsed once per process rather than once per
19
+ * apply — 180–250 ms of blocked event loop at 200 collection files, on every
20
+ * save. The editor re-reads the disk at the start of each edit
21
+ * (`syncWithDisk`): a file changed since — by the IDE, by a commit being put
22
+ * back — is refreshed, a new one is added, a deleted one is dropped.
23
+ *
24
+ * One project cannot take two edits at once. An edit holds unsaved changes in
25
+ * memory across `await project.save()`, and a second edit arriving meanwhile
26
+ * would refresh those files from disk under it, or save them with its own.
27
+ * So every edit, from either router, waits in one queue for the edits before
28
+ * it to settle — a refused one included — and anything an edit does with the
29
+ * files it wrote (reading them back for a commit) belongs inside the same
30
+ * {@link run}, before the next edit can touch them.
31
+ */
32
+ var SchemaSourceEditor = class {
33
+ editor;
34
+ settled = Promise.resolve();
35
+ constructor(editor) {
36
+ this.editor = editor;
37
+ }
38
+ /** Run `edit` once every edit queued before it has finished. */
39
+ run(edit) {
40
+ const result = this.settled.then(() => edit(this.editor));
41
+ this.settled = result.then(() => void 0, () => void 0);
42
+ return result;
43
+ }
44
+ };
45
+ //#endregion
46
+ //#region src/api/schema-editor-routes.ts
47
+ /**
48
+ * Rewriting collection source from the admin panel.
49
+ *
50
+ * Every refusal in `AstSchemaEditor` is written for the person who will read it
51
+ * — "Collection X has no file at …", "Relation Y has no target collection. Pick
52
+ * one before saving." — and every one of them was a plain `Error`, which
53
+ * `errorHandler` treats as an unexpected failure: 500, and a body that says
54
+ * "Internal Server Error". The messages never left the server. They are 400s
55
+ * now, so the panel can show what it was told.
56
+ */
57
+ function refusalsAsBadRequest(run) {
58
+ return run().catch((error) => {
59
+ if (error instanceof ApiError) throw error;
60
+ throw ApiError.badRequest(error instanceof Error ? error.message : String(error), "SCHEMA_EDIT_REFUSED");
61
+ });
62
+ }
63
+ /** The identifier half of every payload here, checked once. */
64
+ var collectionIdSchema = z.string().min(1, "`collectionId` is required");
65
+ var propertyKeySchema = z.string().min(1, "`propertyKey` is required");
66
+ var propertySaveSchema = z.object({
67
+ collectionId: collectionIdSchema,
68
+ propertyKey: propertyKeySchema,
69
+ propertyConfig: z.record(z.string(), z.unknown())
70
+ });
71
+ var propertyDeleteSchema = z.object({
72
+ collectionId: collectionIdSchema,
73
+ propertyKey: propertyKeySchema
74
+ });
75
+ var collectionSaveSchema = z.object({
76
+ collectionId: collectionIdSchema,
77
+ collectionData: z.record(z.string(), z.unknown()).optional(),
78
+ /** What changed about an existing collection — see `collection_patch.ts`. */
79
+ patch: z.array(z.unknown()).optional(),
80
+ partial: z.boolean().optional()
81
+ }).refine((save) => save.collectionData === void 0 !== (save.patch === void 0), { message: "send `patch` (what changed) or `collectionData` (a new collection), not both" });
82
+ var collectionDeleteSchema = z.object({ collectionId: collectionIdSchema });
83
+ /** Parse a body against a schema, or 400 naming the field. */
84
+ async function body(c, schema) {
85
+ const raw = await c.req.json().catch(() => void 0);
86
+ const parsed = schema.safeParse(raw);
87
+ if (!parsed.success) throw ApiError.badRequest(parsed.error.issues.map((i) => `${i.path.join(".") || "body"}: ${i.message}`).join("; "), "INVALID_INPUT");
88
+ return parsed.data;
89
+ }
90
+ /**
91
+ * The editor for `collectionsDir`: one per process, shared by these routes
92
+ * and live schema editing's `/apply`. See {@link SchemaSourceEditor}.
93
+ *
94
+ * Here rather than beside the class because building it loads `ts-morph`, an
95
+ * optional peer dependency this module is imported to probe for.
96
+ */
97
+ function createSchemaSourceEditor(collectionsDir) {
98
+ return new SchemaSourceEditor(new AstSchemaEditor(collectionsDir));
99
+ }
100
+ function createSchemaEditorRoutes(source) {
101
+ const router = new Hono();
102
+ router.onError(errorHandler);
103
+ router.post("/property/save", async (c) => {
104
+ const { collectionId, propertyKey, propertyConfig } = await body(c, propertySaveSchema);
105
+ await refusalsAsBadRequest(() => source.run((editor) => editor.saveProperty(collectionId, propertyKey, propertyConfig)));
106
+ return c.json({ success: true });
107
+ });
108
+ router.post("/property/delete", async (c) => {
109
+ const { collectionId, propertyKey } = await body(c, propertyDeleteSchema);
110
+ await refusalsAsBadRequest(() => source.run((editor) => editor.deleteProperty(collectionId, propertyKey)));
111
+ return c.json({ success: true });
112
+ });
113
+ /**
114
+ * `partial: true` means "this payload is what changed", not "this is the
115
+ * collection". Without it a one-key patch — which is what adding a column
116
+ * posts — is read as a whole-collection save and deletes everything it does
117
+ * not mention, `securityRules` included. Absent, it defaults to a full save,
118
+ * so an older panel keeps the behaviour it was written against.
119
+ */
120
+ router.post("/collection/save", async (c) => {
121
+ const { collectionId, collectionData, patch, partial } = await body(c, collectionSaveSchema);
122
+ if (patch !== void 0 && !isCollectionPatch(patch)) throw ApiError.badRequest(collectionPatchProblems(patch).join(" "), "INVALID_INPUT");
123
+ await refusalsAsBadRequest(() => source.run((editor) => patch ? editor.applyPatch(collectionId, patch) : editor.saveCollection(collectionId, collectionData ?? {}, { partial: partial === true })));
124
+ return c.json({ success: true });
125
+ });
126
+ router.post("/collection/delete", async (c) => {
127
+ const { collectionId } = await body(c, collectionDeleteSchema);
128
+ await refusalsAsBadRequest(() => source.run((editor) => editor.deleteCollection(collectionId)));
129
+ return c.json({ success: true });
130
+ });
131
+ return router;
132
+ }
133
+ //#endregion
134
+ export { createSchemaEditorRoutes, createSchemaSourceEditor };
135
+
136
+ //# sourceMappingURL=schema-editor-routes-BFf3i8jm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-editor-routes-BFf3i8jm.js","names":[],"sources":["../src/schema-edit/source-editor.ts","../src/api/schema-editor-routes.ts"],"sourcesContent":["import type { AstSchemaEditor } from \"../api/ast-schema-editor\";\n\n/**\n * The process's editor of the collection source files, one edit at a time.\n *\n * Two routers rewrite those files: the source-only editor\n * (`/api/schema-editor/*`) and live schema editing's `/apply`. Both go through\n * this one {@link AstSchemaEditor}, so the ts-morph project over the\n * collections directory is parsed once per process rather than once per\n * apply — 180–250 ms of blocked event loop at 200 collection files, on every\n * save. The editor re-reads the disk at the start of each edit\n * (`syncWithDisk`): a file changed since — by the IDE, by a commit being put\n * back — is refreshed, a new one is added, a deleted one is dropped.\n *\n * One project cannot take two edits at once. An edit holds unsaved changes in\n * memory across `await project.save()`, and a second edit arriving meanwhile\n * would refresh those files from disk under it, or save them with its own.\n * So every edit, from either router, waits in one queue for the edits before\n * it to settle — a refused one included — and anything an edit does with the\n * files it wrote (reading them back for a commit) belongs inside the same\n * {@link run}, before the next edit can touch them.\n */\nexport class SchemaSourceEditor {\n private settled: Promise<unknown> = Promise.resolve();\n\n constructor(private readonly editor: AstSchemaEditor) {\n }\n\n /** Run `edit` once every edit queued before it has finished. */\n run<T>(edit: (editor: AstSchemaEditor) => Promise<T>): Promise<T> {\n const result = this.settled.then(() => edit(this.editor));\n this.settled = result.then(() => undefined, () => undefined);\n return result;\n }\n}\n","import { Hono } from \"hono\";\nimport { z } from \"zod\";\nimport { collectionPatchProblems, isCollectionPatch } from \"@rebasepro/types\";\nimport { AstSchemaEditor } from \"./ast-schema-editor\";\nimport { SchemaSourceEditor } from \"../schema-edit/source-editor\";\nimport { ApiError, errorHandler } from \"./errors\";\nimport { HonoEnv } from \"./types\";\n\n/**\n * Rewriting collection source from the admin panel.\n *\n * Every refusal in `AstSchemaEditor` is written for the person who will read it\n * — \"Collection X has no file at …\", \"Relation Y has no target collection. Pick\n * one before saving.\" — and every one of them was a plain `Error`, which\n * `errorHandler` treats as an unexpected failure: 500, and a body that says\n * \"Internal Server Error\". The messages never left the server. They are 400s\n * now, so the panel can show what it was told.\n */\nfunction refusalsAsBadRequest<T>(run: () => Promise<T>): Promise<T> {\n return run().catch((error: unknown) => {\n if (error instanceof ApiError) throw error;\n throw ApiError.badRequest(\n error instanceof Error ? error.message : String(error),\n \"SCHEMA_EDIT_REFUSED\"\n );\n });\n}\n\n/** The identifier half of every payload here, checked once. */\nconst collectionIdSchema = z.string().min(1, \"`collectionId` is required\");\nconst propertyKeySchema = z.string().min(1, \"`propertyKey` is required\");\n\nconst propertySaveSchema = z.object({\n collectionId: collectionIdSchema,\n propertyKey: propertyKeySchema,\n propertyConfig: z.record(z.string(), z.unknown())\n});\nconst propertyDeleteSchema = z.object({\n collectionId: collectionIdSchema,\n propertyKey: propertyKeySchema\n});\nconst collectionSaveSchema = z.object({\n collectionId: collectionIdSchema,\n collectionData: z.record(z.string(), z.unknown()).optional(),\n /** What changed about an existing collection — see `collection_patch.ts`. */\n patch: z.array(z.unknown()).optional(),\n partial: z.boolean().optional()\n}).refine(\n save => (save.collectionData === undefined) !== (save.patch === undefined),\n { message: \"send `patch` (what changed) or `collectionData` (a new collection), not both\" }\n);\nconst collectionDeleteSchema = z.object({\n collectionId: collectionIdSchema\n});\n\n/** Parse a body against a schema, or 400 naming the field. */\nasync function body<S extends z.ZodType>(c: { req: { json: () => Promise<unknown> } }, schema: S): Promise<z.infer<S>> {\n const raw = await c.req.json().catch(() => undefined);\n const parsed = schema.safeParse(raw);\n if (!parsed.success) {\n throw ApiError.badRequest(\n parsed.error.issues.map(i => `${i.path.join(\".\") || \"body\"}: ${i.message}`).join(\"; \"),\n \"INVALID_INPUT\"\n );\n }\n return parsed.data;\n}\n\n/**\n * The editor for `collectionsDir`: one per process, shared by these routes\n * and live schema editing's `/apply`. See {@link SchemaSourceEditor}.\n *\n * Here rather than beside the class because building it loads `ts-morph`, an\n * optional peer dependency this module is imported to probe for.\n */\nexport function createSchemaSourceEditor(collectionsDir: string): SchemaSourceEditor {\n return new SchemaSourceEditor(new AstSchemaEditor(collectionsDir));\n}\n\nexport function createSchemaEditorRoutes(source: SchemaSourceEditor): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n\n router.post(\"/property/save\", async (c) => {\n const { collectionId, propertyKey, propertyConfig } = await body(c, propertySaveSchema);\n await refusalsAsBadRequest(() => source.run(editor => editor.saveProperty(collectionId, propertyKey, propertyConfig)));\n return c.json({ success: true });\n });\n\n router.post(\"/property/delete\", async (c) => {\n const { collectionId, propertyKey } = await body(c, propertyDeleteSchema);\n await refusalsAsBadRequest(() => source.run(editor => editor.deleteProperty(collectionId, propertyKey)));\n return c.json({ success: true });\n });\n\n /**\n * `partial: true` means \"this payload is what changed\", not \"this is the\n * collection\". Without it a one-key patch — which is what adding a column\n * posts — is read as a whole-collection save and deletes everything it does\n * not mention, `securityRules` included. Absent, it defaults to a full save,\n * so an older panel keeps the behaviour it was written against.\n */\n router.post(\"/collection/save\", async (c) => {\n const { collectionId, collectionData, patch, partial } = await body(c, collectionSaveSchema);\n if (patch !== undefined && !isCollectionPatch(patch)) {\n throw ApiError.badRequest(collectionPatchProblems(patch).join(\" \"), \"INVALID_INPUT\");\n }\n await refusalsAsBadRequest(() => source.run(editor => patch\n ? editor.applyPatch(collectionId, patch)\n : editor.saveCollection(collectionId, collectionData ?? {}, { partial: partial === true })));\n return c.json({ success: true });\n });\n\n router.post(\"/collection/delete\", async (c) => {\n const { collectionId } = await body(c, collectionDeleteSchema);\n await refusalsAsBadRequest(() => source.run(editor => editor.deleteCollection(collectionId)));\n return c.json({ success: true });\n });\n\n return router;\n}\n"],"mappings":"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"}
@@ -53,7 +53,65 @@ export interface ServeSPAConfig {
53
53
  * generator emitted a real file per route.
54
54
  */
55
55
  spa?: boolean;
56
+ /**
57
+ * Whether this app answers a request under `basePath` at all (default:
58
+ * every one).
59
+ *
60
+ * For apps told apart by hostname as well as by path, which `excludePaths`
61
+ * cannot express: two apps can both be rooted at "/", one on
62
+ * `admin.example.com` and one on every other hostname, and no list of paths
63
+ * says which of them a request belongs to. The caller, which sees every
64
+ * app, decides; each mount asks. A request this returns `false` for passes
65
+ * through everything the mount registers — assets, caching, compression and
66
+ * the fallback alike — as if the mount were not there, because an app that
67
+ * declined the fallback but still served its files would answer the other
68
+ * app's hostname with its own `/assets/*`.
69
+ */
70
+ owns?: (request: StaticAppRequest) => boolean;
71
+ }
72
+ /** What {@link ServeSPAConfig.owns} is asked about. */
73
+ export interface StaticAppRequest {
74
+ /**
75
+ * The `Host` the request named, exactly as sent: any case, and possibly
76
+ * with a port or a trailing dot. The request URL's host when the header is
77
+ * absent, as it is on a request built in-process.
78
+ *
79
+ * Never `X-Forwarded-Host`. The proxy in front of the runtime passes the
80
+ * original `Host` through, and a forwarded header is one any client can
81
+ * write — reading it would let a request for the site choose the admin.
82
+ */
83
+ host: string;
84
+ /** The request path, as routed. */
85
+ path: string;
56
86
  }
87
+ /**
88
+ * Is `requestPath` the path `prefix`, or something beneath it?
89
+ *
90
+ * Segment-aware on purpose. A plain `startsWith` reads "/api" as excluding
91
+ * "/apidocs", and "/admin" as excluding "/administrators" — both ordinary
92
+ * client-side routes of the app rooted at "/", both then answered with a 404
93
+ * because the SPA fallback declined them and nothing else claims the path.
94
+ * `apiBasePath` is always in the exclusion list, so this reached single-app
95
+ * setups too, not just the multi-app ones the list was added for.
96
+ *
97
+ * Exported because deciding which app owns a request asks the same question of
98
+ * each app's own path, and two readings of "under" would disagree at exactly
99
+ * the boundary this exists for.
100
+ */
101
+ export declare function isUnderPath(requestPath: string, prefix: string): boolean;
102
+ /**
103
+ * The paths of the apps nested inside the one at `basePath` — what its SPA
104
+ * fallback must leave to them, as `excludePaths`.
105
+ *
106
+ * Only those beneath it. Every other app's path used to be excluded, ancestors
107
+ * included, and an app at `/admin/beta` beside one at `/admin` then declined
108
+ * every deep link it had: each is under "/admin". The enclosing app declined
109
+ * them too, as it should, and a URL two apps were mounted for answered 404. An
110
+ * ancestor's path can never be right here — anything that reaches this app's
111
+ * fallback is under it — and an unrelated app's path can never match, so the
112
+ * apps beneath are the whole list.
113
+ */
114
+ export declare function nestedAppPaths(basePath: string, otherPaths: readonly string[]): string[];
57
115
  /**
58
116
  * Serve a Single Page Application from an Hono app.
59
117
  *
@@ -0,0 +1,50 @@
1
+ import { type AddressJudge, type OutboundUrlGuardOptions } from "./outbound-url-guard.js";
2
+ /**
3
+ * How the webhook dispatcher sends a POST to a destination that came from data.
4
+ *
5
+ * On Node, through an http(s) Agent whose `lookup` is the destination guard's
6
+ * ({@link guardedLookup}): the name is resolved once per new connection, every
7
+ * address it answers with is judged, and the socket connects to an address
8
+ * that was. It used to be validate-then-fetch — a `dns.lookup` per delivery
9
+ * attempt on the libuv threadpool, and then a second resolution inside `fetch`
10
+ * that the guard never saw, so a TTL-0 name that flipped between the two
11
+ * could still land on a blocked address. Keep-alive connections are reused, so
12
+ * a steady stream of deliveries to one receiver resolves its name about once.
13
+ *
14
+ * With `allowPrivateNetworks` there is nothing to judge, and on a runtime
15
+ * without `node:http` there is no Agent to hand a resolver to: both send with
16
+ * `fetch`, after {@link assertAllowedOutboundUrl}, as before.
17
+ *
18
+ * Internal to the server.
19
+ */
20
+ /** What the dispatcher reads of a response, from either transport. */
21
+ export interface OutboundResponse {
22
+ status: number;
23
+ header(name: string): string | null;
24
+ /** The body as a stream, so a reader can stop at a cap and cancel the rest. */
25
+ body: ReadableStream<Uint8Array> | null;
26
+ text(): Promise<string>;
27
+ }
28
+ export interface OutboundPostInit {
29
+ headers: Record<string, string>;
30
+ body: string;
31
+ /** Aborts the request — and the response body, while it is being read. */
32
+ signal: AbortSignal;
33
+ }
34
+ /**
35
+ * POST `body` to `rawUrl`, never following a redirect: a redirect would send
36
+ * the POST — signature and all — somewhere the guard never judged.
37
+ *
38
+ * @throws BlockedUrlError when the guard refuses the destination.
39
+ */
40
+ export type OutboundPost = (rawUrl: string, init: OutboundPostInit) => Promise<OutboundResponse>;
41
+ export declare function createOutboundPost(options?: OutboundUrlGuardOptions): OutboundPost;
42
+ /** Validate-then-fetch: the path for `allowPrivateNetworks`, and for a runtime without `node:http`. */
43
+ export declare function createFetchPost(options?: OutboundUrlGuardOptions): OutboundPost;
44
+ /**
45
+ * The guarded Node transport, or `undefined` on a runtime without
46
+ * `node:http`. Loaded on use, like the guard's own resolver, so that this
47
+ * module imports on a runtime that has neither. `judge` is for tests that
48
+ * need a receiver on loopback.
49
+ */
50
+ export declare function createNodePost(options?: OutboundUrlGuardOptions, judge?: AddressJudge): Promise<OutboundPost | undefined>;
@@ -1,3 +1,4 @@
1
+ import type { LookupFunction } from "node:net";
1
2
  /**
2
3
  * Refusal to send a request to a destination the guard does not allow.
3
4
  *
@@ -29,6 +30,8 @@ export interface OutboundUrlGuardOptions {
29
30
  * outbound call whose destination comes from data.
30
31
  */
31
32
  export declare function blockedAddressReason(address: string): string | null;
33
+ /** Why an address may not be connected to, or `null`: {@link blockedAddressReason}. */
34
+ export type AddressJudge = (address: string) => string | null;
32
35
  /**
33
36
  * Resolve and validate a destination before anything connects to it.
34
37
  *
@@ -40,14 +43,40 @@ export declare function blockedAddressReason(address: string): string | null;
40
43
  * Known limit: this is validate-then-fetch. Between the lookup here and the
41
44
  * connection the runtime makes there is a second resolution this code does not
42
45
  * see, so a TTL-0 name that flips between answers can still land on a blocked
43
- * address. Closing that needs the socket pinned to the address checked here,
44
- * which Node's global `fetch` does not expose without an undici dispatcher.
45
- * The redirect policy in {@link WebhookDispatcher} closes the much wider
46
- * version of the same hole — a receiver that simply *points* at the internal
47
- * address.
46
+ * address. On Node the webhook dispatcher does not use this: it checks the
47
+ * address it connects to, through {@link guardedLookup}. This is the check for
48
+ * a runtime whose `fetch` cannot be handed a resolver.
48
49
  *
49
50
  * @throws BlockedUrlError when the destination is not allowed. DNS failures
50
51
  * are rethrown as they are, so callers can tell "never send this" from
51
52
  * "could not send this right now".
52
53
  */
53
54
  export declare function assertAllowedOutboundUrl(rawUrl: string, options?: OutboundUrlGuardOptions): Promise<URL>;
55
+ /**
56
+ * Every check of {@link assertAllowedOutboundUrl} but the lookup, for a
57
+ * connection that resolves the name through {@link guardedLookup}. The scheme,
58
+ * the internal names and an address written into the URL are judged here —
59
+ * a connection to a literal address asks no resolver, so nothing else would.
60
+ *
61
+ * Internal to the server: not exported from the package.
62
+ */
63
+ export declare function assertAllowedBeforeConnecting(rawUrl: string, options?: OutboundUrlGuardOptions): URL;
64
+ /**
65
+ * A `dns.lookup` for an http(s) Agent that refuses to connect anywhere the
66
+ * guard would not send: it resolves the name, judges *every* address it got,
67
+ * and hands the connection only addresses it judged — so the address checked
68
+ * is the address connected to. Validate-then-fetch resolves twice, and a
69
+ * name whose TTL-0 record flips between the two lookups lands on whatever the
70
+ * second one says.
71
+ *
72
+ * Called once per new connection, not per request: a keep-alive connection
73
+ * is to an address already judged. Nothing is cached — an "allowed" verdict
74
+ * that outlived its lookup would be the rebinding window again.
75
+ *
76
+ * Fails with {@link BlockedUrlError}, which the connection reports as its
77
+ * error; a resolver failure is reported as itself.
78
+ *
79
+ * Internal to the server: not exported from the package. `judge` is for the
80
+ * tests that need a receiver on loopback; nothing else passes one.
81
+ */
82
+ export declare function guardedLookup(options?: OutboundUrlGuardOptions, judge?: AddressJudge): LookupFunction;
@@ -15,6 +15,22 @@ interface ClientMessage {
15
15
  export interface WsRealtimeService extends RealtimeProvider {
16
16
  addClient(clientId: string, ws: unknown): void;
17
17
  handleClientMessage(clientId: string, message: ClientMessage, authContext?: unknown): Promise<void> | void;
18
+ /**
19
+ * Re-judge everything a socket holds open as the identity it signed in
20
+ * as — called on every `AUTHENTICATE`, a token refresh included.
21
+ *
22
+ * Required, because the socket calls it. It was added to the Postgres
23
+ * service and the socket and not here, the bootstrapper cast the composite
24
+ * to the service, and every sign-in on a multi-source project answered
25
+ * INTERNAL_ERROR. `routed-realtime-service-forwarding.test.ts` reads this
26
+ * interface and the socket's calls so it cannot happen twice.
27
+ */
28
+ rescopeClient(clientId: string, authContext: unknown): Promise<void>;
29
+ /**
30
+ * What the client opted into when it opened the socket (`?rebase_caps=`).
31
+ * Optional: a provider that sends nothing depending on one ignores them.
32
+ */
33
+ setClientOptIns?(clientId: string, optIns: ReadonlySet<string>): void;
18
34
  /**
19
35
  * Whether this provider actually implements channels, presence and
20
36
  * broadcast. Declared by the provider rather than inferred, because the
@@ -23,6 +23,11 @@ export interface WebhookDeliveryResult {
23
23
  statusCode: number;
24
24
  responseBody: string;
25
25
  success: boolean;
26
+ /**
27
+ * The attempt this result is from, counting from 1. `0` for a delivery
28
+ * refused before any attempt: an in-process queue that was full, with
29
+ * `statusCode: 0` and `responseBody: "queue full"`.
30
+ */
26
31
  attemptNumber: number;
27
32
  }
28
33
  export interface WebhookDispatcherOptions {
@@ -48,7 +53,8 @@ export interface WebhookDispatcherOptions {
48
53
  */
49
54
  onDelivery?: (result: WebhookDeliveryResult) => void;
50
55
  /**
51
- * Hostname resolver used by the destination guard. Defaults to
56
+ * Hostname resolver used by the destination guard — once per new
57
+ * connection, and its answer is where the connection goes. Defaults to
52
58
  * `dns.lookup`; injected by tests so a unit suite never asks a resolver.
53
59
  */
54
60
  lookup?: (hostname: string) => Promise<string[]>;
@@ -61,27 +67,48 @@ export interface WebhookDispatcherOptions {
61
67
  */
62
68
  jobQueue?: JobQueueClient;
63
69
  }
70
+ /**
71
+ * What a delivery queued as a job carries: the webhook's id — never its
72
+ * secret — and the delivery id every attempt sends, so a receiver can
73
+ * deduplicate the queue's at-least-once retries. Rows queued before the id was
74
+ * stored have none; the job's own id stands in for it.
75
+ */
76
+ export interface QueuedWebhookDelivery {
77
+ webhookId: string;
78
+ event: string;
79
+ payload: Record<string, unknown>;
80
+ deliveryId?: string;
81
+ }
64
82
  export declare class WebhookDispatcher {
65
83
  private webhooks;
84
+ /** Attempts per in-process delivery, the first included. */
66
85
  private maxRetries;
86
+ /**
87
+ * The wait before each attempt after the first: 1 s before the second, 5 s
88
+ * before the third. One per retry — a third entry here was never reached.
89
+ */
67
90
  private retryDelays;
68
91
  private readonly options;
69
92
  private readonly timeoutMs;
70
- private queue;
71
- private draining;
93
+ /** In-process deliveries, one lane per webhook id. A lane is removed when it empties. */
94
+ private lanes;
95
+ /** Sends one attempt, through the destination guard — see `outbound-post.ts`. */
96
+ private readonly post;
72
97
  constructor(options?: WebhookDispatcherOptions);
73
98
  /** Register webhooks to watch */
74
99
  setWebhooks(webhooks: WebhookConfig[]): void;
75
100
  /**
76
101
  * Called when a entity changes — checks if any webhook matches, and awaits
77
- * every delivery, retries included.
102
+ * every delivery, retries included. The matching webhooks are delivered to
103
+ * at the same time, so the wait is the slowest receiver's, not the sum of
104
+ * all of them; the results come back in webhook order either way.
78
105
  *
79
106
  * **Do not await this inside a collection callback.** `afterSave` and
80
107
  * `afterDelete` run inside the write's Postgres transaction, so awaiting a
81
108
  * delivery here holds a pooled connection and the row's locks for as long
82
- * as the receiver takes to answer — up to ~36s across three attempts, per
83
- * matching webhook. Use {@link enqueueEntityChange} there; this method is
84
- * for a custom function or a job that wants the results.
109
+ * as the slowest receiver takes to answer — up to ~36s across three
110
+ * attempts. Use {@link enqueueEntityChange} there; this method is for a
111
+ * custom function or a job that wants the results.
85
112
  */
86
113
  onEntityChange(table: string, event: "INSERT" | "UPDATE" | "DELETE", id: string, entity: Record<string, unknown> | null, previousEntity?: Record<string, unknown> | null): Promise<WebhookDeliveryResult[]>;
87
114
  /**
@@ -108,6 +135,20 @@ export declare class WebhookDispatcher {
108
135
  */
109
136
  enqueueEntityChange(table: string, event: "INSERT" | "UPDATE" | "DELETE", id: string, entity: Record<string, unknown> | null, previousEntity?: Record<string, unknown> | null): void;
110
137
  private dispatchDeliveries;
138
+ /**
139
+ * Append each delivery to its webhook's lane, starting the lane if it is
140
+ * idle. Deliveries to one webhook keep their order; deliveries to
141
+ * different webhooks no longer wait for each other — one serial queue for
142
+ * every webhook meant a receiver that refused connections held every
143
+ * other receiver six seconds per queued event.
144
+ */
145
+ private enqueueInProcess;
146
+ /**
147
+ * Turn a delivery away from a full lane, and say so: through `onDelivery`
148
+ * for each one — the hook a caller of {@link enqueueEntityChange} already
149
+ * watches for failures — and in the server log once per burst.
150
+ */
151
+ private refuse;
111
152
  /**
112
153
  * Deliver one queued job, as the worker runs it.
113
154
  *
@@ -116,22 +157,33 @@ export declare class WebhookDispatcher {
116
157
  * of three job attempts is nine deliveries and about two minutes of holding
117
158
  * a worker slot — and only the queue's retries survive a restart, which is
118
159
  * the whole reason for moving.
160
+ *
161
+ * Pass the job context as the second argument —
162
+ * `ctx => dispatcher.deliverQueuedJob(ctx.payload, ctx)` — so each attempt
163
+ * sends its real `X-Webhook-Attempt`, and a row queued without a delivery
164
+ * id still sends the same `X-Webhook-Delivery` on every retry (the job's
165
+ * id). Without it, every attempt says it is the first.
119
166
  */
120
- deliverQueuedJob(job: {
121
- webhookId: string;
122
- event: string;
123
- payload: Record<string, unknown>;
167
+ deliverQueuedJob(job: QueuedWebhookDelivery, context?: {
168
+ id?: string;
169
+ attempt?: number;
124
170
  }): Promise<void>;
125
171
  /**
126
172
  * Wait for every queued delivery to finish. For graceful shutdown, and for
127
173
  * tests that need to observe what {@link enqueueEntityChange} sent.
128
174
  *
175
+ * `timeoutMs` bounds the wait: a receiver that is down costs each of its
176
+ * queued deliveries up to ~36 seconds of retries, which a shutdown cannot
177
+ * always afford. Resolves `true` when everything finished, `false` when
178
+ * the budget ran out first — those deliveries carry on in the background
179
+ * for as long as the process does. Without it, waits however long the
180
+ * deliveries take.
181
+ *
129
182
  * In-process deliveries only — with a `jobQueue` configured there is
130
183
  * nothing here to wait for, because the deliveries are rows.
131
184
  */
132
- flush(): Promise<void>;
133
- private startDraining;
134
- private drainQueue;
185
+ flush(timeoutMs?: number): Promise<boolean>;
186
+ private drainLane;
135
187
  /** The payload each matching webhook gets for this change. */
136
188
  private buildDeliveries;
137
189
  private deliverWithRetry;