@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.
Files changed (246) hide show
  1. package/convex/asset-revalidate.test.ts +49 -43
  2. package/convex/cms.ts +3 -4
  3. package/convex/delivery.ts +8 -11
  4. package/convex/document-revalidate.test.ts +188 -0
  5. package/convex/redirect-revalidate.test.ts +50 -37
  6. package/convex/revalidate.ts +18 -30
  7. package/convex/triggers.ts +47 -38
  8. package/convex/webhooks.ts +77 -62
  9. package/dist/components/account/self-account-view-source.js +2 -1
  10. package/dist/components/account/self-account-view-source.js.map +1 -1
  11. package/dist/components/ai/ai-chat-panel.js +1 -1
  12. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  13. package/dist/components/app-views/default-modules.js +3 -3
  14. package/dist/components/app-views/default-modules.js.map +1 -1
  15. package/dist/components/assets/asset-grid.js +1 -1
  16. package/dist/components/assets/asset-grid.js.map +1 -1
  17. package/dist/components/assets/asset-selection.d.ts +8 -9
  18. package/dist/components/assets/asset-selection.js +26 -16
  19. package/dist/components/assets/asset-selection.js.map +1 -1
  20. package/dist/components/assets/assets-collection-view-source.js +3 -2
  21. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  22. package/dist/components/builder/blocks-model.d.ts +7 -5
  23. package/dist/components/builder/blocks-model.js +11 -9
  24. package/dist/components/builder/blocks-model.js.map +1 -1
  25. package/dist/components/builder/blocks-panel.js +1 -1
  26. package/dist/components/builder/blocks-panel.js.map +1 -1
  27. package/dist/components/builder/builder-store.d.ts +11 -3
  28. package/dist/components/builder/builder-store.js +12 -5
  29. package/dist/components/builder/builder-store.js.map +1 -1
  30. package/dist/components/builder/canvas-dnd.d.ts +7 -1
  31. package/dist/components/builder/canvas-dnd.js +6 -3
  32. package/dist/components/builder/canvas-dnd.js.map +1 -1
  33. package/dist/components/builder/canvas-render-context.d.ts +13 -1
  34. package/dist/components/builder/canvas-render-context.js +28 -18
  35. package/dist/components/builder/canvas-render-context.js.map +1 -1
  36. package/dist/components/builder/canvas.js +1 -1
  37. package/dist/components/builder/canvas.js.map +1 -1
  38. package/dist/components/builder/clipboard-actions.d.ts +0 -6
  39. package/dist/components/builder/clipboard-actions.js +9 -4
  40. package/dist/components/builder/clipboard-actions.js.map +1 -1
  41. package/dist/components/builder/composition-field-editor.d.ts +22 -0
  42. package/dist/components/builder/composition-field-editor.js +47 -8
  43. package/dist/components/builder/composition-field-editor.js.map +1 -1
  44. package/dist/components/builder/inline-edit.d.ts +7 -4
  45. package/dist/components/builder/inline-edit.js +11 -8
  46. package/dist/components/builder/inline-edit.js.map +1 -1
  47. package/dist/components/builder/insert-context.js +9 -5
  48. package/dist/components/builder/insert-context.js.map +1 -1
  49. package/dist/components/builder/layout-inspector.d.ts +2 -2
  50. package/dist/components/builder/layout-inspector.js +8 -6
  51. package/dist/components/builder/layout-inspector.js.map +1 -1
  52. package/dist/components/builder/node-factory.d.ts +31 -14
  53. package/dist/components/builder/node-factory.js +73 -93
  54. package/dist/components/builder/node-factory.js.map +1 -1
  55. package/dist/components/builder/page-tree.d.ts +3 -1
  56. package/dist/components/builder/page-tree.js +6 -4
  57. package/dist/components/builder/page-tree.js.map +1 -1
  58. package/dist/components/builder/property-inspector.js +2 -1
  59. package/dist/components/builder/property-inspector.js.map +1 -1
  60. package/dist/components/builder/reuse-context.js +2 -2
  61. package/dist/components/builder/reuse-context.js.map +1 -1
  62. package/dist/components/builder/reuse.d.ts +7 -3
  63. package/dist/components/builder/reuse.js +19 -11
  64. package/dist/components/builder/reuse.js.map +1 -1
  65. package/dist/components/builder/selection-overlay.d.ts +2 -2
  66. package/dist/components/builder/selection-overlay.js +3 -3
  67. package/dist/components/builder/selection-overlay.js.map +1 -1
  68. package/dist/components/builder/spacing-handles.d.ts +2 -2
  69. package/dist/components/builder/spacing-handles.js +3 -3
  70. package/dist/components/builder/spacing-handles.js.map +1 -1
  71. package/dist/components/cms/asset-field.d.ts +1 -1
  72. package/dist/components/cms/asset-field.js +15 -13
  73. package/dist/components/cms/asset-field.js.map +1 -1
  74. package/dist/components/cms/asset-picker-core.js +3 -4
  75. package/dist/components/cms/asset-picker-core.js.map +1 -1
  76. package/dist/components/cms/cell-edit.d.ts +0 -6
  77. package/dist/components/cms/cell-edit.js +5 -23
  78. package/dist/components/cms/cell-edit.js.map +1 -1
  79. package/dist/components/cms/cms-store.d.ts +3 -2
  80. package/dist/components/cms/cms-store.js +23 -24
  81. package/dist/components/cms/cms-store.js.map +1 -1
  82. package/dist/components/cms/collection-table.js +14 -4
  83. package/dist/components/cms/collection-table.js.map +1 -1
  84. package/dist/components/cms/detail-editor.js +28 -24
  85. package/dist/components/cms/detail-editor.js.map +1 -1
  86. package/dist/components/cms/discard-draft.d.ts +1 -2
  87. package/dist/components/cms/discard-draft.js +1 -1
  88. package/dist/components/cms/discard-draft.js.map +1 -1
  89. package/dist/components/cms/duplicate-document.d.ts +3 -2
  90. package/dist/components/cms/duplicate-document.js +5 -5
  91. package/dist/components/cms/duplicate-document.js.map +1 -1
  92. package/dist/components/cms/editable-cell.js +11 -7
  93. package/dist/components/cms/editable-cell.js.map +1 -1
  94. package/dist/components/cms/field-control-lib.d.ts +10 -0
  95. package/dist/components/cms/field-control-lib.js +20 -0
  96. package/dist/components/cms/field-control-lib.js.map +1 -1
  97. package/dist/components/cms/field-control.js +10 -9
  98. package/dist/components/cms/field-control.js.map +1 -1
  99. package/dist/components/cms/object-array-field.js +3 -5
  100. package/dist/components/cms/object-array-field.js.map +1 -1
  101. package/dist/components/cms/prop-fields.d.ts +16 -3
  102. package/dist/components/cms/prop-fields.js +57 -12
  103. package/dist/components/cms/prop-fields.js.map +1 -1
  104. package/dist/components/cms/publish-clean.d.ts +13 -6
  105. package/dist/components/cms/publish-clean.js +14 -6
  106. package/dist/components/cms/publish-clean.js.map +1 -1
  107. package/dist/components/cms/rich-text/embed-props.d.ts +6 -1
  108. package/dist/components/cms/rich-text/embed-props.js +7 -12
  109. package/dist/components/cms/rich-text/embed-props.js.map +1 -1
  110. package/dist/components/cms/rich-text/insert.d.ts +13 -6
  111. package/dist/components/cms/rich-text/insert.js +16 -17
  112. package/dist/components/cms/rich-text/insert.js.map +1 -1
  113. package/dist/components/cms/use-cell-edit.js +7 -12
  114. package/dist/components/cms/use-cell-edit.js.map +1 -1
  115. package/dist/components/cms/use-discard-controller.d.ts +1 -2
  116. package/dist/components/cms/use-discard-controller.js +1 -2
  117. package/dist/components/cms/use-discard-controller.js.map +1 -1
  118. package/dist/components/cms/use-publish-controller.js +3 -2
  119. package/dist/components/cms/use-publish-controller.js.map +1 -1
  120. package/dist/components/cms/versions-pane.js +2 -2
  121. package/dist/components/cms/versions-pane.js.map +1 -1
  122. package/dist/components/components/component-detail.d.ts +1 -1
  123. package/dist/components/components/component-detail.js +2 -4
  124. package/dist/components/components/component-detail.js.map +1 -1
  125. package/dist/components/components/component-preview.js +9 -4
  126. package/dist/components/components/component-preview.js.map +1 -1
  127. package/dist/components/components/components-index.d.ts +2 -2
  128. package/dist/components/components/components-index.js +3 -6
  129. package/dist/components/components/components-index.js.map +1 -1
  130. package/dist/components/components/field-inspector.js +5 -2
  131. package/dist/components/components/field-inspector.js.map +1 -1
  132. package/dist/components/components/registry-browser.d.ts +6 -5
  133. package/dist/components/components/registry-browser.js +11 -37
  134. package/dist/components/components/registry-browser.js.map +1 -1
  135. package/dist/components/components/taxonomy.d.ts +14 -21
  136. package/dist/components/components/taxonomy.js +5 -29
  137. package/dist/components/components/taxonomy.js.map +1 -1
  138. package/dist/components/components/transfer-dialog.js +1 -0
  139. package/dist/components/components/transfer-dialog.js.map +1 -1
  140. package/dist/components/components/transfer.d.ts +20 -12
  141. package/dist/components/components/transfer.js +27 -21
  142. package/dist/components/components/transfer.js.map +1 -1
  143. package/dist/components/delivery-health-banner.d.ts +3 -2
  144. package/dist/components/delivery-health-banner.js +3 -2
  145. package/dist/components/delivery-health-banner.js.map +1 -1
  146. package/dist/components/redirects/redirects-collection-view-source.js +3 -2
  147. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  148. package/dist/components/users/users-collection-view-source.js +2 -1
  149. package/dist/components/users/users-collection-view-source.js.map +1 -1
  150. package/dist/lib/ai/apply-content.js +32 -28
  151. package/dist/lib/ai/apply-content.js.map +1 -1
  152. package/dist/lib/ai/constraints.d.ts +16 -14
  153. package/dist/lib/ai/constraints.js +64 -48
  154. package/dist/lib/ai/constraints.js.map +1 -1
  155. package/dist/lib/ai/translate-document.js +11 -7
  156. package/dist/lib/ai/translate-document.js.map +1 -1
  157. package/dist/lib/ai/types.d.ts +9 -5
  158. package/dist/lib/ai/types.js.map +1 -1
  159. package/dist/lib/ai/update-fields.js +15 -32
  160. package/dist/lib/ai/update-fields.js.map +1 -1
  161. package/dist/lib/ai/vite.d.ts +22 -2
  162. package/dist/lib/ai/vite.js +12 -3
  163. package/dist/lib/ai/vite.js.map +1 -1
  164. package/dist/lib/assets/asset-placements.d.ts +3 -10
  165. package/dist/lib/assets/asset-placements.js +17 -8
  166. package/dist/lib/assets/asset-placements.js.map +1 -1
  167. package/dist/lib/clipboard/paste-target.js +3 -3
  168. package/dist/lib/clipboard/paste-target.js.map +1 -1
  169. package/dist/lib/cms/published-documents.d.ts +14 -3
  170. package/dist/lib/cms/published-documents.js +12 -3
  171. package/dist/lib/cms/published-documents.js.map +1 -1
  172. package/dist/lib/cms-views/view-model.d.ts +4 -4
  173. package/dist/lib/cms-views/view-model.js +26 -20
  174. package/dist/lib/cms-views/view-model.js.map +1 -1
  175. package/dist/lib/data/collection-content.d.ts +10 -7
  176. package/dist/lib/data/collection-content.js +70 -29
  177. package/dist/lib/data/collection-content.js.map +1 -1
  178. package/dist/lib/delivery/instant-publish.d.ts +0 -3
  179. package/dist/lib/delivery/instant-publish.js +0 -1
  180. package/dist/lib/delivery/instant-publish.js.map +1 -1
  181. package/dist/lib/dnd/drop-rules.d.ts +22 -7
  182. package/dist/lib/dnd/drop-rules.js +21 -14
  183. package/dist/lib/dnd/drop-rules.js.map +1 -1
  184. package/dist/lib/engine-version.d.ts +1 -1
  185. package/dist/lib/engine-version.js +1 -1
  186. package/dist/lib/engine-version.js.map +1 -1
  187. package/dist/lib/i18n/messages.de.js +25 -9
  188. package/dist/lib/i18n/messages.de.js.map +1 -1
  189. package/dist/lib/i18n/messages.en.d.ts +21 -7
  190. package/dist/lib/i18n/messages.en.js +28 -9
  191. package/dist/lib/i18n/messages.en.js.map +1 -1
  192. package/dist/lib/locale/fallback.d.ts +5 -19
  193. package/dist/lib/locale/fallback.js +18 -39
  194. package/dist/lib/locale/fallback.js.map +1 -1
  195. package/dist/lib/locale/locale-store.js +2 -1
  196. package/dist/lib/locale/locale-store.js.map +1 -1
  197. package/dist/lib/operations/cms-persistence.js +7 -4
  198. package/dist/lib/operations/cms-persistence.js.map +1 -1
  199. package/dist/lib/operations/studio-operations.js +4 -6
  200. package/dist/lib/operations/studio-operations.js.map +1 -1
  201. package/dist/lib/persistence/backend-mode.d.ts +5 -25
  202. package/dist/lib/persistence/backend-mode.js +13 -30
  203. package/dist/lib/persistence/backend-mode.js.map +1 -1
  204. package/dist/lib/persona/labels.d.ts +4 -9
  205. package/dist/lib/persona/labels.js +5 -12
  206. package/dist/lib/persona/labels.js.map +1 -1
  207. package/dist/lib/references/reference-model.d.ts +17 -5
  208. package/dist/lib/references/reference-model.js +20 -28
  209. package/dist/lib/references/reference-model.js.map +1 -1
  210. package/dist/lib/references/references-store.js +7 -0
  211. package/dist/lib/references/references-store.js.map +1 -1
  212. package/dist/lib/registry/__fixtures__/demo-components.d.ts +32 -0
  213. package/dist/lib/registry/{project-components.js → __fixtures__/demo-components.js} +18 -5
  214. package/dist/lib/registry/__fixtures__/demo-components.js.map +1 -0
  215. package/dist/lib/registry/index.d.ts +9 -13
  216. package/dist/lib/registry/index.js +34 -61
  217. package/dist/lib/registry/index.js.map +1 -1
  218. package/dist/lib/registry/preview.d.ts +22 -7
  219. package/dist/lib/registry/preview.js +26 -89
  220. package/dist/lib/registry/preview.js.map +1 -1
  221. package/dist/lib/workspace/index.d.ts +0 -7
  222. package/dist/lib/workspace/index.js +0 -9
  223. package/dist/lib/workspace/index.js.map +1 -1
  224. package/dist/previews/enumerate.d.ts +23 -27
  225. package/dist/previews/enumerate.js +27 -48
  226. package/dist/previews/enumerate.js.map +1 -1
  227. package/dist/previews/harness.js +1 -1
  228. package/dist/previews/harness.js.map +1 -1
  229. package/package.json +4 -7
  230. package/dist/lib/registry/project-components.d.ts +0 -16
  231. package/dist/lib/registry/project-components.js.map +0 -1
  232. package/dist/lib/webhooks/config.d.ts +0 -15
  233. package/dist/lib/webhooks/config.js +0 -65
  234. package/dist/lib/webhooks/config.js.map +0 -1
  235. package/dist/lib/webhooks/dispatcher.d.ts +0 -70
  236. package/dist/lib/webhooks/dispatcher.js +0 -61
  237. package/dist/lib/webhooks/dispatcher.js.map +0 -1
  238. package/dist/lib/webhooks/index.d.ts +0 -11
  239. package/dist/lib/webhooks/index.js +0 -10
  240. package/dist/lib/webhooks/index.js.map +0 -1
  241. package/dist/lib/webhooks/signing.d.ts +0 -15
  242. package/dist/lib/webhooks/signing.js +0 -24
  243. package/dist/lib/webhooks/signing.js.map +0 -1
  244. package/dist/lib/webhooks/tags.d.ts +0 -25
  245. package/dist/lib/webhooks/tags.js +0 -289
  246. 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