@elytracms/studio 0.0.88 → 0.0.91
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/convex/asset-revalidate.test.ts +49 -43
- package/convex/cms.ts +3 -4
- package/convex/delivery.ts +8 -11
- package/convex/document-revalidate.test.ts +188 -0
- package/convex/redirect-revalidate.test.ts +50 -37
- package/convex/revalidate.ts +18 -30
- package/convex/triggers.ts +47 -38
- package/convex/webhooks.ts +77 -62
- package/dist/components/account/self-account-view-source.js +2 -1
- package/dist/components/account/self-account-view-source.js.map +1 -1
- package/dist/components/ai/ai-chat-panel.js +1 -1
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/app-views/default-modules.js +3 -3
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-grid.js +1 -1
- package/dist/components/assets/asset-grid.js.map +1 -1
- package/dist/components/assets/asset-selection.d.ts +8 -9
- package/dist/components/assets/asset-selection.js +26 -16
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.js +3 -2
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +7 -5
- package/dist/components/builder/blocks-model.js +11 -9
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.js +1 -1
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +11 -3
- package/dist/components/builder/builder-store.js +12 -5
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +7 -1
- package/dist/components/builder/canvas-dnd.js +6 -3
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +13 -1
- package/dist/components/builder/canvas-render-context.js +28 -18
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas.js +1 -1
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +0 -6
- package/dist/components/builder/clipboard-actions.js +9 -4
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +22 -0
- package/dist/components/builder/composition-field-editor.js +47 -8
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/inline-edit.d.ts +7 -4
- package/dist/components/builder/inline-edit.js +11 -8
- package/dist/components/builder/inline-edit.js.map +1 -1
- package/dist/components/builder/insert-context.js +9 -5
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/layout-inspector.d.ts +2 -2
- package/dist/components/builder/layout-inspector.js +8 -6
- package/dist/components/builder/layout-inspector.js.map +1 -1
- package/dist/components/builder/node-factory.d.ts +31 -14
- package/dist/components/builder/node-factory.js +73 -93
- package/dist/components/builder/node-factory.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +3 -1
- package/dist/components/builder/page-tree.js +6 -4
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +2 -1
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +2 -2
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +7 -3
- package/dist/components/builder/reuse.js +19 -11
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/builder/selection-overlay.d.ts +2 -2
- package/dist/components/builder/selection-overlay.js +3 -3
- package/dist/components/builder/selection-overlay.js.map +1 -1
- package/dist/components/builder/spacing-handles.d.ts +2 -2
- package/dist/components/builder/spacing-handles.js +3 -3
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/asset-field.d.ts +1 -1
- package/dist/components/cms/asset-field.js +15 -13
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.js +3 -4
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +0 -6
- package/dist/components/cms/cell-edit.js +5 -23
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +3 -2
- package/dist/components/cms/cms-store.js +23 -24
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +14 -4
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +28 -24
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/discard-draft.d.ts +1 -2
- package/dist/components/cms/discard-draft.js +1 -1
- package/dist/components/cms/discard-draft.js.map +1 -1
- package/dist/components/cms/duplicate-document.d.ts +3 -2
- package/dist/components/cms/duplicate-document.js +5 -5
- package/dist/components/cms/duplicate-document.js.map +1 -1
- package/dist/components/cms/editable-cell.js +11 -7
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control-lib.d.ts +10 -0
- package/dist/components/cms/field-control-lib.js +20 -0
- package/dist/components/cms/field-control-lib.js.map +1 -1
- package/dist/components/cms/field-control.js +10 -9
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/object-array-field.js +3 -5
- package/dist/components/cms/object-array-field.js.map +1 -1
- package/dist/components/cms/prop-fields.d.ts +16 -3
- package/dist/components/cms/prop-fields.js +57 -12
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/publish-clean.d.ts +13 -6
- package/dist/components/cms/publish-clean.js +14 -6
- package/dist/components/cms/publish-clean.js.map +1 -1
- package/dist/components/cms/rich-text/embed-props.d.ts +6 -1
- package/dist/components/cms/rich-text/embed-props.js +7 -12
- package/dist/components/cms/rich-text/embed-props.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +13 -6
- package/dist/components/cms/rich-text/insert.js +16 -17
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/use-cell-edit.js +7 -12
- package/dist/components/cms/use-cell-edit.js.map +1 -1
- package/dist/components/cms/use-discard-controller.d.ts +1 -2
- package/dist/components/cms/use-discard-controller.js +1 -2
- package/dist/components/cms/use-discard-controller.js.map +1 -1
- package/dist/components/cms/use-publish-controller.js +3 -2
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/cms/versions-pane.js +2 -2
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/components/component-detail.d.ts +1 -1
- package/dist/components/components/component-detail.js +2 -4
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.js +9 -4
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/components/components-index.d.ts +2 -2
- package/dist/components/components/components-index.js +3 -6
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/components/field-inspector.js +5 -2
- package/dist/components/components/field-inspector.js.map +1 -1
- package/dist/components/components/registry-browser.d.ts +6 -5
- package/dist/components/components/registry-browser.js +11 -37
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.d.ts +14 -21
- package/dist/components/components/taxonomy.js +5 -29
- package/dist/components/components/taxonomy.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +1 -0
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +20 -12
- package/dist/components/components/transfer.js +27 -21
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/delivery-health-banner.d.ts +3 -2
- package/dist/components/delivery-health-banner.js +3 -2
- package/dist/components/delivery-health-banner.js.map +1 -1
- package/dist/components/redirects/redirects-collection-view-source.js +3 -2
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
- package/dist/components/users/users-collection-view-source.js +2 -1
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- package/dist/lib/ai/apply-content.js +32 -28
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +16 -14
- package/dist/lib/ai/constraints.js +64 -48
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/translate-document.js +11 -7
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +9 -5
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.js +15 -32
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/vite.d.ts +22 -2
- package/dist/lib/ai/vite.js +12 -3
- package/dist/lib/ai/vite.js.map +1 -1
- package/dist/lib/assets/asset-placements.d.ts +3 -10
- package/dist/lib/assets/asset-placements.js +17 -8
- package/dist/lib/assets/asset-placements.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +3 -3
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/published-documents.d.ts +14 -3
- package/dist/lib/cms/published-documents.js +12 -3
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +4 -4
- package/dist/lib/cms-views/view-model.js +26 -20
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +10 -7
- package/dist/lib/data/collection-content.js +70 -29
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/delivery/instant-publish.d.ts +0 -3
- package/dist/lib/delivery/instant-publish.js +0 -1
- package/dist/lib/delivery/instant-publish.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +22 -7
- package/dist/lib/dnd/drop-rules.js +21 -14
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +25 -9
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +21 -7
- package/dist/lib/i18n/messages.en.js +28 -9
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/locale/fallback.d.ts +5 -19
- package/dist/lib/locale/fallback.js +18 -39
- package/dist/lib/locale/fallback.js.map +1 -1
- package/dist/lib/locale/locale-store.js +2 -1
- package/dist/lib/locale/locale-store.js.map +1 -1
- package/dist/lib/operations/cms-persistence.js +7 -4
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +4 -6
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/backend-mode.d.ts +5 -25
- package/dist/lib/persistence/backend-mode.js +13 -30
- package/dist/lib/persistence/backend-mode.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +4 -9
- package/dist/lib/persona/labels.js +5 -12
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/references/reference-model.d.ts +17 -5
- package/dist/lib/references/reference-model.js +20 -28
- package/dist/lib/references/reference-model.js.map +1 -1
- package/dist/lib/references/references-store.js +7 -0
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/registry/__fixtures__/demo-components.d.ts +32 -0
- package/dist/lib/registry/{project-components.js → __fixtures__/demo-components.js} +18 -5
- package/dist/lib/registry/__fixtures__/demo-components.js.map +1 -0
- package/dist/lib/registry/index.d.ts +9 -13
- package/dist/lib/registry/index.js +34 -61
- package/dist/lib/registry/index.js.map +1 -1
- package/dist/lib/registry/preview.d.ts +22 -7
- package/dist/lib/registry/preview.js +26 -89
- package/dist/lib/registry/preview.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +0 -7
- package/dist/lib/workspace/index.js +0 -9
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/previews/enumerate.d.ts +23 -27
- package/dist/previews/enumerate.js +27 -48
- package/dist/previews/enumerate.js.map +1 -1
- package/dist/previews/harness.js +1 -1
- package/dist/previews/harness.js.map +1 -1
- package/package.json +4 -7
- package/dist/lib/registry/project-components.d.ts +0 -16
- package/dist/lib/registry/project-components.js.map +0 -1
- package/dist/lib/webhooks/config.d.ts +0 -15
- package/dist/lib/webhooks/config.js +0 -65
- package/dist/lib/webhooks/config.js.map +0 -1
- package/dist/lib/webhooks/dispatcher.d.ts +0 -70
- package/dist/lib/webhooks/dispatcher.js +0 -61
- package/dist/lib/webhooks/dispatcher.js.map +0 -1
- package/dist/lib/webhooks/index.d.ts +0 -11
- package/dist/lib/webhooks/index.js +0 -10
- package/dist/lib/webhooks/index.js.map +0 -1
- package/dist/lib/webhooks/signing.d.ts +0 -15
- package/dist/lib/webhooks/signing.js +0 -24
- package/dist/lib/webhooks/signing.js.map +0 -1
- package/dist/lib/webhooks/tags.d.ts +0 -25
- package/dist/lib/webhooks/tags.js +0 -289
- package/dist/lib/webhooks/tags.js.map +0 -1
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { attachRevalidationWebhooks } from './dispatcher.js';
|
|
2
|
-
import { studioEnv } from '../persistence/backend-mode.js';
|
|
3
|
-
import { resolveDeploymentLocales } from '../workspace/index.js';
|
|
4
|
-
/**
|
|
5
|
-
* Environment wiring for the EC-146 revalidation webhooks. The dispatcher is
|
|
6
|
-
* attached only when a host endpoint is configured, so dev sessions and tests
|
|
7
|
-
* (no env) run with zero webhook overhead.
|
|
8
|
-
*
|
|
9
|
-
* Configuration (Vite client env; SSR falls back to `process.env`):
|
|
10
|
-
*
|
|
11
|
-
* - `VITE_REVALIDATE_ENDPOINT` — the host's revalidate route URL
|
|
12
|
-
* (e.g. `https://example-site.vercel.app/api/revalidate`)
|
|
13
|
-
* - `VITE_REVALIDATE_SECRET` — shared HMAC secret (must equal the host's
|
|
14
|
-
* `REVALIDATE_SECRET`). Browser-exposed by design in the v1 dispatch path —
|
|
15
|
-
* see the CORS/secret notes in `dispatcher.ts`.
|
|
16
|
-
* - `VITE_REVALIDATE_LOCALES` — optional comma-separated locale list that can ADD
|
|
17
|
-
* locales to the revalidation tag set. It can never NARROW it: the project's
|
|
18
|
-
* `elytra.config.ts` locales are always covered (EC-192), so a non-default
|
|
19
|
-
* locale like `/de` can never be the one left stale after a publish.
|
|
20
|
-
*
|
|
21
|
-
* The host must allow the studio origin via
|
|
22
|
-
* `createRevalidateRoute({ allowOrigin })` for the browser preflight to pass.
|
|
23
|
-
*/
|
|
24
|
-
// EC-357: env reads go through the ONE studio env surface (`studioEnv()` — the
|
|
25
|
-
// mount-provided env first, then the engine-visible Vite env over `process.env`).
|
|
26
|
-
// A direct `import.meta.env` read here would be dep-optimized away in a real
|
|
27
|
-
// consumer (pinned by `env-read-discipline.test.ts`).
|
|
28
|
-
function readEnv(name) {
|
|
29
|
-
return studioEnv()[name];
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Attach the dispatcher from environment configuration. Returns `null` when
|
|
33
|
-
* no endpoint/secret is configured (the common local-dev case).
|
|
34
|
-
*/
|
|
35
|
-
export function attachConfiguredRevalidationWebhooks(events) {
|
|
36
|
-
const url = readEnv('VITE_REVALIDATE_ENDPOINT');
|
|
37
|
-
const secret = readEnv('VITE_REVALIDATE_SECRET');
|
|
38
|
-
if (!url || !secret)
|
|
39
|
-
return null;
|
|
40
|
-
return attachRevalidationWebhooks(events, {
|
|
41
|
-
endpoints: [{ url, secret }],
|
|
42
|
-
locales: revalidationLocales(),
|
|
43
|
-
onError: (error, context) => {
|
|
44
|
-
// Visible, never fatal: a host outage must not break editing.
|
|
45
|
-
console.warn(`[elytra] revalidation webhook delivery failed (${context.url})`, error);
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* The locales the dispatcher emits per-collection tags for. The project's
|
|
51
|
-
* configured locales (the same set the host tags by) are always covered so no
|
|
52
|
-
* locale can be silently dropped (EC-192 — the `/de` freshness fix); the
|
|
53
|
-
* `VITE_REVALIDATE_LOCALES` env may only ADD to that set. Falls back to `en`
|
|
54
|
-
* only when neither config nor env supplies a locale (env-only wiring).
|
|
55
|
-
*/
|
|
56
|
-
export function revalidationLocales() {
|
|
57
|
-
const configLocales = resolveDeploymentLocales();
|
|
58
|
-
const envLocales = (readEnv('VITE_REVALIDATE_LOCALES') ?? '')
|
|
59
|
-
.split(',')
|
|
60
|
-
.map((locale) => locale.trim())
|
|
61
|
-
.filter((locale) => locale.length > 0);
|
|
62
|
-
const union = [...new Set([...configLocales, ...envLocales])];
|
|
63
|
-
return union.length > 0 ? union : ['en'];
|
|
64
|
-
}
|
|
65
|
-
//# sourceMappingURL=config.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/lib/webhooks/config.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AAEzD,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAA;AACvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAA;AAEvD;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,+EAA+E;AAC/E,kFAAkF;AAClF,6EAA6E;AAC7E,sDAAsD;AACtD,SAAS,OAAO,CAAC,IAAY;IAC3B,OAAO,SAAS,EAAE,CAAC,IAAI,CAAC,CAAA;AAC1B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oCAAoC,CAClD,MAAyB;IAEzB,MAAM,GAAG,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAA;IAC/C,MAAM,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAA;IAChD,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAEhC,OAAO,0BAA0B,CAAC,MAAM,EAAE;QACxC,SAAS,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;QAC5B,OAAO,EAAE,mBAAmB,EAAE;QAC9B,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;YAC1B,8DAA8D;YAC9D,OAAO,CAAC,IAAI,CAAC,kDAAkD,OAAO,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC,CAAA;QACvF,CAAC;KACF,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,aAAa,GAAG,wBAAwB,EAAE,CAAA;IAChD,MAAM,UAAU,GAAG,CAAC,OAAO,CAAC,yBAAyB,CAAC,IAAI,EAAE,CAAC;SAC1D,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;SAC9B,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IACxC,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,aAAa,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,CAAA;IAC7D,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;AAC1C,CAAC","sourcesContent":["import type { OperationEventBus } from '@elytracms/core/operations'\nimport { attachRevalidationWebhooks } from './dispatcher'\nimport type { WebhookDispatcherAttachment } from './dispatcher'\nimport { studioEnv } from '../persistence/backend-mode'\nimport { resolveDeploymentLocales } from '../workspace'\n\n/**\n * Environment wiring for the EC-146 revalidation webhooks. The dispatcher is\n * attached only when a host endpoint is configured, so dev sessions and tests\n * (no env) run with zero webhook overhead.\n *\n * Configuration (Vite client env; SSR falls back to `process.env`):\n *\n * - `VITE_REVALIDATE_ENDPOINT` — the host's revalidate route URL\n * (e.g. `https://example-site.vercel.app/api/revalidate`)\n * - `VITE_REVALIDATE_SECRET` — shared HMAC secret (must equal the host's\n * `REVALIDATE_SECRET`). Browser-exposed by design in the v1 dispatch path —\n * see the CORS/secret notes in `dispatcher.ts`.\n * - `VITE_REVALIDATE_LOCALES` — optional comma-separated locale list that can ADD\n * locales to the revalidation tag set. It can never NARROW it: the project's\n * `elytra.config.ts` locales are always covered (EC-192), so a non-default\n * locale like `/de` can never be the one left stale after a publish.\n *\n * The host must allow the studio origin via\n * `createRevalidateRoute({ allowOrigin })` for the browser preflight to pass.\n */\n\n// EC-357: env reads go through the ONE studio env surface (`studioEnv()` — the\n// mount-provided env first, then the engine-visible Vite env over `process.env`).\n// A direct `import.meta.env` read here would be dep-optimized away in a real\n// consumer (pinned by `env-read-discipline.test.ts`).\nfunction readEnv(name: string): string | undefined {\n return studioEnv()[name]\n}\n\n/**\n * Attach the dispatcher from environment configuration. Returns `null` when\n * no endpoint/secret is configured (the common local-dev case).\n */\nexport function attachConfiguredRevalidationWebhooks(\n events: OperationEventBus,\n): WebhookDispatcherAttachment | null {\n const url = readEnv('VITE_REVALIDATE_ENDPOINT')\n const secret = readEnv('VITE_REVALIDATE_SECRET')\n if (!url || !secret) return null\n\n return attachRevalidationWebhooks(events, {\n endpoints: [{ url, secret }],\n locales: revalidationLocales(),\n onError: (error, context) => {\n // Visible, never fatal: a host outage must not break editing.\n console.warn(`[elytra] revalidation webhook delivery failed (${context.url})`, error)\n },\n })\n}\n\n/**\n * The locales the dispatcher emits per-collection tags for. The project's\n * configured locales (the same set the host tags by) are always covered so no\n * locale can be silently dropped (EC-192 — the `/de` freshness fix); the\n * `VITE_REVALIDATE_LOCALES` env may only ADD to that set. Falls back to `en`\n * only when neither config nor env supplies a locale (env-only wiring).\n */\nexport function revalidationLocales(): string[] {\n const configLocales = resolveDeploymentLocales()\n const envLocales = (readEnv('VITE_REVALIDATE_LOCALES') ?? '')\n .split(',')\n .map((locale) => locale.trim())\n .filter((locale) => locale.length > 0)\n const union = [...new Set([...configLocales, ...envLocales])]\n return union.length > 0 ? union : ['en']\n}\n"]}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { OperationEvent, OperationEventBus } from '@elytracms/core/operations';
|
|
2
|
-
/**
|
|
3
|
-
* Revalidation webhook dispatcher (EC-146): subscribes to the EC-139
|
|
4
|
-
* operations event bus and POSTs a signed `{projectId, event, tags,
|
|
5
|
-
* timestamp}` payload to every configured host revalidate endpoint whenever a
|
|
6
|
-
* command that can change **published** output succeeds (document
|
|
7
|
-
* publish/unpublish/pinVersion/delete, editorial redirect save/remove, plus
|
|
8
|
-
* asset edits — EC-501 narrowed this from "any document write": a draft save
|
|
9
|
-
* cannot move published state, so it fires nothing; EC-510 added the redirect
|
|
10
|
-
* writes, which plainly can). The host's `createRevalidateRoute` (`@elytracms/next`)
|
|
11
|
-
* verifies the HMAC and maps the tags onto `revalidateTag`.
|
|
12
|
-
*
|
|
13
|
-
* This runs **wherever the operations client runs** — in the v1 studio that
|
|
14
|
-
* is the browser, so deliveries are cross-origin `fetch` calls. CORS
|
|
15
|
-
* implications (documented honestly):
|
|
16
|
-
*
|
|
17
|
-
* - the custom signature header always triggers a preflight, so the host
|
|
18
|
-
* endpoint must answer `OPTIONS` and allow the studio origin —
|
|
19
|
-
* `createRevalidateRoute({ allowOrigin })` does both;
|
|
20
|
-
* - the shared secret necessarily ships in the studio bundle in this mode.
|
|
21
|
-
* Acceptable for v1 (the secret only gates cache invalidation, never
|
|
22
|
-
* content access); the server-side Convex action variant
|
|
23
|
-
* (`apps/builder/convex/webhooks.ts`) keeps the secret out of the browser
|
|
24
|
-
* for Convex-backed deployments.
|
|
25
|
-
*
|
|
26
|
-
* Deliveries are fire-and-forget on a serial queue (same pattern as the
|
|
27
|
-
* EC-155 reference index): a slow or unreachable host never blocks the
|
|
28
|
-
* command pipeline, failures surface through `onError` only, and tests await
|
|
29
|
-
* `flush()` for determinism.
|
|
30
|
-
*/
|
|
31
|
-
export interface WebhookEndpoint {
|
|
32
|
-
/** Absolute URL of the host's revalidate route, e.g. `https://site/api/revalidate`. */
|
|
33
|
-
url: string;
|
|
34
|
-
/** Shared HMAC secret for this endpoint. */
|
|
35
|
-
secret: string;
|
|
36
|
-
}
|
|
37
|
-
export interface WebhookDispatcherOptions {
|
|
38
|
-
endpoints: readonly WebhookEndpoint[];
|
|
39
|
-
/**
|
|
40
|
-
* All locales the project serves. Document events sweep the collection
|
|
41
|
-
* membership tag once per locale (`collection:<name>:<locale>`).
|
|
42
|
-
*/
|
|
43
|
-
locales: readonly string[];
|
|
44
|
-
/** Injectable transport (tests); defaults to global `fetch`. */
|
|
45
|
-
fetchFn?: typeof fetch;
|
|
46
|
-
/** Injectable clock (tests); payload `timestamp` is `now().toISOString()`. */
|
|
47
|
-
now?: () => Date;
|
|
48
|
-
/** Delivery failures land here (never thrown into the command pipeline). */
|
|
49
|
-
onError?: (error: unknown, context: {
|
|
50
|
-
url: string;
|
|
51
|
-
event: OperationEvent;
|
|
52
|
-
}) => void;
|
|
53
|
-
}
|
|
54
|
-
export interface WebhookDispatcherAttachment {
|
|
55
|
-
detach(): void;
|
|
56
|
-
/** Await all queued deliveries (tests / shutdown). */
|
|
57
|
-
flush(): Promise<void>;
|
|
58
|
-
}
|
|
59
|
-
/** The signed payload shape (mirror of `revalidatePayloadSchema` in @elytracms/next). */
|
|
60
|
-
export interface RevalidateWebhookPayload {
|
|
61
|
-
projectId: string;
|
|
62
|
-
event: string;
|
|
63
|
-
tags: string[];
|
|
64
|
-
timestamp: string;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Attach the dispatcher to an event bus; returns a detach/flush handle.
|
|
68
|
-
* With no endpoints configured this still subscribes but never queues work.
|
|
69
|
-
*/
|
|
70
|
-
export declare function attachRevalidationWebhooks(events: OperationEventBus, options: WebhookDispatcherOptions): WebhookDispatcherAttachment;
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { WEBHOOK_SIGNATURE_HEADER, signWebhookBody } from './signing.js';
|
|
2
|
-
import { webhookTagsForEvent } from './tags.js';
|
|
3
|
-
async function deliver(endpoint, tagSet, timestamp, fetchFn) {
|
|
4
|
-
const payload = {
|
|
5
|
-
projectId: tagSet.projectId,
|
|
6
|
-
event: tagSet.event,
|
|
7
|
-
tags: tagSet.tags,
|
|
8
|
-
timestamp,
|
|
9
|
-
};
|
|
10
|
-
// Sign the exact string that goes on the wire.
|
|
11
|
-
const rawBody = JSON.stringify(payload);
|
|
12
|
-
const signature = await signWebhookBody(rawBody, endpoint.secret);
|
|
13
|
-
const response = await fetchFn(endpoint.url, {
|
|
14
|
-
method: 'POST',
|
|
15
|
-
headers: {
|
|
16
|
-
'content-type': 'application/json',
|
|
17
|
-
[WEBHOOK_SIGNATURE_HEADER]: signature,
|
|
18
|
-
},
|
|
19
|
-
body: rawBody,
|
|
20
|
-
});
|
|
21
|
-
if (!response.ok) {
|
|
22
|
-
throw new Error(`Revalidation webhook to ${endpoint.url} answered ${response.status}.`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Attach the dispatcher to an event bus; returns a detach/flush handle.
|
|
27
|
-
* With no endpoints configured this still subscribes but never queues work.
|
|
28
|
-
*/
|
|
29
|
-
export function attachRevalidationWebhooks(events, options) {
|
|
30
|
-
const fetchFn = options.fetchFn ?? fetch;
|
|
31
|
-
const now = options.now ?? (() => new Date());
|
|
32
|
-
let pending = Promise.resolve();
|
|
33
|
-
const listener = (event) => {
|
|
34
|
-
if (options.endpoints.length === 0)
|
|
35
|
-
return;
|
|
36
|
-
const tagSet = webhookTagsForEvent(event, { locales: options.locales });
|
|
37
|
-
if (!tagSet)
|
|
38
|
-
return;
|
|
39
|
-
const timestamp = now().toISOString();
|
|
40
|
-
for (const endpoint of options.endpoints) {
|
|
41
|
-
pending = pending
|
|
42
|
-
.then(() => deliver(endpoint, tagSet, timestamp, fetchFn))
|
|
43
|
-
.catch((error) => {
|
|
44
|
-
options.onError?.(error, { url: endpoint.url, event });
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
const unsubscribe = events.subscribe(listener);
|
|
49
|
-
return {
|
|
50
|
-
detach: unsubscribe,
|
|
51
|
-
flush: async () => {
|
|
52
|
-
// Drain until no new deliveries were queued while awaiting.
|
|
53
|
-
let current;
|
|
54
|
-
do {
|
|
55
|
-
current = pending;
|
|
56
|
-
await current;
|
|
57
|
-
} while (current !== pending);
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
//# sourceMappingURL=dispatcher.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dispatcher.js","sourceRoot":"","sources":["../../../src/lib/webhooks/dispatcher.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AACrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAA;AAqE5C,KAAK,UAAU,OAAO,CACpB,QAAyB,EACzB,MAAqB,EACrB,SAAiB,EACjB,OAAqB;IAErB,MAAM,OAAO,GAA6B;QACxC,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,SAAS;KACV,CAAA;IACD,+CAA+C;IAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;IACvC,MAAM,SAAS,GAAG,MAAM,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IACjE,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE;QAC3C,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,cAAc,EAAE,kBAAkB;YAClC,CAAC,wBAAwB,CAAC,EAAE,SAAS;SACtC;QACD,IAAI,EAAE,OAAO;KACd,CAAC,CAAA;IACF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,2BAA2B,QAAQ,CAAC,GAAG,aAAa,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAA;IACzF,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,0BAA0B,CACxC,MAAyB,EACzB,OAAiC;IAEjC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,KAAK,CAAA;IACxC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAA;IAE7C,IAAI,OAAO,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAA;IAE9C,MAAM,QAAQ,GAAG,CAAC,KAAqB,EAAQ,EAAE;QAC/C,IAAI,OAAO,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,OAAM;QAC1C,MAAM,MAAM,GAAG,mBAAmB,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;QACvE,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC,WAAW,EAAE,CAAA;QACrC,KAAK,MAAM,QAAQ,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;YACzC,OAAO,GAAG,OAAO;iBACd,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;iBACzD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;gBACxB,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,CAAA;YACxD,CAAC,CAAC,CAAA;QACN,CAAC;IACH,CAAC,CAAA;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;IAC9C,OAAO;QACL,MAAM,EAAE,WAAW;QACnB,KAAK,EAAE,KAAK,IAAI,EAAE;YAChB,4DAA4D;YAC5D,IAAI,OAAsB,CAAA;YAC1B,GAAG,CAAC;gBACF,OAAO,GAAG,OAAO,CAAA;gBACjB,MAAM,OAAO,CAAA;YACf,CAAC,QAAQ,OAAO,KAAK,OAAO,EAAC;QAC/B,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["import type { OperationEvent, OperationEventBus } from '@elytracms/core/operations'\nimport { WEBHOOK_SIGNATURE_HEADER, signWebhookBody } from './signing'\nimport { webhookTagsForEvent } from './tags'\nimport type { WebhookTagSet } from './tags'\n\n/**\n * Revalidation webhook dispatcher (EC-146): subscribes to the EC-139\n * operations event bus and POSTs a signed `{projectId, event, tags,\n * timestamp}` payload to every configured host revalidate endpoint whenever a\n * command that can change **published** output succeeds (document\n * publish/unpublish/pinVersion/delete, editorial redirect save/remove, plus\n * asset edits — EC-501 narrowed this from \"any document write\": a draft save\n * cannot move published state, so it fires nothing; EC-510 added the redirect\n * writes, which plainly can). The host's `createRevalidateRoute` (`@elytracms/next`)\n * verifies the HMAC and maps the tags onto `revalidateTag`.\n *\n * This runs **wherever the operations client runs** — in the v1 studio that\n * is the browser, so deliveries are cross-origin `fetch` calls. CORS\n * implications (documented honestly):\n *\n * - the custom signature header always triggers a preflight, so the host\n * endpoint must answer `OPTIONS` and allow the studio origin —\n * `createRevalidateRoute({ allowOrigin })` does both;\n * - the shared secret necessarily ships in the studio bundle in this mode.\n * Acceptable for v1 (the secret only gates cache invalidation, never\n * content access); the server-side Convex action variant\n * (`apps/builder/convex/webhooks.ts`) keeps the secret out of the browser\n * for Convex-backed deployments.\n *\n * Deliveries are fire-and-forget on a serial queue (same pattern as the\n * EC-155 reference index): a slow or unreachable host never blocks the\n * command pipeline, failures surface through `onError` only, and tests await\n * `flush()` for determinism.\n */\n\nexport interface WebhookEndpoint {\n /** Absolute URL of the host's revalidate route, e.g. `https://site/api/revalidate`. */\n url: string\n /** Shared HMAC secret for this endpoint. */\n secret: string\n}\n\nexport interface WebhookDispatcherOptions {\n endpoints: readonly WebhookEndpoint[]\n /**\n * All locales the project serves. Document events sweep the collection\n * membership tag once per locale (`collection:<name>:<locale>`).\n */\n locales: readonly string[]\n /** Injectable transport (tests); defaults to global `fetch`. */\n fetchFn?: typeof fetch\n /** Injectable clock (tests); payload `timestamp` is `now().toISOString()`. */\n now?: () => Date\n /** Delivery failures land here (never thrown into the command pipeline). */\n onError?: (error: unknown, context: { url: string; event: OperationEvent }) => void\n}\n\nexport interface WebhookDispatcherAttachment {\n detach(): void\n /** Await all queued deliveries (tests / shutdown). */\n flush(): Promise<void>\n}\n\n/** The signed payload shape (mirror of `revalidatePayloadSchema` in @elytracms/next). */\nexport interface RevalidateWebhookPayload {\n projectId: string\n event: string\n tags: string[]\n timestamp: string\n}\n\nasync function deliver(\n endpoint: WebhookEndpoint,\n tagSet: WebhookTagSet,\n timestamp: string,\n fetchFn: typeof fetch,\n): Promise<void> {\n const payload: RevalidateWebhookPayload = {\n projectId: tagSet.projectId,\n event: tagSet.event,\n tags: tagSet.tags,\n timestamp,\n }\n // Sign the exact string that goes on the wire.\n const rawBody = JSON.stringify(payload)\n const signature = await signWebhookBody(rawBody, endpoint.secret)\n const response = await fetchFn(endpoint.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n [WEBHOOK_SIGNATURE_HEADER]: signature,\n },\n body: rawBody,\n })\n if (!response.ok) {\n throw new Error(`Revalidation webhook to ${endpoint.url} answered ${response.status}.`)\n }\n}\n\n/**\n * Attach the dispatcher to an event bus; returns a detach/flush handle.\n * With no endpoints configured this still subscribes but never queues work.\n */\nexport function attachRevalidationWebhooks(\n events: OperationEventBus,\n options: WebhookDispatcherOptions,\n): WebhookDispatcherAttachment {\n const fetchFn = options.fetchFn ?? fetch\n const now = options.now ?? (() => new Date())\n\n let pending: Promise<void> = Promise.resolve()\n\n const listener = (event: OperationEvent): void => {\n if (options.endpoints.length === 0) return\n const tagSet = webhookTagsForEvent(event, { locales: options.locales })\n if (!tagSet) return\n const timestamp = now().toISOString()\n for (const endpoint of options.endpoints) {\n pending = pending\n .then(() => deliver(endpoint, tagSet, timestamp, fetchFn))\n .catch((error: unknown) => {\n options.onError?.(error, { url: endpoint.url, event })\n })\n }\n }\n\n const unsubscribe = events.subscribe(listener)\n return {\n detach: unsubscribe,\n flush: async () => {\n // Drain until no new deliveries were queued while awaiting.\n let current: Promise<void>\n do {\n current = pending\n await current\n } while (current !== pending)\n },\n }\n}\n"]}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* EC-146 instant publish, emitter side: operations-event-driven webhook
|
|
3
|
-
* dispatch to host revalidate endpoints. See `dispatcher.ts` for the
|
|
4
|
-
* delivery/CORS model and `tags.ts` for the tag computation.
|
|
5
|
-
*/
|
|
6
|
-
export { attachRevalidationWebhooks } from './dispatcher.js';
|
|
7
|
-
export type { RevalidateWebhookPayload, WebhookDispatcherAttachment, WebhookDispatcherOptions, WebhookEndpoint, } from './dispatcher.js';
|
|
8
|
-
export { attachConfiguredRevalidationWebhooks } from './config.js';
|
|
9
|
-
export { WEBHOOK_SIGNATURE_HEADER, signWebhookBody } from './signing.js';
|
|
10
|
-
export { collectionWebhookTag, docWebhookTag, projectSweepWebhookTag, webhookTagsForEvent, } from './tags.js';
|
|
11
|
-
export type { WebhookTagSet } from './tags.js';
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* EC-146 instant publish, emitter side: operations-event-driven webhook
|
|
3
|
-
* dispatch to host revalidate endpoints. See `dispatcher.ts` for the
|
|
4
|
-
* delivery/CORS model and `tags.ts` for the tag computation.
|
|
5
|
-
*/
|
|
6
|
-
export { attachRevalidationWebhooks } from './dispatcher.js';
|
|
7
|
-
export { attachConfiguredRevalidationWebhooks } from './config.js';
|
|
8
|
-
export { WEBHOOK_SIGNATURE_HEADER, signWebhookBody } from './signing.js';
|
|
9
|
-
export { collectionWebhookTag, docWebhookTag, projectSweepWebhookTag, webhookTagsForEvent, } from './tags.js';
|
|
10
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/webhooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AAOzD,OAAO,EAAE,oCAAoC,EAAE,MAAM,UAAU,CAAA;AAC/D,OAAO,EAAE,wBAAwB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AACrE,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,QAAQ,CAAA","sourcesContent":["/**\n * EC-146 instant publish, emitter side: operations-event-driven webhook\n * dispatch to host revalidate endpoints. See `dispatcher.ts` for the\n * delivery/CORS model and `tags.ts` for the tag computation.\n */\nexport { attachRevalidationWebhooks } from './dispatcher'\nexport type {\n RevalidateWebhookPayload,\n WebhookDispatcherAttachment,\n WebhookDispatcherOptions,\n WebhookEndpoint,\n} from './dispatcher'\nexport { attachConfiguredRevalidationWebhooks } from './config'\nexport { WEBHOOK_SIGNATURE_HEADER, signWebhookBody } from './signing'\nexport {\n collectionWebhookTag,\n docWebhookTag,\n projectSweepWebhookTag,\n webhookTagsForEvent,\n} from './tags'\nexport type { WebhookTagSet } from './tags'\n"]}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HMAC-SHA256 webhook signing (EC-146), emitter side.
|
|
3
|
-
*
|
|
4
|
-
* Uses the Web Crypto API (`crypto.subtle`) so the same code runs in the
|
|
5
|
-
* browser (the v1 studio dispatcher), in Node tests, and in Convex actions.
|
|
6
|
-
* The receiving end (`@elytracms/next` `evaluateRevalidateRequest`) verifies
|
|
7
|
-
* the same scheme with `node:crypto` and a timing-safe compare.
|
|
8
|
-
*
|
|
9
|
-
* The signature is computed over the **exact raw body string** that is sent
|
|
10
|
-
* — never re-serialize between signing and sending.
|
|
11
|
-
*/
|
|
12
|
-
/** Header carrying the signature (mirror of `REVALIDATE_SIGNATURE_HEADER`). */
|
|
13
|
-
export declare const WEBHOOK_SIGNATURE_HEADER = "x-elytra-signature";
|
|
14
|
-
/** Sign a raw webhook body: `sha256=<hex hmac>`. */
|
|
15
|
-
export declare function signWebhookBody(rawBody: string, secret: string): Promise<string>;
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HMAC-SHA256 webhook signing (EC-146), emitter side.
|
|
3
|
-
*
|
|
4
|
-
* Uses the Web Crypto API (`crypto.subtle`) so the same code runs in the
|
|
5
|
-
* browser (the v1 studio dispatcher), in Node tests, and in Convex actions.
|
|
6
|
-
* The receiving end (`@elytracms/next` `evaluateRevalidateRequest`) verifies
|
|
7
|
-
* the same scheme with `node:crypto` and a timing-safe compare.
|
|
8
|
-
*
|
|
9
|
-
* The signature is computed over the **exact raw body string** that is sent
|
|
10
|
-
* — never re-serialize between signing and sending.
|
|
11
|
-
*/
|
|
12
|
-
/** Header carrying the signature (mirror of `REVALIDATE_SIGNATURE_HEADER`). */
|
|
13
|
-
export const WEBHOOK_SIGNATURE_HEADER = 'x-elytra-signature';
|
|
14
|
-
function toHex(buffer) {
|
|
15
|
-
return [...new Uint8Array(buffer)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
16
|
-
}
|
|
17
|
-
/** Sign a raw webhook body: `sha256=<hex hmac>`. */
|
|
18
|
-
export async function signWebhookBody(rawBody, secret) {
|
|
19
|
-
const encoder = new TextEncoder();
|
|
20
|
-
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
21
|
-
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(rawBody));
|
|
22
|
-
return `sha256=${toHex(signature)}`;
|
|
23
|
-
}
|
|
24
|
-
//# sourceMappingURL=signing.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"signing.js","sourceRoot":"","sources":["../../../src/lib/webhooks/signing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,+EAA+E;AAC/E,MAAM,CAAC,MAAM,wBAAwB,GAAG,oBAAoB,CAAA;AAE5D,SAAS,KAAK,CAAC,MAAmB;IAChC,OAAO,CAAC,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AAC/F,CAAC;AAED,oDAAoD;AACpD,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAe,EAAE,MAAc;IACnE,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;IACjC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CACvC,KAAK,EACL,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EACtB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EACjC,KAAK,EACL,CAAC,MAAM,CAAC,CACT,CAAA;IACD,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAA;IAChF,OAAO,UAAU,KAAK,CAAC,SAAS,CAAC,EAAE,CAAA;AACrC,CAAC","sourcesContent":["/**\n * HMAC-SHA256 webhook signing (EC-146), emitter side.\n *\n * Uses the Web Crypto API (`crypto.subtle`) so the same code runs in the\n * browser (the v1 studio dispatcher), in Node tests, and in Convex actions.\n * The receiving end (`@elytracms/next` `evaluateRevalidateRequest`) verifies\n * the same scheme with `node:crypto` and a timing-safe compare.\n *\n * The signature is computed over the **exact raw body string** that is sent\n * — never re-serialize between signing and sending.\n */\n\n/** Header carrying the signature (mirror of `REVALIDATE_SIGNATURE_HEADER`). */\nexport const WEBHOOK_SIGNATURE_HEADER = 'x-elytra-signature'\n\nfunction toHex(buffer: ArrayBuffer): string {\n return [...new Uint8Array(buffer)].map((byte) => byte.toString(16).padStart(2, '0')).join('')\n}\n\n/** Sign a raw webhook body: `sha256=<hex hmac>`. */\nexport async function signWebhookBody(rawBody: string, secret: string): Promise<string> {\n const encoder = new TextEncoder()\n const key = await crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n )\n const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(rawBody))\n return `sha256=${toHex(signature)}`\n}\n"]}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { OperationEvent } from '@elytracms/core/operations';
|
|
2
|
-
/** `p:<projectId>:doc:<collection>:<id>` — mirror of `docCacheTag`. */
|
|
3
|
-
export declare function docWebhookTag(projectId: string, collection: string, id: string): string;
|
|
4
|
-
/** `p:<projectId>:collection:<name>:<locale>` — mirror of `collectionCacheTag`. */
|
|
5
|
-
export declare function collectionWebhookTag(projectId: string, collection: string, locale: string): string;
|
|
6
|
-
/** `p:<projectId>:asset:<id>` — mirror of `assetCacheTag` (EC-227). */
|
|
7
|
-
export declare function assetWebhookTag(projectId: string, assetId: string): string;
|
|
8
|
-
/** `p:<projectId>:project` — mirror of `projectSweepTag`. */
|
|
9
|
-
export declare function projectSweepWebhookTag(projectId: string): string;
|
|
10
|
-
/** One computed webhook emission: which project, why, and which tags. */
|
|
11
|
-
export interface WebhookTagSet {
|
|
12
|
-
projectId: string;
|
|
13
|
-
/** The operation name, carried verbatim as the webhook `event`. */
|
|
14
|
-
event: string;
|
|
15
|
-
/** Sorted, deduplicated, fully scoped Next tag strings. */
|
|
16
|
-
tags: string[];
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Compute the webhook tag set for one lifecycle event, or `null` when the
|
|
20
|
-
* event is not a successful publish-shaped command (queries, failures, and
|
|
21
|
-
* unrelated domains never fire webhooks).
|
|
22
|
-
*/
|
|
23
|
-
export declare function webhookTagsForEvent(event: Pick<OperationEvent, 'operation' | 'kind' | 'status' | 'inputSummary'>, options: {
|
|
24
|
-
locales: readonly string[];
|
|
25
|
-
}): WebhookTagSet | null;
|
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure tag computation for the EC-146 revalidation webhooks: map a succeeded
|
|
3
|
-
* publish-shaped operation event onto the Next cache tags it invalidates.
|
|
4
|
-
*
|
|
5
|
-
* ## Tag format (mirror of `packages/next/src/cache.ts`)
|
|
6
|
-
*
|
|
7
|
-
* The canonical tag format lives in `@elytracms/next` (`src/cache.ts`). The
|
|
8
|
-
* builder cannot depend on that package — it would drag the `next` peer
|
|
9
|
-
* dependency into the studio bundle — so the three formatters are mirrored
|
|
10
|
-
* here. Both sides pin the exact literal strings in their tests
|
|
11
|
-
* (`tags.test.ts` here, `cache.test.ts` there) to keep them in lock-step.
|
|
12
|
-
*
|
|
13
|
-
* Every emitted tag is scoped `p:<projectId>:…` — the dispatcher always knows
|
|
14
|
-
* the project id, and the scope keeps multi-project hosts from
|
|
15
|
-
* cross-invalidating. Hosts must configure the same id as `cache.scope`.
|
|
16
|
-
*
|
|
17
|
-
* ## Which events produce which tags
|
|
18
|
-
*
|
|
19
|
-
* The gate is **"can this operation change PUBLISHED output?"** — not "did this
|
|
20
|
-
* operation touch a document" (EC-501). Live delivery is published-only, so an
|
|
21
|
-
* operation that cannot move published state has no cache entry to drop and
|
|
22
|
-
* every tag it emits is waste by construction.
|
|
23
|
-
*
|
|
24
|
-
* - **Document publish/unpublish/pin/delete** (`documents.publish`,
|
|
25
|
-
* `documents.unpublish`, `documents.pinVersion`, `documents.delete`): the
|
|
26
|
-
* document's own
|
|
27
|
-
* `doc:<collection>:<id>` tag — dropping its detail page *and every cached
|
|
28
|
-
* list that actually contained it* (fetch-time tagging) — plus the coarse
|
|
29
|
-
* `collection:<name>:<locale>` membership tag for **all** configured
|
|
30
|
-
* locales, sweeping cached lists whose membership may have changed
|
|
31
|
-
* (new/removed/newly-matching documents).
|
|
32
|
-
* - **Document SAVE** (`documents.upsert`) fires **nothing** — see
|
|
33
|
-
* {@link PUBLISHED_STATE_OPERATIONS}.
|
|
34
|
-
* - **Editorial redirect write** (`redirects.save`, `redirects.remove`,
|
|
35
|
-
* EC-510): the whole-project sweep tag — deliberately coarse, see
|
|
36
|
-
* {@link ROUTING_OPERATIONS}.
|
|
37
|
-
* - **Asset edit** (`assets.upsert` of an existing asset, `assets.remove`,
|
|
38
|
-
* EC-227): the `asset:<id>` tag, dropping exactly the pages that *baked* that
|
|
39
|
-
* asset's delivery shape (alt/dimensions/url). An `assets.upsert` CREATE (no
|
|
40
|
-
* id in the summary — minted server-side) fires nothing: a brand-new asset is
|
|
41
|
-
* in no cached page yet.
|
|
42
|
-
* - **Fallback**: when a `documents` (or `assets.remove`) event's input summary
|
|
43
|
-
* was truncated past the identity (EC-139 caps summaries at 160 chars), the
|
|
44
|
-
* project sweep tag is emitted — coarse, always correct, mirroring the EC-155
|
|
45
|
-
* reference-index fallback. Since EC-501 the surviving document operations
|
|
46
|
-
* all carry a small `{ projectId, ref }` input, so this is a defensive path
|
|
47
|
-
* rather than a routine one — the routine truncation was the document-bearing
|
|
48
|
-
* `documents.upsert`, which no longer reaches here at all.
|
|
49
|
-
*
|
|
50
|
-
* Every branch needs only the `projectId`, which serializes FIRST in every
|
|
51
|
-
* operation input (measured 2026-08-01 across the real `redirects.save`
|
|
52
|
-
* shapes), so {@link projectIdOfSummary}'s regex survives truncation even when
|
|
53
|
-
* the rest of the summary is cut mid-string.
|
|
54
|
-
*/
|
|
55
|
-
function scoped(projectId, tag) {
|
|
56
|
-
return `p:${projectId}:${tag}`;
|
|
57
|
-
}
|
|
58
|
-
/** `p:<projectId>:doc:<collection>:<id>` — mirror of `docCacheTag`. */
|
|
59
|
-
export function docWebhookTag(projectId, collection, id) {
|
|
60
|
-
return scoped(projectId, `doc:${collection}:${id}`);
|
|
61
|
-
}
|
|
62
|
-
/** `p:<projectId>:collection:<name>:<locale>` — mirror of `collectionCacheTag`. */
|
|
63
|
-
export function collectionWebhookTag(projectId, collection, locale) {
|
|
64
|
-
return scoped(projectId, `collection:${collection}:${locale}`);
|
|
65
|
-
}
|
|
66
|
-
/** `p:<projectId>:asset:<id>` — mirror of `assetCacheTag` (EC-227). */
|
|
67
|
-
export function assetWebhookTag(projectId, assetId) {
|
|
68
|
-
return scoped(projectId, `asset:${assetId}`);
|
|
69
|
-
}
|
|
70
|
-
/** `p:<projectId>:project` — mirror of `projectSweepTag`. */
|
|
71
|
-
export function projectSweepWebhookTag(projectId) {
|
|
72
|
-
return scoped(projectId, 'project');
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* The document operations that can change **published** output, and therefore
|
|
76
|
-
* the ones whose success must reach the host's revalidate endpoint.
|
|
77
|
-
*
|
|
78
|
-
* EC-224: publish/unpublish/pinVersion move the version pointer; their
|
|
79
|
-
* `{ projectId, ref }` input drives the doc + per-locale collection tag set
|
|
80
|
-
* (the flat `documents.setState` op is gone). `documents.delete` removes the
|
|
81
|
-
* row outright, taking any pinned version with it.
|
|
82
|
-
*
|
|
83
|
-
* ## Why `documents.upsert` (a draft SAVE) is NOT here — EC-501
|
|
84
|
-
*
|
|
85
|
-
* A save writes the **draft row only**; published readers serve the *pinned
|
|
86
|
-
* version snapshot*, never the draft. That is structural, not a heuristic:
|
|
87
|
-
*
|
|
88
|
-
* - the operation hands the document straight to `cms.upsertDocument`
|
|
89
|
-
* (`core/operations/documents.ts`), whose Convex arg list has no
|
|
90
|
-
* `publishedVersion` field at all (`studio/convex/cms.ts` `upsertDocument`),
|
|
91
|
-
* and whose in-memory twin re-attaches the previous pin verbatim with the
|
|
92
|
-
* comment "only publish/unpublish move the pointer"
|
|
93
|
-
* (`core/persistence/cms.ts`);
|
|
94
|
-
* - the published delivery snapshot skips every row without a pin and serves
|
|
95
|
-
* the pinned snapshot, "never falling back to the draft"
|
|
96
|
-
* (`studio/convex/delivery.ts`);
|
|
97
|
-
* - the Convex write-chokepoint trigger already encodes exactly this verdict
|
|
98
|
-
* server-side — it schedules a revalidation only when `publishedVersion`
|
|
99
|
-
* actually changed, "a plain draft save must not invalidate the published
|
|
100
|
-
* host cache" (`studio/convex/triggers.ts`, EC-286/EC-146). This emitter was
|
|
101
|
-
* simply the one path that never asked.
|
|
102
|
-
*
|
|
103
|
-
* Measured before the change (connected studio → capture endpoint, one short
|
|
104
|
-
* editing session): 12 `documents.upsert` deliveries to 1 `documents.publish`,
|
|
105
|
-
* and 7 of those 12 carried the **whole-project sweep** tag rather than the
|
|
106
|
-
* document's own — because a real document's `inputSummary` exceeds the EC-139
|
|
107
|
-
* 160-char cap, so the identity parse below fails and falls back to the sweep.
|
|
108
|
-
* A single keystroke-batch on a live page was dropping every cached page *and*
|
|
109
|
-
* every cached 404 on the site (EC-489 widened 404s to carry collection tags).
|
|
110
|
-
*
|
|
111
|
-
* ## No exceptions (EC-574)
|
|
112
|
-
*
|
|
113
|
-
* This set used to carry one documented exception: `cms-data-sources` rows, the
|
|
114
|
-
* one document class the published delivery snapshot read from the **draft**
|
|
115
|
-
* side, so a draft save there could change published output silently (the
|
|
116
|
-
* wave-0089 finding). EC-574 deleted the data-binding subsystem and the
|
|
117
|
-
* snapshot's `sources` strand with it (`studio/convex/delivery.ts`), so every
|
|
118
|
-
* document class now reaches delivery through its published pin only and this
|
|
119
|
-
* set is complete as written.
|
|
120
|
-
*/
|
|
121
|
-
const PUBLISHED_STATE_OPERATIONS = new Set([
|
|
122
|
-
'documents.delete',
|
|
123
|
-
// EC-576: the drop takes every pinned version with it; its input carries no `ref`,
|
|
124
|
-
// so it resolves to the whole-project sweep below — the only correct tag for it.
|
|
125
|
-
'documents.clear',
|
|
126
|
-
'documents.publish',
|
|
127
|
-
'documents.unpublish',
|
|
128
|
-
'documents.pinVersion',
|
|
129
|
-
]);
|
|
130
|
-
/**
|
|
131
|
-
* The **routing** operations (EC-510): editorial redirects, which EC-303 moved
|
|
132
|
-
* out of config and into the database. Every *enabled* redirect row is
|
|
133
|
-
* projected into the delivery snapshot the router and `resolvePage` consume
|
|
134
|
-
* (`studio/convex/delivery.ts`), in BOTH perspectives — so saving, retargeting,
|
|
135
|
-
* disabling or deleting one changes published output, exactly the question
|
|
136
|
-
* {@link PUBLISHED_STATE_OPERATIONS} asks. They belong to that question, not a
|
|
137
|
-
* parallel one; they are a separate set only because their tag is different.
|
|
138
|
-
*
|
|
139
|
-
* ## Why the coarse project sweep and NOT a targeted `route:<from>:<locale>`
|
|
140
|
-
*
|
|
141
|
-
* The tag vocabulary has `route:<pattern>:<locale>` and a redirect's `from` IS
|
|
142
|
-
* a route path, so a targeted tag looks available. It is not — it would be
|
|
143
|
-
* silently wrong in four independent ways, and a silent wrong invalidation is
|
|
144
|
-
* the exact failure family EC-489/EC-501/this ticket exist to remove:
|
|
145
|
-
*
|
|
146
|
-
* 1. **`redirects.remove` has no `from` at all.** Its input is
|
|
147
|
-
* `{ projectId, id }` (`core/operations/redirects.ts`), so the path whose
|
|
148
|
-
* cache entry must drop is not in the event. Precision is unobtainable, and
|
|
149
|
-
* a delete is the single most important case to get right.
|
|
150
|
-
* 2. **A save can RENAME `from`.** The event carries only the NEW path; the OLD
|
|
151
|
-
* one keeps serving a redirect that no longer exists. `OperationEvent` has
|
|
152
|
-
* `operation`/`kind`/`status`/`inputSummary` and nothing else — no result,
|
|
153
|
-
* no previous row (EC-501 rejected widening it for the same reason) — so the
|
|
154
|
-
* emitter cannot even detect that a rename happened.
|
|
155
|
-
* 3. **Normalization would have to be mirrored.** The cached entry is tagged
|
|
156
|
-
* with the resolver's `normalizePath(url)`; `from` is editor-typed free text
|
|
157
|
-
* (`z.string().min(1)`), stored verbatim and normalized only at match time.
|
|
158
|
-
* A trailing slash or a query string would produce a tag that matches
|
|
159
|
-
* nothing — no error, no invalidation.
|
|
160
|
-
* 4. **The identity truncates.** MEASURED 2026-08-01 on the real schema: a
|
|
161
|
-
* Konditorei-shaped migration redirect (legacy permalink → new path)
|
|
162
|
-
* serializes to 226 chars and an EC-311 `meta` bag to 216, both past the
|
|
163
|
-
* EC-139 160-char cap. A `from`-based tag would work on a demo-sized
|
|
164
|
-
* redirect and degrade on the real one — EC-501's pathology in reverse.
|
|
165
|
-
*
|
|
166
|
-
* The sweep has none of those failure modes: it needs only `projectId`, which
|
|
167
|
-
* serializes first and therefore survives truncation, so **the truncation
|
|
168
|
-
* fallback and the primary answer are the same tag** — correct by design rather
|
|
169
|
-
* than by accident. `nextCacheTags` stamps `project` on every cached entry, so
|
|
170
|
-
* it is guaranteed to reach the affected URL whatever the redirect changed.
|
|
171
|
-
*
|
|
172
|
-
* ## The cost, and why it is affordable here (unlike EC-501)
|
|
173
|
-
*
|
|
174
|
-
* A sweep drops every cached entry, lazily on next read. That was intolerable
|
|
175
|
-
* for `documents.upsert` because it fired on every ~1s keystroke batch and
|
|
176
|
-
* *none* of those invalidations were necessary. A redirect write is an
|
|
177
|
-
* explicit form save on a rail with `inlineEdit: false` and no autosave
|
|
178
|
-
* (`redirects-collection-view-source.ts`), and every one of them IS a routing
|
|
179
|
-
* change. Measured on a connected studio: a session that deliberately exercised
|
|
180
|
-
* every mutation kind — create, retarget, disable, rename, delete — produced 5
|
|
181
|
-
* writes. The Convex-side path already sweeps the project on every document
|
|
182
|
-
* publish (`convex/revalidate.ts`, "the honest, reliable v1"), so this is
|
|
183
|
-
* strictly rarer than a cost the engine already pays.
|
|
184
|
-
*
|
|
185
|
-
* If a precise redirect tag is ever wanted, the place to get it is the Convex
|
|
186
|
-
* write chokepoint, where `change.oldDoc` holds the previous `from` — not here,
|
|
187
|
-
* where the information does not exist.
|
|
188
|
-
*/
|
|
189
|
-
const ROUTING_OPERATIONS = new Set(['redirects.save', 'redirects.remove']);
|
|
190
|
-
/**
|
|
191
|
-
* Asset-record mutations (EC-227): editing an asset (alt/dimensions/re-upload)
|
|
192
|
-
* must drop the published pages that *baked* its delivery shape, which carry the
|
|
193
|
-
* `asset:<id>` tag. `assets.remove` always carries `assetId`; `assets.upsert`
|
|
194
|
-
* carries `asset.id` only on an EDIT (a CREATE mints the id server-side).
|
|
195
|
-
*/
|
|
196
|
-
const ASSET_OPERATIONS = new Set(['assets.upsert', 'assets.remove']);
|
|
197
|
-
/** Tolerant parse of an EC-139 input summary (may be truncated past 160 chars). */
|
|
198
|
-
function parseSummary(summary) {
|
|
199
|
-
try {
|
|
200
|
-
const parsed = JSON.parse(summary);
|
|
201
|
-
return typeof parsed === 'object' && parsed !== null
|
|
202
|
-
? parsed
|
|
203
|
-
: undefined;
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
return undefined;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
/** Extract the projectId even from a truncated summary (it serializes first). */
|
|
210
|
-
function projectIdOfSummary(summary) {
|
|
211
|
-
const match = /"projectId":"([^"]+)"/.exec(summary);
|
|
212
|
-
return match?.[1];
|
|
213
|
-
}
|
|
214
|
-
function documentRefOf(value) {
|
|
215
|
-
if (typeof value !== 'object' || value === null)
|
|
216
|
-
return undefined;
|
|
217
|
-
const candidate = value;
|
|
218
|
-
return typeof candidate.collection === 'string' &&
|
|
219
|
-
candidate.collection.length > 0 &&
|
|
220
|
-
typeof candidate.id === 'string' &&
|
|
221
|
-
candidate.id.length > 0
|
|
222
|
-
? { collection: candidate.collection, id: candidate.id }
|
|
223
|
-
: undefined;
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Compute the webhook tag set for one lifecycle event, or `null` when the
|
|
227
|
-
* event is not a successful publish-shaped command (queries, failures, and
|
|
228
|
-
* unrelated domains never fire webhooks).
|
|
229
|
-
*/
|
|
230
|
-
export function webhookTagsForEvent(event, options) {
|
|
231
|
-
if (event.status !== 'succeeded' || event.kind !== 'command')
|
|
232
|
-
return null;
|
|
233
|
-
const isDocument = PUBLISHED_STATE_OPERATIONS.has(event.operation);
|
|
234
|
-
const isAsset = ASSET_OPERATIONS.has(event.operation);
|
|
235
|
-
const isRouting = ROUTING_OPERATIONS.has(event.operation);
|
|
236
|
-
if (!isDocument && !isAsset && !isRouting)
|
|
237
|
-
return null;
|
|
238
|
-
const projectId = projectIdOfSummary(event.inputSummary);
|
|
239
|
-
if (projectId === undefined)
|
|
240
|
-
return null;
|
|
241
|
-
// EC-510: one answer for every redirect write — see `ROUTING_OPERATIONS` for
|
|
242
|
-
// why the targeted `route:` tag is unobtainable and the sweep is the honest one.
|
|
243
|
-
if (isRouting) {
|
|
244
|
-
return { projectId, event: event.operation, tags: [projectSweepWebhookTag(projectId)] };
|
|
245
|
-
}
|
|
246
|
-
if (isAsset)
|
|
247
|
-
return assetTagsForEvent(projectId, event);
|
|
248
|
-
const parsed = parseSummary(event.inputSummary);
|
|
249
|
-
const ref = documentRefOf(parsed?.['ref']) ?? documentRefOf(parsed?.['document']);
|
|
250
|
-
if (!ref) {
|
|
251
|
-
// Truncated summary (large upsert payload): honest coarse fallback.
|
|
252
|
-
return { projectId, event: event.operation, tags: [projectSweepWebhookTag(projectId)] };
|
|
253
|
-
}
|
|
254
|
-
const tags = new Set([docWebhookTag(projectId, ref.collection, ref.id)]);
|
|
255
|
-
for (const locale of options.locales) {
|
|
256
|
-
tags.add(collectionWebhookTag(projectId, ref.collection, locale));
|
|
257
|
-
}
|
|
258
|
-
// EC-207: ROUTES are config-owned (a code deploy, which rebuilds the host), so
|
|
259
|
-
// a document event never needs a `route:` tag. Redirects are NOT — EC-303 made
|
|
260
|
-
// them editorial data and EC-510 gives their own operations the project sweep
|
|
261
|
-
// (see `ROUTING_OPERATIONS`); a *document* write still cannot change them.
|
|
262
|
-
return { projectId, event: event.operation, tags: [...tags].sort() };
|
|
263
|
-
}
|
|
264
|
-
/** A non-empty string, else `undefined` (the asset id out of a summary). */
|
|
265
|
-
function stringOf(value) {
|
|
266
|
-
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* Tags for an asset mutation (EC-227). The edit cases — `assets.remove` (carries
|
|
270
|
-
* `assetId`) and `assets.upsert` of an existing asset (carries `asset.id`) —
|
|
271
|
-
* emit exactly `asset:<id>`, dropping the pages that baked that asset. An
|
|
272
|
-
* `assets.upsert` with **no** id is a CREATE: the id is minted server-side and a
|
|
273
|
-
* brand-new asset cannot be baked into any existing page, so it invalidates
|
|
274
|
-
* nothing (`null`, no webhook). A `remove` missing its id means a truncated
|
|
275
|
-
* summary — sweep the project (coarse, always correct — the document fallback).
|
|
276
|
-
*/
|
|
277
|
-
function assetTagsForEvent(projectId, event) {
|
|
278
|
-
const parsed = parseSummary(event.inputSummary);
|
|
279
|
-
const assetId = event.operation === 'assets.remove'
|
|
280
|
-
? stringOf(parsed?.['assetId'])
|
|
281
|
-
: stringOf(parsed?.['asset']?.id);
|
|
282
|
-
if (assetId) {
|
|
283
|
-
return { projectId, event: event.operation, tags: [assetWebhookTag(projectId, assetId)] };
|
|
284
|
-
}
|
|
285
|
-
return event.operation === 'assets.remove'
|
|
286
|
-
? { projectId, event: event.operation, tags: [projectSweepWebhookTag(projectId)] }
|
|
287
|
-
: null;
|
|
288
|
-
}
|
|
289
|
-
//# sourceMappingURL=tags.js.map
|