@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.
- package/README.md +1 -1
- package/bin/rebase-server.js +4 -2
- package/dist/GCSStorageController-DivE9bAx.js +402 -0
- package/dist/GCSStorageController-DivE9bAx.js.map +1 -0
- package/dist/S3StorageController-D_vKs4kP.js +480 -0
- package/dist/S3StorageController-D_vKs4kP.js.map +1 -0
- package/dist/api/ast-schema-editor.d.ts +92 -1
- package/dist/api/errors.d.ts +9 -0
- package/dist/api/live-schema-routes.d.ts +38 -8
- package/dist/api/logs-routes.d.ts +39 -1
- package/dist/api/openapi-generator.d.ts +17 -0
- package/dist/api/rest/api-generator.d.ts +97 -11
- package/dist/api/rest/auth-collection-writes.d.ts +28 -21
- package/dist/api/rest/field-access-query.d.ts +28 -8
- package/dist/api/rest/field-ops.d.ts +19 -7
- package/dist/api/rest/query-body.d.ts +10 -0
- package/dist/api/rest/write-validation.d.ts +9 -7
- package/dist/api/schema-editor-routes.d.ts +10 -1
- package/dist/api/sql-export-routes.d.ts +8 -0
- package/dist/api/types.d.ts +28 -1
- package/dist/{ast-schema-editor-Mvr50v_S.js → ast-schema-editor-CSdL0iCk.js} +310 -12
- package/dist/ast-schema-editor-CSdL0iCk.js.map +1 -0
- package/dist/auth/access.d.ts +121 -0
- package/dist/auth/adapter-middleware.d.ts +2 -1
- package/dist/auth/address-ownership.d.ts +16 -1
- package/dist/auth/admin-roles-route.d.ts +4 -2
- package/dist/auth/admin-roles.d.ts +17 -20
- package/dist/auth/admin-user-ops.d.ts +24 -2
- package/dist/auth/admin-users-route.d.ts +2 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +56 -55
- package/dist/auth/api-keys/api-key-routes.d.ts +41 -11
- package/dist/auth/api-keys/api-key-store.d.ts +31 -8
- package/dist/auth/api-keys/api-key-types.d.ts +14 -16
- package/dist/auth/api-keys/http-operation.d.ts +19 -0
- package/dist/auth/api-keys/index.d.ts +11 -11
- package/dist/auth/api-keys/key-grant.d.ts +41 -0
- package/dist/auth/api-keys/legacy-permissions.d.ts +33 -0
- package/dist/auth/auth-hooks.d.ts +46 -7
- package/dist/auth/builtin-auth-adapter.d.ts +8 -0
- package/dist/auth/cookie-utils.d.ts +7 -0
- package/dist/auth/deliverable-address.d.ts +6 -0
- package/dist/auth/email-change-routes.d.ts +41 -0
- package/dist/auth/expired-token-sweep.d.ts +71 -0
- package/dist/auth/impersonation.d.ts +110 -0
- package/dist/auth/index.d.ts +4 -2
- package/dist/auth/interfaces.d.ts +120 -59
- package/dist/auth/jwt-crypto.d.ts +7 -0
- package/dist/auth/jwt-keys.d.ts +4 -0
- package/dist/auth/jwt.d.ts +54 -3
- package/dist/auth/magic-link-routes.d.ts +2 -6
- package/dist/auth/mfa-routes.d.ts +2 -9
- package/dist/auth/middleware.d.ts +17 -5
- package/dist/auth/otp-routes.d.ts +2 -6
- package/dist/auth/passwordless-signup.d.ts +27 -0
- package/dist/auth/platform-token.d.ts +122 -0
- package/dist/auth/rate-limiter.d.ts +54 -1
- package/dist/auth/registration-policy.d.ts +17 -0
- package/dist/auth/routes.d.ts +45 -0
- package/dist/auth/scope-routes.d.ts +22 -0
- package/dist/auth/session-routes.d.ts +11 -6
- package/dist/auth/token-revocation.d.ts +50 -1
- package/dist/auth/user-picking.d.ts +35 -0
- package/dist/auth/verify-credential.d.ts +28 -0
- package/dist/{auth-B-GIMpDG.js → auth-OPq_FeMs.js} +3115 -638
- package/dist/auth-OPq_FeMs.js.map +1 -0
- package/dist/auth_adapter-CIItCTc8.js +21 -0
- package/dist/auth_adapter-CIItCTc8.js.map +1 -0
- package/dist/backend-DTAOsLQc.js.map +1 -1
- package/dist/backup/backup-common.d.ts +24 -3
- package/dist/backup/backup-routes.d.ts +46 -5
- package/dist/backup/backup-schedule.d.ts +33 -0
- package/dist/backup/backup-storage.d.ts +16 -0
- package/dist/backup/index.d.ts +5 -1
- package/dist/backup-5d7XhLX3.js +581 -0
- package/dist/backup-5d7XhLX3.js.map +1 -0
- package/dist/boot/bundle.d.ts +19 -0
- package/dist/boot/driver.d.ts +3 -0
- package/dist/boot/env.d.ts +56 -4
- package/dist/boot/http-server.d.ts +22 -0
- package/dist/boot/options.d.ts +16 -1
- package/dist/boot/runtime-cors.d.ts +17 -0
- package/dist/boot/security-headers.d.ts +26 -0
- package/dist/boot/static-routing.d.ts +65 -0
- package/dist/collection_patch-BRu-BvDv.js +472 -0
- package/dist/collection_patch-BRu-BvDv.js.map +1 -0
- package/dist/{contract-routes-CbFjuBwa.js → contract-routes-02lASyJI.js} +5 -8
- package/dist/contract-routes-02lASyJI.js.map +1 -0
- package/dist/cron/cron-routes.d.ts +1 -1
- package/dist/cron/cron-scheduler.d.ts +94 -23
- package/dist/cron/cron-store.d.ts +31 -4
- package/dist/cron/define-cron.d.ts +6 -2
- package/dist/cron/index.d.ts +1 -1
- package/dist/{cron-loader-DfTj2Hbi.js → cron-loader-D7OmleAb.js} +11 -7
- package/dist/cron-loader-D7OmleAb.js.map +1 -0
- package/dist/{cron-routes-eE8nif_b.js → cron-routes-CBfiaqEL.js} +34 -8
- package/dist/cron-routes-CBfiaqEL.js.map +1 -0
- package/dist/{cron-scheduler-B0pLfAix.js → cron-scheduler-C2vbETMo.js} +349 -65
- package/dist/cron-scheduler-C2vbETMo.js.map +1 -0
- package/dist/cron-store-B2Ih0gCN.js +540 -0
- package/dist/cron-store-B2Ih0gCN.js.map +1 -0
- package/dist/{ddl-bootstrap-C6mo0Kmz.js → ddl-bootstrap-BaqMSa4Y.js} +2 -2
- package/dist/{ddl-bootstrap-C6mo0Kmz.js.map → ddl-bootstrap-BaqMSa4Y.js.map} +1 -1
- package/dist/email/index.d.ts +2 -2
- package/dist/email/smtp-email-service.d.ts +23 -0
- package/dist/email/templates.d.ts +22 -0
- package/dist/email/types.d.ts +44 -0
- package/dist/env.d.ts +1 -39
- package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
- package/dist/errors-D6_y86c5.js.map +1 -0
- package/dist/filter-dialect-REl-TjZr.js +1628 -0
- package/dist/filter-dialect-REl-TjZr.js.map +1 -0
- package/dist/{function-loader-xnbDAPfa.js → function-loader-BDKKiBmy.js} +5 -4
- package/dist/function-loader-BDKKiBmy.js.map +1 -0
- package/dist/{function-routes-Chet4-lB.js → function-routes-DWB7BBsi.js} +25 -13
- package/dist/function-routes-DWB7BBsi.js.map +1 -0
- package/dist/functions/context.d.ts +26 -6
- package/dist/functions/define-function.d.ts +7 -4
- package/dist/functions/guards.d.ts +22 -5
- package/dist/functions/index.d.ts +2 -2
- package/dist/functions/index.js +114 -37
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/request-timeout.d.ts +7 -5
- package/dist/history/history-routes.d.ts +9 -0
- package/dist/history/retention-sweep.d.ts +81 -0
- package/dist/{history-recorder-B4MpJfJK.js → history-recorder-DHenr3lm.js} +4 -4
- package/dist/{history-recorder-B4MpJfJK.js.map → history-recorder-DHenr3lm.js.map} +1 -1
- package/dist/{history-store-BhxWOuz9.js → history-store-DPofGVDp.js} +2 -2
- package/dist/{history-store-BhxWOuz9.js.map → history-store-DPofGVDp.js.map} +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.es.js +23363 -17552
- package/dist/index.es.js.map +1 -1
- package/dist/init/health.d.ts +17 -2
- package/dist/init/shutdown.d.ts +20 -0
- package/dist/init.d.ts +70 -6
- package/dist/internal-tables-L2t39RdT.js +136 -0
- package/dist/internal-tables-L2t39RdT.js.map +1 -0
- package/dist/jobs/job-queue.d.ts +8 -0
- package/dist/jobs/job-store.d.ts +24 -0
- package/dist/jobs/types.d.ts +25 -4
- package/dist/{jobs-CazMYhyy.js → jobs-BpD4Q0qR.js} +235 -25
- package/dist/jobs-BpD4Q0qR.js.map +1 -0
- package/dist/{jwt-DnQHNFCl.js → jwt-Bqcu7cGt.js} +207 -77
- package/dist/jwt-Bqcu7cGt.js.map +1 -0
- package/dist/{keys-CogCQpxG.js → keys-GAVZqbqx.js} +3 -17
- package/dist/{keys-CogCQpxG.js.map → keys-GAVZqbqx.js.map} +1 -1
- package/dist/load-failure-UvzZgGXT.js +48 -0
- package/dist/load-failure-UvzZgGXT.js.map +1 -0
- package/dist/{logger-DO2PZc4i.js → logger-D-S-hO5e.js} +26 -3
- package/dist/logger-D-S-hO5e.js.map +1 -0
- package/dist/{logs-routes-Bj4TYYUl.js → logs-routes-DAdv37GI.js} +48 -8
- package/dist/logs-routes-DAdv37GI.js.map +1 -0
- package/dist/mcp/consent-page.d.ts +1 -1
- package/dist/mcp/mcp-routes.d.ts +7 -0
- package/dist/mcp/mcp-tools.d.ts +23 -9
- package/dist/mcp/oauth-metadata.d.ts +21 -16
- package/dist/mcp/oauth-routes.d.ts +7 -1
- package/dist/metrics/index.d.ts +18 -0
- package/dist/metrics/pool-wait.d.ts +50 -0
- package/dist/offline-8Ks3yp9n-CadBKm2D.js +3305 -0
- package/dist/offline-8Ks3yp9n-CadBKm2D.js.map +1 -0
- package/dist/offline-store-D9ytNNaa-BCr2vsFB.js +1903 -0
- package/dist/offline-store-D9ytNNaa-BCr2vsFB.js.map +1 -0
- package/dist/{openapi-generator-O_O24MAT.js → openapi-generator-BqoUh4Ij.js} +455 -80
- package/dist/openapi-generator-BqoUh4Ij.js.map +1 -0
- package/dist/policy-PdzX-Opd.js +121 -0
- package/dist/policy-PdzX-Opd.js.map +1 -0
- package/dist/project_manifest-R6y_KHms.js +59 -0
- package/dist/project_manifest-R6y_KHms.js.map +1 -0
- package/dist/{proxy-Czngl3p9.js → proxy-qRlqeUmO.js} +2 -2
- package/dist/{proxy-Czngl3p9.js.map → proxy-qRlqeUmO.js.map} +1 -1
- package/dist/{query-parser-DGRVFNM3.js → query-parser-DK9eMkb9.js} +497 -52
- package/dist/query-parser-DK9eMkb9.js.map +1 -0
- package/dist/{request-timeout-C_4C2BeR.js → request-timeout-Dnl2_OBc.js} +17 -10
- package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
- package/dist/schema-edit/apply-schema-change.d.ts +73 -3
- package/dist/schema-edit/local-source.d.ts +29 -0
- package/dist/schema-edit/project-root.d.ts +3 -2
- package/dist/schema-edit/remote-source.d.ts +20 -5
- package/dist/schema-edit/source-editor.d.ts +28 -0
- package/dist/schema-editor-routes-BFf3i8jm.js +136 -0
- package/dist/schema-editor-routes-BFf3i8jm.js.map +1 -0
- package/dist/serve-spa.d.ts +58 -0
- package/dist/services/outbound-post.d.ts +50 -0
- package/dist/services/outbound-url-guard.d.ts +34 -5
- package/dist/services/routed-realtime-service.d.ts +16 -0
- package/dist/services/webhook-service.d.ts +66 -14
- package/dist/soft-delete-params-47yBGs3_.js +668 -0
- package/dist/soft-delete-params-47yBGs3_.js.map +1 -0
- package/dist/sql-export-routes-BNNuGGLP.js +240 -0
- package/dist/sql-export-routes-BNNuGGLP.js.map +1 -0
- package/dist/{src-vkcwKXbT.js → src-CatHFUym.js} +439 -20
- package/dist/src-CatHFUym.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +48 -2
- package/dist/storage/LocalStorageController.d.ts +69 -2
- package/dist/storage/S3StorageController.d.ts +39 -3
- package/dist/storage/image-transform.d.ts +5 -0
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/maintenance-routes.d.ts +23 -0
- package/dist/storage/maintenance.d.ts +126 -0
- package/dist/storage/property-limits.d.ts +41 -6
- package/dist/storage/rendition-cache.d.ts +9 -11
- package/dist/storage/request-keys.d.ts +15 -0
- package/dist/storage/request-principals.d.ts +30 -0
- package/dist/storage/requested-object.d.ts +74 -0
- package/dist/storage/routes.d.ts +52 -14
- package/dist/storage/source-variables.d.ts +10 -0
- package/dist/storage/streams.d.ts +42 -0
- package/dist/storage/tus-handler.d.ts +76 -7
- package/dist/storage/types.d.ts +101 -7
- package/dist/storage/upload-validation.d.ts +13 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
- package/dist/strings.es-BeZWAodn.js +256 -0
- package/dist/strings.es-BeZWAodn.js.map +1 -0
- package/dist/upload-validation-Dd-fnsbA.js +172 -0
- package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
- package/dist/utils/byte-size.d.ts +10 -0
- package/dist/utils/compression.d.ts +6 -0
- package/dist/utils/load-failure.d.ts +14 -0
- package/dist/utils/logger.d.ts +12 -0
- package/package.json +5 -5
- package/dist/GCSStorageController-CjrA4PMo.js +0 -242
- package/dist/GCSStorageController-CjrA4PMo.js.map +0 -1
- package/dist/S3StorageController-B6pKDNVj.js +0 -264
- package/dist/S3StorageController-B6pKDNVj.js.map +0 -1
- package/dist/admin-roles-vYdp_Pil.js +0 -36
- package/dist/admin-roles-vYdp_Pil.js.map +0 -1
- package/dist/admin_block-DxKLmdiv.js +0 -206
- package/dist/admin_block-DxKLmdiv.js.map +0 -1
- package/dist/ast-schema-editor-Mvr50v_S.js.map +0 -1
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
- package/dist/auth-B-GIMpDG.js.map +0 -1
- package/dist/backup-D7YR94N3.js +0 -253
- package/dist/backup-D7YR94N3.js.map +0 -1
- package/dist/contract-routes-CbFjuBwa.js.map +0 -1
- package/dist/cron-loader-DfTj2Hbi.js.map +0 -1
- package/dist/cron-routes-eE8nif_b.js.map +0 -1
- package/dist/cron-scheduler-B0pLfAix.js.map +0 -1
- package/dist/cron-store-TcoGz-xS.js +0 -305
- package/dist/cron-store-TcoGz-xS.js.map +0 -1
- package/dist/dynamic-import-X40pTZUQ.js +0 -23
- package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
- package/dist/errors-DWsX4yTd.js.map +0 -1
- package/dist/function-loader-xnbDAPfa.js.map +0 -1
- package/dist/function-routes-Chet4-lB.js.map +0 -1
- package/dist/jobs-CazMYhyy.js.map +0 -1
- package/dist/jwt-DnQHNFCl.js.map +0 -1
- package/dist/logger-DO2PZc4i.js.map +0 -1
- package/dist/logs-routes-Bj4TYYUl.js.map +0 -1
- package/dist/openapi-generator-O_O24MAT.js.map +0 -1
- package/dist/query-parser-DGRVFNM3.js.map +0 -1
- package/dist/request-timeout-C_4C2BeR.js.map +0 -1
- package/dist/schema-editor-routes-C5-lh_jO.js +0 -87
- package/dist/schema-editor-routes-C5-lh_jO.js.map +0 -1
- package/dist/src-pmvW7BFx.js +0 -6688
- package/dist/src-pmvW7BFx.js.map +0 -1
- package/dist/src-vkcwKXbT.js.map +0 -1
- package/dist/types-BfKcm9do.js +0 -39
- 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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"}
|
package/dist/serve-spa.d.ts
CHANGED
|
@@ -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.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
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
|
-
|
|
71
|
-
private
|
|
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
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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
|
-
|
|
122
|
-
|
|
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<
|
|
133
|
-
private
|
|
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;
|