@elytracms/studio 0.0.93 → 0.0.95

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 (541) hide show
  1. package/convex/analytics-cost.test.ts +817 -0
  2. package/convex/asset-alt-migration.test.ts +2 -2
  3. package/convex/asset-meta-patch.test.ts +218 -0
  4. package/convex/asset-replace-delete-race.test.ts +127 -0
  5. package/convex/assets.ts +27 -2
  6. package/convex/auth.ts +5 -2
  7. package/convex/authority.ts +35 -11
  8. package/convex/contract-parity.test.ts +7 -0
  9. package/convex/delivery-redirects.test.ts +167 -0
  10. package/convex/delivery.ts +23 -10
  11. package/convex/guard.ts +9 -7
  12. package/convex/http-routes.test.ts +18 -8
  13. package/convex/http.ts +23 -17
  14. package/convex/ingest.test.ts +326 -0
  15. package/convex/ingest.ts +281 -0
  16. package/convex/invites.ts +13 -5
  17. package/convex/members.ts +10 -4
  18. package/convex/rail-write-parity.test.ts +6 -6
  19. package/convex/record-functions.ts +325 -0
  20. package/convex/records.test.ts +156 -0
  21. package/convex/redirect-migration.test.ts +205 -0
  22. package/convex/redirect-revalidate.test.ts +5 -1
  23. package/convex/redirects.ts +120 -79
  24. package/convex/role-policy.test.ts +315 -0
  25. package/convex/roles.ts +83 -0
  26. package/convex/schema.ts +36 -0
  27. package/convex/triggers.ts +21 -1
  28. package/dist/addon.d.ts +71 -0
  29. package/dist/addon.js +67 -0
  30. package/dist/addon.js.map +1 -0
  31. package/dist/components/account/account-settings.js +2 -2
  32. package/dist/components/account/account-settings.js.map +1 -1
  33. package/dist/components/account/credential-panel.js +2 -2
  34. package/dist/components/account/credential-panel.js.map +1 -1
  35. package/dist/components/account/self-account-schema.d.ts +6 -15
  36. package/dist/components/account/self-account-schema.js +52 -34
  37. package/dist/components/account/self-account-schema.js.map +1 -1
  38. package/dist/components/account/self-account-view-source.d.ts +1 -1
  39. package/dist/components/account/self-account-view-source.js +6 -4
  40. package/dist/components/account/self-account-view-source.js.map +1 -1
  41. package/dist/components/ai/ai-chat-panel-lazy.js +1 -1
  42. package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
  43. package/dist/components/ai/ai-chat-panel.js +6 -6
  44. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  45. package/dist/components/ai/ai-cms-panel.js +2 -2
  46. package/dist/components/ai/ai-cms-panel.js.map +1 -1
  47. package/dist/components/ai/ai-panel-toggle.js +4 -4
  48. package/dist/components/ai/ai-panel-toggle.js.map +1 -1
  49. package/dist/components/ai/chat-ui.js +2 -2
  50. package/dist/components/ai/chat-ui.js.map +1 -1
  51. package/dist/components/ai/proposal-card.d.ts +2 -2
  52. package/dist/components/ai/proposal-card.js +2 -2
  53. package/dist/components/ai/proposal-card.js.map +1 -1
  54. package/dist/components/ai/proposal-issues.js +2 -2
  55. package/dist/components/ai/proposal-issues.js.map +1 -1
  56. package/dist/components/ai/translate-document-action.js +2 -2
  57. package/dist/components/ai/translate-document-action.js.map +1 -1
  58. package/dist/components/ai/translate-document-dialog.js +6 -6
  59. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  60. package/dist/components/ai/translate-skipped.d.ts +1 -1
  61. package/dist/components/ai/translate-skipped.js +2 -2
  62. package/dist/components/ai/translate-skipped.js.map +1 -1
  63. package/dist/components/app-views/default-modules.d.ts +50 -2
  64. package/dist/components/app-views/default-modules.js +65 -20
  65. package/dist/components/app-views/default-modules.js.map +1 -1
  66. package/dist/components/app-views/module.d.ts +53 -3
  67. package/dist/components/app-views/module.js +14 -7
  68. package/dist/components/app-views/module.js.map +1 -1
  69. package/dist/components/app-views/sidebar.js +1 -1
  70. package/dist/components/app-views/sidebar.js.map +1 -1
  71. package/dist/components/app-views/studio-config.d.ts +11 -2
  72. package/dist/components/app-views/studio-config.js.map +1 -1
  73. package/dist/components/assets/asset-detail.js +28 -13
  74. package/dist/components/assets/asset-detail.js.map +1 -1
  75. package/dist/components/assets/asset-grid.js +2 -2
  76. package/dist/components/assets/asset-grid.js.map +1 -1
  77. package/dist/components/assets/asset-locale.d.ts +9 -0
  78. package/dist/components/assets/asset-locale.js +16 -0
  79. package/dist/components/assets/asset-locale.js.map +1 -0
  80. package/dist/components/assets/asset-meta-label.d.ts +12 -0
  81. package/dist/components/assets/asset-meta-label.js +15 -0
  82. package/dist/components/assets/asset-meta-label.js.map +1 -0
  83. package/dist/components/assets/asset-metadata.d.ts +69 -15
  84. package/dist/components/assets/asset-metadata.js +99 -11
  85. package/dist/components/assets/asset-metadata.js.map +1 -1
  86. package/dist/components/assets/asset-search.js +8 -2
  87. package/dist/components/assets/asset-search.js.map +1 -1
  88. package/dist/components/assets/asset-thumb.js +9 -5
  89. package/dist/components/assets/asset-thumb.js.map +1 -1
  90. package/dist/components/assets/assets-collection-view-source.d.ts +25 -11
  91. package/dist/components/assets/assets-collection-view-source.js +78 -26
  92. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  93. package/dist/components/assets/assets-module.js +1 -1
  94. package/dist/components/assets/assets-module.js.map +1 -1
  95. package/dist/components/assets/assets-store.d.ts +2 -2
  96. package/dist/components/assets/assets-store.js +22 -9
  97. package/dist/components/assets/assets-store.js.map +1 -1
  98. package/dist/components/assets/assets-view-preference.js +1 -1
  99. package/dist/components/assets/assets-view-preference.js.map +1 -1
  100. package/dist/components/assets/replace-file.js +1 -1
  101. package/dist/components/assets/replace-file.js.map +1 -1
  102. package/dist/components/auth-context.d.ts +1 -1
  103. package/dist/components/auth-context.js +6 -6
  104. package/dist/components/auth-context.js.map +1 -1
  105. package/dist/components/builder/blocks-model.d.ts +1 -1
  106. package/dist/components/builder/blocks-model.js +3 -3
  107. package/dist/components/builder/blocks-model.js.map +1 -1
  108. package/dist/components/builder/blocks-panel.d.ts +1 -1
  109. package/dist/components/builder/blocks-panel.js +4 -4
  110. package/dist/components/builder/blocks-panel.js.map +1 -1
  111. package/dist/components/builder/builder-store.d.ts +21 -0
  112. package/dist/components/builder/builder-store.js +24 -2
  113. package/dist/components/builder/builder-store.js.map +1 -1
  114. package/dist/components/builder/canvas-breakpoints.js +1 -1
  115. package/dist/components/builder/canvas-breakpoints.js.map +1 -1
  116. package/dist/components/builder/canvas-commit-boundary.d.ts +22 -0
  117. package/dist/components/builder/canvas-commit-boundary.js +27 -0
  118. package/dist/components/builder/canvas-commit-boundary.js.map +1 -0
  119. package/dist/components/builder/canvas-dnd.d.ts +2 -2
  120. package/dist/components/builder/canvas-dnd.js +1 -1
  121. package/dist/components/builder/canvas-dnd.js.map +1 -1
  122. package/dist/components/builder/canvas-inline-editing.d.ts +1 -1
  123. package/dist/components/builder/canvas-inline-editing.js.map +1 -1
  124. package/dist/components/builder/canvas-overlay-geometry.d.ts +1 -1
  125. package/dist/components/builder/canvas-overlay-geometry.js.map +1 -1
  126. package/dist/components/builder/canvas-rebase.d.ts +77 -0
  127. package/dist/components/builder/canvas-rebase.js +169 -0
  128. package/dist/components/builder/canvas-rebase.js.map +1 -0
  129. package/dist/components/builder/canvas-render-context.d.ts +7 -4
  130. package/dist/components/builder/canvas-render-context.js +21 -13
  131. package/dist/components/builder/canvas-render-context.js.map +1 -1
  132. package/dist/components/builder/canvas-theme-notice.js +2 -2
  133. package/dist/components/builder/canvas-theme-notice.js.map +1 -1
  134. package/dist/components/builder/canvas.js +3 -3
  135. package/dist/components/builder/canvas.js.map +1 -1
  136. package/dist/components/builder/clipboard-actions.d.ts +2 -2
  137. package/dist/components/builder/clipboard-actions.js +4 -4
  138. package/dist/components/builder/clipboard-actions.js.map +1 -1
  139. package/dist/components/builder/composition-field-editor-lazy.js +1 -1
  140. package/dist/components/builder/composition-field-editor-lazy.js.map +1 -1
  141. package/dist/components/builder/composition-field-editor.d.ts +21 -3
  142. package/dist/components/builder/composition-field-editor.js +48 -30
  143. package/dist/components/builder/composition-field-editor.js.map +1 -1
  144. package/dist/components/builder/drag-session.d.ts +1 -1
  145. package/dist/components/builder/drag-session.js.map +1 -1
  146. package/dist/components/builder/fixture-thumbnail.js +1 -1
  147. package/dist/components/builder/fixture-thumbnail.js.map +1 -1
  148. package/dist/components/builder/insert-context.d.ts +1 -1
  149. package/dist/components/builder/insert-context.js +3 -3
  150. package/dist/components/builder/insert-context.js.map +1 -1
  151. package/dist/components/builder/layout-inspector.js +1 -1
  152. package/dist/components/builder/layout-inspector.js.map +1 -1
  153. package/dist/components/builder/node-factory.js +15 -3
  154. package/dist/components/builder/node-factory.js.map +1 -1
  155. package/dist/components/builder/node-title.js +1 -1
  156. package/dist/components/builder/node-title.js.map +1 -1
  157. package/dist/components/builder/page-tree.d.ts +1 -1
  158. package/dist/components/builder/page-tree.js +5 -5
  159. package/dist/components/builder/page-tree.js.map +1 -1
  160. package/dist/components/builder/placement-refusal.js +3 -3
  161. package/dist/components/builder/placement-refusal.js.map +1 -1
  162. package/dist/components/builder/property-inspector.js +7 -5
  163. package/dist/components/builder/property-inspector.js.map +1 -1
  164. package/dist/components/builder/reuse-context.js +4 -4
  165. package/dist/components/builder/reuse-context.js.map +1 -1
  166. package/dist/components/builder/reuse.js +1 -1
  167. package/dist/components/builder/reuse.js.map +1 -1
  168. package/dist/components/builder/rich-text-toolbar.js +1 -1
  169. package/dist/components/builder/rich-text-toolbar.js.map +1 -1
  170. package/dist/components/builder/run-inspector.js +11 -7
  171. package/dist/components/builder/run-inspector.js.map +1 -1
  172. package/dist/components/builder/selection-overlay.js +3 -3
  173. package/dist/components/builder/selection-overlay.js.map +1 -1
  174. package/dist/components/builder/spacing-handles.d.ts +2 -2
  175. package/dist/components/builder/spacing-handles.js +3 -3
  176. package/dist/components/builder/spacing-handles.js.map +1 -1
  177. package/dist/components/cms/asset-field.d.ts +5 -2
  178. package/dist/components/cms/asset-field.js +36 -19
  179. package/dist/components/cms/asset-field.js.map +1 -1
  180. package/dist/components/cms/asset-picker.js +5 -3
  181. package/dist/components/cms/asset-picker.js.map +1 -1
  182. package/dist/components/cms/bulk-actions.d.ts +1 -1
  183. package/dist/components/cms/bulk-actions.js +2 -2
  184. package/dist/components/cms/bulk-actions.js.map +1 -1
  185. package/dist/components/cms/bulk-bar.d.ts +2 -2
  186. package/dist/components/cms/bulk-bar.js +2 -2
  187. package/dist/components/cms/bulk-bar.js.map +1 -1
  188. package/dist/components/cms/canvas-commit.d.ts +56 -0
  189. package/dist/components/cms/canvas-commit.js +35 -0
  190. package/dist/components/cms/canvas-commit.js.map +1 -0
  191. package/dist/components/cms/cell-edit.d.ts +1 -1
  192. package/dist/components/cms/cell-edit.js +1 -1
  193. package/dist/components/cms/cell-edit.js.map +1 -1
  194. package/dist/components/cms/cell.d.ts +1 -1
  195. package/dist/components/cms/cell.js +3 -3
  196. package/dist/components/cms/cell.js.map +1 -1
  197. package/dist/components/cms/cms-index.js +2 -2
  198. package/dist/components/cms/cms-index.js.map +1 -1
  199. package/dist/components/cms/cms-labels.js +2 -2
  200. package/dist/components/cms/cms-labels.js.map +1 -1
  201. package/dist/components/cms/cms-store.d.ts +2 -2
  202. package/dist/components/cms/cms-store.js +10 -10
  203. package/dist/components/cms/cms-store.js.map +1 -1
  204. package/dist/components/cms/collection-table.js +30 -14
  205. package/dist/components/cms/collection-table.js.map +1 -1
  206. package/dist/components/cms/commit-policy.d.ts +6 -2
  207. package/dist/components/cms/commit-policy.js +1 -1
  208. package/dist/components/cms/commit-policy.js.map +1 -1
  209. package/dist/components/cms/detail-editor.d.ts +5 -2
  210. package/dist/components/cms/detail-editor.js +31 -15
  211. package/dist/components/cms/detail-editor.js.map +1 -1
  212. package/dist/components/cms/details-pane.js +1 -1
  213. package/dist/components/cms/details-pane.js.map +1 -1
  214. package/dist/components/cms/doc-state.js +1 -1
  215. package/dist/components/cms/doc-state.js.map +1 -1
  216. package/dist/components/cms/document-diff-view.js +5 -5
  217. package/dist/components/cms/document-diff-view.js.map +1 -1
  218. package/dist/components/cms/document-editor-context.js +1 -1
  219. package/dist/components/cms/document-editor-context.js.map +1 -1
  220. package/dist/components/cms/document-staging.js +1 -1
  221. package/dist/components/cms/document-staging.js.map +1 -1
  222. package/dist/components/cms/document-url-path.js +2 -2
  223. package/dist/components/cms/document-url-path.js.map +1 -1
  224. package/dist/components/cms/duplicate-document.d.ts +1 -1
  225. package/dist/components/cms/duplicate-document.js.map +1 -1
  226. package/dist/components/cms/editable-cell.js +3 -3
  227. package/dist/components/cms/editable-cell.js.map +1 -1
  228. package/dist/components/cms/field-collaboration.d.ts +1 -1
  229. package/dist/components/cms/field-collaboration.js +1 -1
  230. package/dist/components/cms/field-collaboration.js.map +1 -1
  231. package/dist/components/cms/field-connection.d.ts +1 -1
  232. package/dist/components/cms/field-connection.js +2 -2
  233. package/dist/components/cms/field-connection.js.map +1 -1
  234. package/dist/components/cms/field-control-lib.js +1 -1
  235. package/dist/components/cms/field-control-lib.js.map +1 -1
  236. package/dist/components/cms/field-control.js +4 -4
  237. package/dist/components/cms/field-control.js.map +1 -1
  238. package/dist/components/cms/field-renderer.d.ts +1 -1
  239. package/dist/components/cms/field-renderer.js +2 -2
  240. package/dist/components/cms/field-renderer.js.map +1 -1
  241. package/dist/components/cms/filter-builder.d.ts +1 -1
  242. package/dist/components/cms/filter-builder.js +3 -3
  243. package/dist/components/cms/filter-builder.js.map +1 -1
  244. package/dist/components/cms/gate-validation.js +1 -1
  245. package/dist/components/cms/gate-validation.js.map +1 -1
  246. package/dist/components/cms/import-export.js +2 -2
  247. package/dist/components/cms/import-export.js.map +1 -1
  248. package/dist/components/cms/issue-copy.js +1 -1
  249. package/dist/components/cms/issue-copy.js.map +1 -1
  250. package/dist/components/cms/issues-pane.js +3 -3
  251. package/dist/components/cms/issues-pane.js.map +1 -1
  252. package/dist/components/cms/object-array-field.js +2 -2
  253. package/dist/components/cms/object-array-field.js.map +1 -1
  254. package/dist/components/cms/prop-fields.js +2 -2
  255. package/dist/components/cms/prop-fields.js.map +1 -1
  256. package/dist/components/cms/reference-confirm.d.ts +1 -1
  257. package/dist/components/cms/reference-confirm.js +2 -2
  258. package/dist/components/cms/reference-confirm.js.map +1 -1
  259. package/dist/components/cms/relation-picker.js +2 -2
  260. package/dist/components/cms/relation-picker.js.map +1 -1
  261. package/dist/components/cms/rich-text/block-style.js +1 -1
  262. package/dist/components/cms/rich-text/block-style.js.map +1 -1
  263. package/dist/components/cms/rich-text/component-embed.js +4 -4
  264. package/dist/components/cms/rich-text/component-embed.js.map +1 -1
  265. package/dist/components/cms/rich-text/editor.js +3 -3
  266. package/dist/components/cms/rich-text/editor.js.map +1 -1
  267. package/dist/components/cms/rich-text/embed-picker.js +3 -3
  268. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  269. package/dist/components/cms/rich-text/embed-props.js +3 -3
  270. package/dist/components/cms/rich-text/embed-props.js.map +1 -1
  271. package/dist/components/cms/rich-text/extensions.js +1 -1
  272. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  273. package/dist/components/cms/rich-text/font-size-control.d.ts +1 -1
  274. package/dist/components/cms/rich-text/font-size-control.js +2 -2
  275. package/dist/components/cms/rich-text/font-size-control.js.map +1 -1
  276. package/dist/components/cms/rich-text/lazy.js +1 -1
  277. package/dist/components/cms/rich-text/lazy.js.map +1 -1
  278. package/dist/components/cms/rich-text/link-control.js +2 -2
  279. package/dist/components/cms/rich-text/link-control.js.map +1 -1
  280. package/dist/components/cms/rich-text/slash-command.js +3 -3
  281. package/dist/components/cms/rich-text/slash-command.js.map +1 -1
  282. package/dist/components/cms/rich-text/toolbar.js +1 -1
  283. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  284. package/dist/components/cms/rich-text/unknown-node.js +2 -2
  285. package/dist/components/cms/rich-text/unknown-node.js.map +1 -1
  286. package/dist/components/cms/rich-text-preview.js +2 -2
  287. package/dist/components/cms/rich-text-preview.js.map +1 -1
  288. package/dist/components/cms/row-actions.d.ts +1 -1
  289. package/dist/components/cms/row-actions.js.map +1 -1
  290. package/dist/components/cms/scalar-value-editor.d.ts +1 -1
  291. package/dist/components/cms/scalar-value-editor.js +2 -2
  292. package/dist/components/cms/scalar-value-editor.js.map +1 -1
  293. package/dist/components/cms/use-discard-controller.d.ts +1 -1
  294. package/dist/components/cms/use-discard-controller.js +1 -1
  295. package/dist/components/cms/use-discard-controller.js.map +1 -1
  296. package/dist/components/cms/use-document-draft.d.ts +69 -5
  297. package/dist/components/cms/use-document-draft.js +118 -34
  298. package/dist/components/cms/use-document-draft.js.map +1 -1
  299. package/dist/components/cms/use-document-versions.js +1 -1
  300. package/dist/components/cms/use-document-versions.js.map +1 -1
  301. package/dist/components/cms/use-field-focus.d.ts +1 -1
  302. package/dist/components/cms/use-field-focus.js +2 -2
  303. package/dist/components/cms/use-field-focus.js.map +1 -1
  304. package/dist/components/cms/use-live-url.d.ts +7 -2
  305. package/dist/components/cms/use-live-url.js +15 -12
  306. package/dist/components/cms/use-live-url.js.map +1 -1
  307. package/dist/components/cms/use-publish-controller.d.ts +1 -1
  308. package/dist/components/cms/use-publish-controller.js +1 -1
  309. package/dist/components/cms/use-publish-controller.js.map +1 -1
  310. package/dist/components/cms/use-published-perspective.d.ts +1 -1
  311. package/dist/components/cms/use-published-perspective.js.map +1 -1
  312. package/dist/components/cms/use-restore-version.d.ts +1 -1
  313. package/dist/components/cms/use-restore-version.js +1 -1
  314. package/dist/components/cms/use-restore-version.js.map +1 -1
  315. package/dist/components/cms/version-compare-lazy.js +1 -1
  316. package/dist/components/cms/version-compare-lazy.js.map +1 -1
  317. package/dist/components/cms/version-compare.js +1 -1
  318. package/dist/components/cms/version-compare.js.map +1 -1
  319. package/dist/components/cms/versions-pane.js +3 -3
  320. package/dist/components/cms/versions-pane.js.map +1 -1
  321. package/dist/components/cms/view-bar.js +3 -3
  322. package/dist/components/cms/view-bar.js.map +1 -1
  323. package/dist/components/collection-view/cms-collection-view-source.d.ts +1 -1
  324. package/dist/components/collection-view/cms-collection-view-source.js +11 -7
  325. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  326. package/dist/components/collection-view/collection-view-context.d.ts +1 -1
  327. package/dist/components/collection-view/collection-view-context.js +1 -1
  328. package/dist/components/collection-view/collection-view-context.js.map +1 -1
  329. package/dist/components/collection-view/collection-view-surface.d.ts +13 -0
  330. package/dist/components/collection-view/collection-view-surface.js +18 -0
  331. package/dist/components/collection-view/collection-view-surface.js.map +1 -0
  332. package/dist/components/components/component-detail.js +10 -10
  333. package/dist/components/components/component-detail.js.map +1 -1
  334. package/dist/components/components/component-preview.d.ts +2 -2
  335. package/dist/components/components/component-preview.js +5 -5
  336. package/dist/components/components/component-preview.js.map +1 -1
  337. package/dist/components/components/component-status.d.ts +1 -1
  338. package/dist/components/components/component-status.js +1 -1
  339. package/dist/components/components/component-status.js.map +1 -1
  340. package/dist/components/components/components-index.js +2 -2
  341. package/dist/components/components/components-index.js.map +1 -1
  342. package/dist/components/components/field-inspector.js +1 -1
  343. package/dist/components/components/field-inspector.js.map +1 -1
  344. package/dist/components/components/registry-browser.js +3 -3
  345. package/dist/components/components/registry-browser.js.map +1 -1
  346. package/dist/components/components/taxonomy.d.ts +1 -1
  347. package/dist/components/components/taxonomy.js.map +1 -1
  348. package/dist/components/components/transfer-dialog.js +3 -3
  349. package/dist/components/components/transfer-dialog.js.map +1 -1
  350. package/dist/components/current-user-menu.js +1 -1
  351. package/dist/components/current-user-menu.js.map +1 -1
  352. package/dist/components/delivery-health-banner.js +2 -2
  353. package/dist/components/delivery-health-banner.js.map +1 -1
  354. package/dist/components/deployment-setup-gate.js +13 -4
  355. package/dist/components/deployment-setup-gate.js.map +1 -1
  356. package/dist/components/dev-login-badge.js +2 -2
  357. package/dist/components/dev-login-badge.js.map +1 -1
  358. package/dist/components/guards.js +1 -1
  359. package/dist/components/guards.js.map +1 -1
  360. package/dist/components/inspector/document-panes.d.ts +54 -0
  361. package/dist/components/inspector/document-panes.js +41 -0
  362. package/dist/components/inspector/document-panes.js.map +1 -0
  363. package/dist/components/inspector/inspector-sidebar.js +1 -1
  364. package/dist/components/inspector/inspector-sidebar.js.map +1 -1
  365. package/dist/components/nav-issues-registry.d.ts +53 -0
  366. package/dist/components/nav-issues-registry.js +69 -0
  367. package/dist/components/nav-issues-registry.js.map +1 -0
  368. package/dist/components/nav-issues.js +4 -4
  369. package/dist/components/nav-issues.js.map +1 -1
  370. package/dist/components/operation-error.d.ts +1 -1
  371. package/dist/components/operation-error.js +4 -4
  372. package/dist/components/operation-error.js.map +1 -1
  373. package/dist/components/playground-badge.js +2 -2
  374. package/dist/components/playground-badge.js.map +1 -1
  375. package/dist/components/project-list.js +3 -3
  376. package/dist/components/project-list.js.map +1 -1
  377. package/dist/components/project-session.d.ts +2 -2
  378. package/dist/components/project-session.js +5 -5
  379. package/dist/components/project-session.js.map +1 -1
  380. package/dist/components/project-switcher.js +2 -2
  381. package/dist/components/project-switcher.js.map +1 -1
  382. package/dist/components/rail-store-status.js +1 -1
  383. package/dist/components/rail-store-status.js.map +1 -1
  384. package/dist/components/rail-view-source.d.ts +6 -5
  385. package/dist/components/rail-view-source.js +4 -3
  386. package/dist/components/rail-view-source.js.map +1 -1
  387. package/dist/components/records/record-lens.d.ts +51 -0
  388. package/dist/components/records/record-lens.js +151 -0
  389. package/dist/components/records/record-lens.js.map +1 -0
  390. package/dist/components/records/record-rail.d.ts +86 -0
  391. package/dist/components/records/record-rail.js +241 -0
  392. package/dist/components/records/record-rail.js.map +1 -0
  393. package/dist/components/redirects/redirects-schema.js +1 -1
  394. package/dist/components/redirects/redirects-schema.js.map +1 -1
  395. package/dist/components/references/references-panel.js +2 -2
  396. package/dist/components/references/references-panel.js.map +1 -1
  397. package/dist/components/section-nav.js +27 -11
  398. package/dist/components/section-nav.js.map +1 -1
  399. package/dist/components/section-state.js +2 -2
  400. package/dist/components/section-state.js.map +1 -1
  401. package/dist/components/section-surface.js +2 -2
  402. package/dist/components/section-surface.js.map +1 -1
  403. package/dist/components/session-expired-overlay.js +3 -3
  404. package/dist/components/session-expired-overlay.js.map +1 -1
  405. package/dist/components/settings-form.js +1 -1
  406. package/dist/components/settings-form.js.map +1 -1
  407. package/dist/components/sign-in-needed-badge.js +1 -1
  408. package/dist/components/sign-in-needed-badge.js.map +1 -1
  409. package/dist/components/sign-in-view.d.ts +2 -2
  410. package/dist/components/sign-in-view.js +5 -5
  411. package/dist/components/sign-in-view.js.map +1 -1
  412. package/dist/components/studio-context.js +3 -3
  413. package/dist/components/studio-context.js.map +1 -1
  414. package/dist/components/studio-project-screen.js +30 -3
  415. package/dist/components/studio-project-screen.js.map +1 -1
  416. package/dist/components/studio-shell.js +6 -5
  417. package/dist/components/studio-shell.js.map +1 -1
  418. package/dist/components/theme-context.d.ts +1 -1
  419. package/dist/components/theme-context.js +2 -2
  420. package/dist/components/theme-context.js.map +1 -1
  421. package/dist/components/use-hydration-phase.d.ts +1 -1
  422. package/dist/components/use-hydration-phase.js +1 -1
  423. package/dist/components/use-hydration-phase.js.map +1 -1
  424. package/dist/components/users/invite-user-bar.d.ts +27 -0
  425. package/dist/components/users/invite-user-bar.js +25 -9
  426. package/dist/components/users/invite-user-bar.js.map +1 -1
  427. package/dist/components/users/role-policy-drift-issues.d.ts +38 -0
  428. package/dist/components/users/role-policy-drift-issues.js +71 -0
  429. package/dist/components/users/role-policy-drift-issues.js.map +1 -0
  430. package/dist/components/users/users-collection-view-source.d.ts +1 -1
  431. package/dist/components/users/users-collection-view-source.js +4 -2
  432. package/dist/components/users/users-collection-view-source.js.map +1 -1
  433. package/dist/components/users/users-module.js +1 -1
  434. package/dist/components/users/users-module.js.map +1 -1
  435. package/dist/components/users/users-schema.d.ts +10 -1
  436. package/dist/components/users/users-schema.js +45 -25
  437. package/dist/components/users/users-schema.js.map +1 -1
  438. package/dist/components/users/users-store.js +3 -3
  439. package/dist/components/users/users-store.js.map +1 -1
  440. package/dist/components/workspace-projects.d.ts +1 -1
  441. package/dist/components/workspace-projects.js +1 -1
  442. package/dist/components/workspace-projects.js.map +1 -1
  443. package/dist/lib/ai/index.d.ts +1 -1
  444. package/dist/lib/ai/index.js +1 -1
  445. package/dist/lib/ai/index.js.map +1 -1
  446. package/dist/lib/ai/node-handler.d.ts +1 -1
  447. package/dist/lib/ai/node-handler.js.map +1 -1
  448. package/dist/lib/ai/translate-document.js +1 -1
  449. package/dist/lib/ai/translate-document.js.map +1 -1
  450. package/dist/lib/assets/upload.d.ts +25 -7
  451. package/dist/lib/assets/upload.js +30 -21
  452. package/dist/lib/assets/upload.js.map +1 -1
  453. package/dist/lib/clipboard/paste-issues.js +1 -1
  454. package/dist/lib/clipboard/paste-issues.js.map +1 -1
  455. package/dist/lib/clipboard/paste-target.js +2 -2
  456. package/dist/lib/clipboard/paste-target.js.map +1 -1
  457. package/dist/lib/cms/published-documents.d.ts +24 -6
  458. package/dist/lib/cms/published-documents.js +39 -6
  459. package/dist/lib/cms/published-documents.js.map +1 -1
  460. package/dist/lib/cms-views/view-model.d.ts +12 -99
  461. package/dist/lib/cms-views/view-model.js +22 -103
  462. package/dist/lib/cms-views/view-model.js.map +1 -1
  463. package/dist/lib/collection-view/collection-view-source.d.ts +13 -3
  464. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  465. package/dist/lib/data/content-session-refresh.d.ts +1 -1
  466. package/dist/lib/data/content-session-refresh.js.map +1 -1
  467. package/dist/lib/data/playground-unload-guard.d.ts +1 -1
  468. package/dist/lib/data/playground-unload-guard.js +1 -1
  469. package/dist/lib/data/playground-unload-guard.js.map +1 -1
  470. package/dist/lib/engine-version.d.ts +1 -1
  471. package/dist/lib/engine-version.js +1 -1
  472. package/dist/lib/engine-version.js.map +1 -1
  473. package/dist/lib/i18n/i18n.js +1 -1
  474. package/dist/lib/i18n/i18n.js.map +1 -1
  475. package/dist/lib/i18n/messages.de.js +11 -3
  476. package/dist/lib/i18n/messages.de.js.map +1 -1
  477. package/dist/lib/i18n/messages.en.d.ts +9 -1
  478. package/dist/lib/i18n/messages.en.js +16 -4
  479. package/dist/lib/i18n/messages.en.js.map +1 -1
  480. package/dist/lib/operations/config-manifest-lookup.d.ts +1 -1
  481. package/dist/lib/operations/config-manifest-lookup.js +3 -3
  482. package/dist/lib/operations/config-manifest-lookup.js.map +1 -1
  483. package/dist/lib/operations/project-runtime-config.d.ts +9 -0
  484. package/dist/lib/operations/project-runtime-config.js +13 -0
  485. package/dist/lib/operations/project-runtime-config.js.map +1 -1
  486. package/dist/lib/operations/studio-operations.d.ts +9 -1
  487. package/dist/lib/operations/studio-operations.js +6 -1
  488. package/dist/lib/operations/studio-operations.js.map +1 -1
  489. package/dist/lib/persistence/convex-adapter.d.ts +16 -3
  490. package/dist/lib/persistence/convex-adapter.js +78 -47
  491. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  492. package/dist/lib/persistence/deployment-readiness.js +14 -11
  493. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  494. package/dist/lib/persistence/index.js +14 -1
  495. package/dist/lib/persistence/index.js.map +1 -1
  496. package/dist/lib/persona/labels.js +2 -1
  497. package/dist/lib/persona/labels.js.map +1 -1
  498. package/dist/lib/query/index.d.ts +2 -2
  499. package/dist/lib/query/index.js +1 -1
  500. package/dist/lib/query/index.js.map +1 -1
  501. package/dist/lib/query/operations-query.d.ts +27 -1
  502. package/dist/lib/query/operations-query.js +13 -1
  503. package/dist/lib/query/operations-query.js.map +1 -1
  504. package/dist/lib/references/reference-model.js +1 -1
  505. package/dist/lib/references/reference-model.js.map +1 -1
  506. package/dist/lib/references/references-store.js +1 -1
  507. package/dist/lib/references/references-store.js.map +1 -1
  508. package/dist/lib/registry/index.js +2 -2
  509. package/dist/lib/registry/index.js.map +1 -1
  510. package/dist/lib/registry/preview.d.ts +10 -0
  511. package/dist/lib/registry/preview.js +13 -4
  512. package/dist/lib/registry/preview.js.map +1 -1
  513. package/dist/lib/routes/route-issues.js +6 -3
  514. package/dist/lib/routes/route-issues.js.map +1 -1
  515. package/dist/lib/workspace/index.d.ts +6 -0
  516. package/dist/lib/workspace/index.js +8 -0
  517. package/dist/lib/workspace/index.js.map +1 -1
  518. package/dist/lib/workspace/workspace-source.js +6 -7
  519. package/dist/lib/workspace/workspace-source.js.map +1 -1
  520. package/dist/mount.d.ts +28 -0
  521. package/dist/mount.js +27 -0
  522. package/dist/mount.js.map +1 -0
  523. package/dist/previews/entry-node.d.ts +14 -0
  524. package/dist/previews/entry-node.js +22 -0
  525. package/dist/previews/entry-node.js.map +1 -0
  526. package/dist/previews/harness.js +2 -16
  527. package/dist/previews/harness.js.map +1 -1
  528. package/dist/router/root.js +10 -10
  529. package/dist/router/root.js.map +1 -1
  530. package/dist/ui/badge.d.ts +1 -1
  531. package/dist/ui/button.d.ts +2 -2
  532. package/package.json +15 -11
  533. package/dist/components/redirects/redirects-collection-view-source.d.ts +0 -53
  534. package/dist/components/redirects/redirects-collection-view-source.js +0 -146
  535. package/dist/components/redirects/redirects-collection-view-source.js.map +0 -1
  536. package/dist/components/redirects/redirects-module.d.ts +0 -21
  537. package/dist/components/redirects/redirects-module.js +0 -40
  538. package/dist/components/redirects/redirects-module.js.map +0 -1
  539. package/dist/components/redirects/redirects-store.d.ts +0 -68
  540. package/dist/components/redirects/redirects-store.js +0 -132
  541. package/dist/components/redirects/redirects-store.js.map +0 -1
@@ -0,0 +1,167 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { convexTest } from 'convex-test'
3
+ import { makeFunctionReference } from 'convex/server'
4
+ import type { FunctionReference } from 'convex/server'
5
+ import type { ConvexHttpClient } from 'convex/browser'
6
+ // The host's two readers, from their own modules — the package index also loads the React
7
+ // delivery kit, which the edge-runtime environment cannot evaluate.
8
+ import { assembleLiveSource } from '../../next/src/host-content'
9
+ import { createContentSnapshot } from '../../next/src/snapshot'
10
+ import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
11
+ import schema from './schema'
12
+
13
+ /**
14
+ * The delivery snapshot's redirect strand, end to end: rows stored in the deployment's
15
+ * `records` table (kind `redirect`) → the REAL `delivery:snapshot` query (convex-test) →
16
+ * the REAL host loader (`assembleLiveSource`, `@elytracms/next`).
17
+ *
18
+ * EC-628 (Codex F04, `tasks/codex-review-2026-10-27/backend.convex.test.ts` "F04 upstream"):
19
+ * the record store's open data bag accepts drift the normal CRUD refuses (a dashboard edit,
20
+ * an older writer). Before, an enabled row with `to: 42` was SERVED by the snapshot and
21
+ * vanished in the host with zero issues, and a switch that is not a boolean was decided by
22
+ * truthiness (`'false'` served, a missing switch silently paused). Now the snapshot reads
23
+ * every stored redirect through the one rule (`readLiveRedirects`): what it cannot read is
24
+ * withheld and reported on the snapshot's `redirectIssues` strand, which the host carries
25
+ * into its `sourceIssues` and onto the 404 at the redirect's `from`.
26
+ */
27
+
28
+ const modules = {
29
+ ...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
30
+ './_generated/api.ts': async () => ({}),
31
+ }
32
+ const snapshot = makeFunctionReference<'query'>('delivery:snapshot')
33
+
34
+ const AT = '2026-10-27T12:00:00.000Z'
35
+ const LOCALES = { default: 'en', locales: ['en'] }
36
+
37
+ /** Insert redirect rows as the record store holds them — raw, as drift reaches it. */
38
+ async function deploymentWith(rows: ReadonlyArray<{ id: string; data: Record<string, unknown> }>) {
39
+ const t = convexTest(schema, modules)
40
+ await t.run(async (ctx) => {
41
+ for (const row of rows) {
42
+ await ctx.db.insert('records', { kind: 'redirect', recordId: row.id, data: row.data, at: AT, createdAt: AT, updatedAt: AT })
43
+ }
44
+ })
45
+ return t
46
+ }
47
+
48
+ const redirect = (id: string, data: Record<string, unknown> = {}) => ({
49
+ id,
50
+ data: { from: `/${id}`, to: '/target', permanent: true, enabled: true, ...data },
51
+ })
52
+
53
+ describe('EC-628 — the snapshot names every stored redirect it cannot serve', () => {
54
+ it('drift is withheld and reported upstream, then located by the host (F04 + disabled-state drift)', async () => {
55
+ const t = await deploymentWith([
56
+ redirect('good'),
57
+ redirect('bad', { to: 42 }), // F04's row
58
+ redirect('paused', { enabled: false }), // a paused redirect: not live, not a finding
59
+ redirect('string-off', { enabled: 'false' }), // truthy: was SERVED before
60
+ { id: 'no-switch', data: { from: '/no-switch', to: '/target', permanent: true } }, // falsy: was silently paused
61
+ ])
62
+ const payload = (await t.query(snapshot, { projectId: 'site' })) as {
63
+ redirects: unknown[]
64
+ redirectIssues: Array<{ code: string; path: unknown[] }>
65
+ }
66
+ expect(payload.redirects).toEqual([{ id: 'good', from: '/good', to: '/target', permanent: true }])
67
+ expect(payload.redirectIssues.map((issue) => [issue.code, issue.path])).toEqual([
68
+ ['invalid-redirect', ['redirects', 'bad']],
69
+ ['invalid-redirect', ['redirects', 'no-switch']],
70
+ ['invalid-redirect', ['redirects', 'string-off']],
71
+ ])
72
+
73
+ const client = assembleLiveSource(payload as never, { locales: LOCALES, collections: [], routes: [] }, 'published').client({
74
+ perspective: 'published',
75
+ })
76
+ // Control: the readable redirect traverses the whole pipeline, with no issue.
77
+ expect(client.resolvePage('/good').status).toBe('redirect')
78
+ expect(client.resolvePage('/good').issues).toEqual([])
79
+ expect(client.sourceIssues.map((issue) => issue.path[1])).toEqual(['bad', 'no-switch', 'string-off'])
80
+ for (const path of ['/bad', '/string-off', '/no-switch']) {
81
+ const result = client.resolvePage(path)
82
+ expect(result.status).toBe('notFound')
83
+ expect(result.issues.map((issue) => issue.code)).toEqual(['invalid-redirect'])
84
+ }
85
+ // The paused redirect is a 404 with nothing to say — pausing is not drift.
86
+ expect(client.resolvePage('/paused').status).toBe('notFound')
87
+ expect(client.resolvePage('/paused').issues).toEqual([])
88
+ })
89
+ })
90
+
91
+ /**
92
+ * EC-627 — "which redirects are live" is decided by ONE reader on every side. The same
93
+ * stored set is read (1) by the Convex delivery snapshot (`delivery:snapshot`, through
94
+ * `readLiveRedirects`) and assembled by the host's live loader, and (2) by the host's OTHER
95
+ * reader, `createContentSnapshot` (`next/src/snapshot.ts`, `readLiveRedirects` over
96
+ * `adapter.records(redirectRecordKind).list`) — through the REAL `ConvexPersistenceAdapter`
97
+ * over the same convex-test deployment, so both sides read the very same stored rows.
98
+ *
99
+ * The set holds what a hand-built copy of the rule gets wrong: a paused redirect (a copy that
100
+ * forgets the switch serves it), a switch that is not a boolean (a truthy copy serves
101
+ * `'false'`), a duplicate `from` (`/old` and `/old/` — the lower id applies on both sides)
102
+ * and an editorial `meta` bag (never on the routing record). Measured: replacing either
103
+ * side's call with a hand-built `.filter((r) => r.enabled)` projection turns this red.
104
+ */
105
+ describe('EC-627 — the Convex snapshot and the host adapter snapshot serve one live set', () => {
106
+ const STORED = [
107
+ redirect('a', { meta: { note: 'editorial only' } }),
108
+ redirect('paused', { enabled: false }),
109
+ redirect('string-off', { enabled: 'false' }),
110
+ { id: 'dup-1', data: { from: '/old', to: '/one', permanent: true, enabled: true } },
111
+ { id: 'dup-2', data: { from: '/old/', to: '/two', permanent: false, enabled: true } },
112
+ ]
113
+ const PATHS = ['/a', '/paused', '/string-off', '/old', '/old/']
114
+
115
+ /** The decision a reader makes per URL: what a visitor gets. */
116
+ const outcomes = (client: { resolvePage(url: string): { status: string; redirect: unknown } }) =>
117
+ PATHS.map((path) => {
118
+ const result = client.resolvePage(path)
119
+ return { path, status: result.status, redirect: result.redirect }
120
+ })
121
+
122
+ it('the same stored set yields the same live set through both readers', async () => {
123
+ const t = await deploymentWith(STORED)
124
+ await t.run(async (ctx) => {
125
+ await ctx.db.insert('members', { userId: 'admin', role: 'admin', createdAt: AT, updatedAt: AT })
126
+ })
127
+
128
+ // (1) the deployment's public snapshot → the host's live loader
129
+ const payload = await t.query(snapshot, { projectId: 'site' })
130
+ const viaSnapshot = assembleLiveSource(payload as never, { locales: LOCALES, collections: [], routes: [] }, 'published')
131
+ .client({ perspective: 'published' })
132
+
133
+ // (2) the host's adapter reader over the same deployment
134
+ const asAdmin = t.withIdentity({ subject: 'admin', issuer: 'https://convex.test' })
135
+ const adapter = new ConvexPersistenceAdapter(
136
+ {
137
+ query: (ref: FunctionReference<'query'>, args?: Record<string, unknown>) => asAdmin.query(ref, args ?? {}),
138
+ mutation: (ref: FunctionReference<'mutation'>, args?: Record<string, unknown>) => asAdmin.mutation(ref, args ?? {}),
139
+ } as unknown as ConvexHttpClient,
140
+ 'site',
141
+ )
142
+ const viaAdapter = (await createContentSnapshot(adapter, 'site', { locales: LOCALES, collections: [], routes: [] }))
143
+ .client({ perspective: 'published' })
144
+
145
+ // The instrument can tell outcomes apart — pinned explicitly, not only compared.
146
+ expect(outcomes(viaSnapshot)).toEqual([
147
+ { path: '/a', status: 'redirect', redirect: { target: '/target', permanent: true } },
148
+ { path: '/paused', status: 'notFound', redirect: null },
149
+ { path: '/string-off', status: 'notFound', redirect: null },
150
+ { path: '/old', status: 'redirect', redirect: { target: '/one', permanent: true } },
151
+ { path: '/old/', status: 'redirect', redirect: { target: '/one', permanent: true } },
152
+ ])
153
+ expect(outcomes(viaAdapter)).toEqual(outcomes(viaSnapshot))
154
+ // The duplicate is one issue, the same on both sides (the router's one redirect set).
155
+ const duplicates = (issues: readonly { code: string; path: readonly unknown[] }[]) =>
156
+ issues.filter((issue) => issue.code === 'invalid-redirect').map((issue) => issue.path)
157
+ expect(duplicates(viaSnapshot.routerIssues)).toEqual([['redirects', 'dup-2']])
158
+ expect(duplicates(viaAdapter.routerIssues)).toEqual(duplicates(viaSnapshot.routerIssues))
159
+
160
+ // EC-628 (the adapter snapshot): the drifted row is NAMED on both sides, not only withheld —
161
+ // on the source and on the 404 at its `from`. (Before, the adapter path withheld it silently.)
162
+ const sourcePaths = (client: typeof viaSnapshot) => client.sourceIssues.map((issue) => [issue.code, issue.path])
163
+ expect(sourcePaths(viaSnapshot)).toEqual([['invalid-redirect', ['redirects', 'string-off']]])
164
+ expect(sourcePaths(viaAdapter)).toEqual(sourcePaths(viaSnapshot))
165
+ expect(viaAdapter.resolvePage('/string-off').issues.map((issue) => issue.code)).toEqual(['invalid-redirect'])
166
+ })
167
+ })
@@ -1,12 +1,14 @@
1
1
  import { v } from 'convex/values'
2
- import { DRAFT_PERSPECTIVE, DRAFT_VARIANT, unfoldValue, variantOf } from '@elytracms/core/cms-core'
2
+ import { DRAFT_PERSPECTIVE, DRAFT_VARIANT, readLiveRedirects, unfoldValue, variantOf } from '@elytracms/core/cms-core'
3
3
  import type { Perspective } from '@elytracms/core/cms-core'
4
4
  import { assetRecordFromRow, deliveryAssetRecord } from '@elytracms/core/persistence'
5
+ import { constantTimeEquals } from '@elytracms/core/revalidation'
5
6
  import type { AssetRecord } from '@elytracms/core/persistence'
6
7
  import studioPackage from '../package.json'
7
8
  import { query } from './functions'
8
9
  import type { QueryCtx } from './functions'
9
10
  import { requireCapability } from './guard'
11
+ import { loadRedirectEntries } from './redirects'
10
12
 
11
13
  // EC-207: routes/redirects are config-sourced (authored in elytra.config.ts and
12
14
  // resolved by studio-core), NOT backend data. The delivery snapshot no longer
@@ -75,6 +77,12 @@ interface DeliverySnapshot {
75
77
  * infra redirects stay native (`next.config`/`vercel`), evaluated before these.
76
78
  */
77
79
  redirects: unknown[]
80
+ /**
81
+ * EC-628 — one `invalid-redirect` issue per stored redirect the live rule withheld because
82
+ * it could not read it (id, `from` and the kind of fault — never the target or a value).
83
+ * Paused redirects are not here: pausing is not drift.
84
+ */
85
+ redirectIssues: unknown[]
78
86
  }
79
87
 
80
88
  /**
@@ -156,20 +164,23 @@ export async function loadSnapshot(
156
164
 
157
165
  const assets = await loadDeliveryAssets(ctx, projectId)
158
166
 
159
- // Editorial redirects (EC-303): only ENABLED entries serve — a disabled redirect
160
- // is stored but never live, in either perspective. Projected to the routing
161
- // `RedirectRecord` shape the router/`resolvePage` consume.
162
- const redirectRows = await ctx.db.query('redirects').withIndex('by_redirect').collect()
163
- const redirects = redirectRows
164
- .filter((row) => row.enabled)
165
- .map((row) => ({ id: row.redirectId, from: row.from, to: row.to, permanent: row.permanent }))
167
+ // Editorial redirects (EC-303): only ENABLED entries serve — a disabled redirect is
168
+ // stored but never live, in either perspective. EC-616: read through the redirect
169
+ // kind's one server-side reader (records + the marked legacy tolerant read) and
170
+ // decided by the ONE live-redirect rule, `readLiveRedirects` (EC-590 / EC-627 — this
171
+ // was the last hand-built copy on Convex). EC-628: a stored redirect the rule cannot
172
+ // read (a non-boolean switch, a non-string `to`) is withheld AND reported — the host
173
+ // carries `redirectIssues` into its source issues — instead of being served or dropped
174
+ // by truthiness. The record kind's own read, not the project schema (EC-585 1e).
175
+ const live = readLiveRedirects(await loadRedirectEntries(ctx, projectId))
166
176
 
167
177
  return {
168
178
  projectId,
169
179
  perspective,
170
180
  documents,
171
181
  assets,
172
- redirects,
182
+ redirects: live.redirects,
183
+ redirectIssues: live.issues,
173
184
  }
174
185
  }
175
186
 
@@ -214,7 +225,9 @@ export const draftSnapshot = query({
214
225
  if (expected === undefined || expected.length === 0) {
215
226
  throw new Error('unauthorized: draft preview is not configured on this deployment')
216
227
  }
217
- if (args.token !== expected) {
228
+ // EC-617: the one timing-safe comparison (`constantTimeEquals`, the MAC-compare the
229
+ // revalidation and ingest verifiers rest on) — never a string `!==`.
230
+ if (!(await constantTimeEquals(args.token, expected))) {
218
231
  throw new Error('unauthorized: invalid draft preview token')
219
232
  }
220
233
  return loadSnapshot(ctx, { projectId: args.projectId }, DRAFT_PERSPECTIVE)
package/convex/guard.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { getAuthUserId } from '@convex-dev/auth/server'
2
- import { defaultRolePolicy, roleGrantsCapability } from '@elytracms/core/persistence'
2
+ import { roleGrantsCapability } from '@elytracms/core/persistence'
3
3
  import type { OperationCapabilityId, ProjectRole, RolePolicy } from '@elytracms/core/persistence'
4
4
  import type { QueryCtx } from './functions'
5
+ import { deployedRolePolicy } from './roles'
5
6
 
6
7
  /**
7
8
  * Server-side authorization guards (EC-150). The operations chokepoint in the
@@ -18,9 +19,10 @@ import type { QueryCtx } from './functions'
18
19
  * imported here rather than re-declared. Handlers gate on CAPABILITIES
19
20
  * (`content.read` / `content.write` / `project.administer`), not literal roles
20
21
  * (EC-240c), so a project's custom role set enforces server-side by the same
21
- * policy the operations layer uses. The Convex trust boundary reads the shipped
22
- * {@link defaultRolePolicy}; a project overriding roles injects its policy (the
23
- * documented follow-up seam).
22
+ * policy the operations layer uses. EC-605: the policy is the project's `roles`
23
+ * declaration as DEPLOYED — the copy the `elytra` CLI provisions into
24
+ * `ELYTRA_ROLE_POLICY` (`./roles` `deployedRolePolicy`: the default when unset, a
25
+ * thrown, located error when malformed — never a silent default).
24
26
  *
25
27
  * Failures throw plain `Error('unauthorized: …')` — Convex transports the
26
28
  * message; the app's adapters normalize thrown backend errors onto the
@@ -78,13 +80,13 @@ export async function memberRole(ctx: GuardCtx, userId: string): Promise<Project
78
80
  * This is the ONE role-enforcement primitive at the trust boundary — every
79
81
  * handler gates on a capability rather than a literal role, so a project's
80
82
  * custom role set is honoured by the shared policy. `policy` defaults to the
81
- * shipped {@link defaultRolePolicy}; the override seam (a project injecting its
82
- * own policy) is the documented follow-up.
83
+ * deployment's policy (EC-605, {@link deployedRolePolicy}); the parameter stays for a
84
+ * caller that must judge against another (tests).
83
85
  */
84
86
  export async function requireCapability(
85
87
  ctx: GuardCtx,
86
88
  capability: OperationCapabilityId,
87
- policy: RolePolicy = defaultRolePolicy,
89
+ policy: RolePolicy = deployedRolePolicy(),
88
90
  ): Promise<string> {
89
91
  const userId = await requireUserId(ctx)
90
92
  const actual = await memberRole(ctx, userId)
@@ -33,9 +33,20 @@ function routeTable(http: HttpRouter): string[] {
33
33
  .sort()
34
34
  }
35
35
 
36
- describe('defineStudioHttp — the engine registers no HTTP routes of its own (EC-481)', () => {
37
- it('contributes an EMPTY route table beyond what the injected auth registers', () => {
38
- expect(routeTable(defineStudioHttp({ auth: silentAuth }))).toEqual([])
36
+ /** EC-617: the engine's ONE route — the public-ingest receiver, an exact path (the EC-481 bar: a real client, a narrow prefix). */
37
+ const ENGINE_ROUTES = ['POST /elytra/ingest']
38
+
39
+ describe('defineStudioHttp — the engine registers exactly one HTTP route of its own (EC-481 → EC-617)', () => {
40
+ it('contributes ONLY the ingest receiver beyond what the injected auth registers', () => {
41
+ expect(routeTable(defineStudioHttp({ auth: silentAuth }))).toEqual(ENGINE_ROUTES)
42
+ })
43
+
44
+ it('the receiver is an EXACT path, POST only — it claims no prefix', () => {
45
+ const http = defineStudioHttp({ auth: silentAuth })
46
+ expect(http.lookup('/elytra/ingest', 'POST')).not.toBeNull()
47
+ expect(http.lookup('/elytra/ingest/anything', 'POST')).toBeNull()
48
+ expect(http.lookup('/elytra/', 'POST')).toBeNull()
49
+ expect(http.lookup('/elytra/ingest', 'GET')).toBeNull()
39
50
  })
40
51
 
41
52
  it('control: the recorder DOES see routes, so the empty result above is real', () => {
@@ -45,10 +56,9 @@ describe('defineStudioHttp — the engine registers no HTTP routes of its own (E
45
56
  http.route({ pathPrefix: '/api/auth/callback/', method: 'POST', handler })
46
57
  },
47
58
  }
48
- expect(routeTable(defineStudioHttp({ auth }))).toEqual([
49
- 'GET /.well-known/jwks.json',
50
- 'POST /api/auth/callback/*',
51
- ])
59
+ expect(routeTable(defineStudioHttp({ auth }))).toEqual(
60
+ ['GET /.well-known/jwks.json', 'POST /api/auth/callback/*', ...ENGINE_ROUTES].sort(),
61
+ )
52
62
  })
53
63
 
54
64
  it('passes the auth routes through untouched — nothing is added, dropped, or reordered', () => {
@@ -61,7 +71,7 @@ describe('defineStudioHttp — the engine registers no HTTP routes of its own (E
61
71
  }
62
72
  },
63
73
  }
64
- expect(routeTable(defineStudioHttp({ auth }))).toEqual([...registered].sort())
74
+ expect(routeTable(defineStudioHttp({ auth }))).toEqual([...registered, ...ENGINE_ROUTES].sort())
65
75
  })
66
76
  })
67
77
 
package/convex/http.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { httpRouter } from 'convex/server'
2
2
  import type { HttpRouter } from 'convex/server'
3
+ import { INGEST_RECEIVER_PATH } from '@elytracms/core/ingest'
4
+ import { ingestReceiver } from './ingest'
5
+ import type { IngestHandlers } from './ingest'
3
6
 
4
7
  /** The slice of the Convex Auth object the studio's HTTP routes need. */
5
8
  interface AuthHttpRoutes {
@@ -8,33 +11,36 @@ interface AuthHttpRoutes {
8
11
 
9
12
  /**
10
13
  * EC-252 — the studio's HTTP routes as a factory. Pure engine infra ships in the
11
- * package; only the consumer's `auth` (provider choice) is injected. The consumer's
12
- * `convex/http.ts` is a two-liner:
14
+ * package; only the consumer's `auth` (provider choice) and — since EC-617 — its ingest
15
+ * handlers are injected. The consumer's `convex/http.ts`:
13
16
  *
14
17
  * ```ts
15
18
  * import { auth } from './auth'
16
19
  * export default defineStudioHttp({ auth })
20
+ * // with an addon that handles ingest payloads (one import, one entry):
21
+ * // import { formsIngest } from '@elytracms/forms/convex'
22
+ * // export default defineStudioHttp({ auth, ingest: { submission: formsIngest } })
17
23
  * ```
18
24
  *
19
- * ## The engine registers NO HTTP routes of its own (EC-481)
25
+ * ## The engine registers exactly ONE route of its own (EC-481 → EC-617)
20
26
  *
21
- * Everything on this router comes from Convex Auth's `addHttpRoutes` — the JWT
22
- * discovery endpoints (`/.well-known/*`, EC-150) and, only when a consumer supplies
23
- * an OAuth provider, `/api/auth/signin/*` + `/api/auth/callback/*`.
27
+ * Everything else on this router comes from Convex Auth's `addHttpRoutes` — the JWT
28
+ * discovery endpoints (`/.well-known/*`, EC-150) and, only when a consumer supplies an
29
+ * OAuth provider, `/api/auth/signin/*` + `/api/auth/callback/*`.
24
30
  *
25
- * The engine used to add one more: `GET /api/projects/*`, the EC-157 Bearer-token
26
- * registry-sync endpoint backing `builder typegen`/`builder check`. EC-470 deleted
27
- * that CLI and with it the endpoint's only client; the handler had in any case
28
- * answered with config-owned placeholders since EC-287 retired the project graph
29
- * (`project: null`, `collections: []`). EC-481 removed the route and its handler
30
- * together rather than leave a Bearer-authenticated surface serving stub data on
31
- * every deployment. `http-routes.test.ts` pins that shut.
32
- *
33
- * A future engine-owned HTTP surface is fine — but it needs a real client and a
34
- * narrower prefix than `/api/projects/`.
31
+ * EC-481 removed the engine's former `GET /api/projects/*` (a Bearer-authenticated
32
+ * registry-sync endpoint with no client left) and set the bar for any future one: "a
33
+ * real client and a narrower prefix". EC-617's public-ingest RECEIVER is that route:
34
+ * `POST /elytra/ingest` — an EXACT path, not a prefix; its client is the site's
35
+ * `createIngestRoute` (`@elytracms/next`), which signs every payload with the
36
+ * server-held secret, and `elytra doctor`'s probe. It is mounted on every deployment,
37
+ * with or without handlers: closed (401) until `ELYTRA_INGEST_SECRET` is set, and a
38
+ * payload kind with no registered handler is refused visibly (422), never dropped.
39
+ * `http-routes.test.ts` pins the table.
35
40
  */
36
- export function defineStudioHttp({ auth }: { auth: AuthHttpRoutes }): HttpRouter {
41
+ export function defineStudioHttp({ auth, ingest }: { auth: AuthHttpRoutes; ingest?: IngestHandlers }): HttpRouter {
37
42
  const http = httpRouter()
38
43
  auth.addHttpRoutes(http)
44
+ http.route({ path: INGEST_RECEIVER_PATH, method: 'POST', handler: ingestReceiver(ingest) })
39
45
  return http
40
46
  }