@elytracms/studio 0.0.92 → 0.0.94

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 (599) hide show
  1. package/convex/analytics-cost.test.ts +817 -0
  2. package/convex/asset-alt-migration.test.ts +127 -0
  3. package/convex/asset-replace.test.ts +168 -0
  4. package/convex/asset-revalidate.test.ts +6 -6
  5. package/convex/assets.ts +41 -5
  6. package/convex/auth.ts +5 -2
  7. package/convex/authority.ts +35 -11
  8. package/convex/cms.ts +25 -11
  9. package/convex/contract-parity.test.ts +25 -3
  10. package/convex/delivery.ts +12 -9
  11. package/convex/document-revalidate.test.ts +10 -12
  12. package/convex/guard.ts +9 -7
  13. package/convex/http-routes.test.ts +18 -8
  14. package/convex/http.ts +23 -17
  15. package/convex/ingest.test.ts +326 -0
  16. package/convex/ingest.ts +281 -0
  17. package/convex/invites.ts +13 -5
  18. package/convex/members.ts +10 -4
  19. package/convex/orphan-hydration.test.ts +116 -0
  20. package/convex/rail-write-parity.test.ts +11 -8
  21. package/convex/record-functions.ts +325 -0
  22. package/convex/records.test.ts +156 -0
  23. package/convex/redirect-migration.test.ts +205 -0
  24. package/convex/redirect-revalidate.test.ts +5 -1
  25. package/convex/redirects.ts +120 -79
  26. package/convex/role-policy.test.ts +315 -0
  27. package/convex/roles.ts +83 -0
  28. package/convex/schema.ts +36 -0
  29. package/convex/triggers.ts +21 -1
  30. package/dist/addon.d.ts +71 -0
  31. package/dist/addon.js +67 -0
  32. package/dist/addon.js.map +1 -0
  33. package/dist/components/account/account-settings.js +2 -2
  34. package/dist/components/account/account-settings.js.map +1 -1
  35. package/dist/components/account/credential-panel.js +2 -2
  36. package/dist/components/account/credential-panel.js.map +1 -1
  37. package/dist/components/account/self-account-schema.d.ts +6 -15
  38. package/dist/components/account/self-account-schema.js +52 -34
  39. package/dist/components/account/self-account-schema.js.map +1 -1
  40. package/dist/components/account/self-account-view-source.d.ts +1 -1
  41. package/dist/components/account/self-account-view-source.js +6 -4
  42. package/dist/components/account/self-account-view-source.js.map +1 -1
  43. package/dist/components/ai/ai-chat-panel-lazy.js +1 -1
  44. package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
  45. package/dist/components/ai/ai-chat-panel.js +6 -6
  46. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  47. package/dist/components/ai/ai-cms-panel.js +2 -2
  48. package/dist/components/ai/ai-cms-panel.js.map +1 -1
  49. package/dist/components/ai/ai-panel-toggle.js +4 -4
  50. package/dist/components/ai/ai-panel-toggle.js.map +1 -1
  51. package/dist/components/ai/chat-ui.js +2 -2
  52. package/dist/components/ai/chat-ui.js.map +1 -1
  53. package/dist/components/ai/proposal-card.d.ts +2 -2
  54. package/dist/components/ai/proposal-card.js +4 -3
  55. package/dist/components/ai/proposal-card.js.map +1 -1
  56. package/dist/components/ai/proposal-issues.js +2 -2
  57. package/dist/components/ai/proposal-issues.js.map +1 -1
  58. package/dist/components/ai/translate-document-action.js +2 -2
  59. package/dist/components/ai/translate-document-action.js.map +1 -1
  60. package/dist/components/ai/translate-document-dialog.js +10 -8
  61. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  62. package/dist/components/ai/translate-skipped.d.ts +19 -0
  63. package/dist/components/ai/translate-skipped.js +46 -0
  64. package/dist/components/ai/translate-skipped.js.map +1 -0
  65. package/dist/components/app-views/default-modules.d.ts +50 -2
  66. package/dist/components/app-views/default-modules.js +65 -20
  67. package/dist/components/app-views/default-modules.js.map +1 -1
  68. package/dist/components/app-views/module.d.ts +53 -3
  69. package/dist/components/app-views/module.js +14 -7
  70. package/dist/components/app-views/module.js.map +1 -1
  71. package/dist/components/app-views/sidebar.js +1 -1
  72. package/dist/components/app-views/sidebar.js.map +1 -1
  73. package/dist/components/app-views/studio-config.d.ts +11 -2
  74. package/dist/components/app-views/studio-config.js.map +1 -1
  75. package/dist/components/assets/asset-detail.js +20 -10
  76. package/dist/components/assets/asset-detail.js.map +1 -1
  77. package/dist/components/assets/asset-grid.js +2 -2
  78. package/dist/components/assets/asset-grid.js.map +1 -1
  79. package/dist/components/assets/asset-metadata.d.ts +15 -4
  80. package/dist/components/assets/asset-metadata.js +15 -2
  81. package/dist/components/assets/asset-metadata.js.map +1 -1
  82. package/dist/components/assets/asset-search.js +4 -1
  83. package/dist/components/assets/asset-search.js.map +1 -1
  84. package/dist/components/assets/asset-selection.d.ts +0 -8
  85. package/dist/components/assets/asset-selection.js +4 -23
  86. package/dist/components/assets/asset-selection.js.map +1 -1
  87. package/dist/components/assets/asset-thumb.js +6 -5
  88. package/dist/components/assets/asset-thumb.js.map +1 -1
  89. package/dist/components/assets/assets-collection-view-source.d.ts +5 -5
  90. package/dist/components/assets/assets-collection-view-source.js +9 -5
  91. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  92. package/dist/components/assets/assets-module.js +1 -1
  93. package/dist/components/assets/assets-module.js.map +1 -1
  94. package/dist/components/assets/assets-store.d.ts +16 -2
  95. package/dist/components/assets/assets-store.js +28 -6
  96. package/dist/components/assets/assets-store.js.map +1 -1
  97. package/dist/components/assets/assets-view-preference.js +1 -1
  98. package/dist/components/assets/assets-view-preference.js.map +1 -1
  99. package/dist/components/assets/replace-file.d.ts +22 -0
  100. package/dist/components/assets/replace-file.js +59 -0
  101. package/dist/components/assets/replace-file.js.map +1 -0
  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 +15 -27
  106. package/dist/components/builder/blocks-model.js +38 -44
  107. package/dist/components/builder/blocks-model.js.map +1 -1
  108. package/dist/components/builder/blocks-panel.d.ts +8 -4
  109. package/dist/components/builder/blocks-panel.js +10 -9
  110. package/dist/components/builder/blocks-panel.js.map +1 -1
  111. package/dist/components/builder/builder-store.d.ts +104 -15
  112. package/dist/components/builder/builder-store.js +157 -32
  113. package/dist/components/builder/builder-store.js.map +1 -1
  114. package/dist/components/builder/canvas-body.d.ts +30 -0
  115. package/dist/components/builder/canvas-body.js +38 -0
  116. package/dist/components/builder/canvas-body.js.map +1 -0
  117. package/dist/components/builder/canvas-breakpoints.js +1 -1
  118. package/dist/components/builder/canvas-breakpoints.js.map +1 -1
  119. package/dist/components/builder/canvas-chrome-css.js +7 -0
  120. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  121. package/dist/components/builder/canvas-dnd.d.ts +9 -9
  122. package/dist/components/builder/canvas-dnd.js +6 -6
  123. package/dist/components/builder/canvas-dnd.js.map +1 -1
  124. package/dist/components/builder/canvas-inline-editing.d.ts +1 -1
  125. package/dist/components/builder/canvas-inline-editing.js.map +1 -1
  126. package/dist/components/builder/canvas-overlay-geometry.d.ts +1 -1
  127. package/dist/components/builder/canvas-overlay-geometry.js.map +1 -1
  128. package/dist/components/builder/canvas-render-context.d.ts +28 -20
  129. package/dist/components/builder/canvas-render-context.js +46 -47
  130. package/dist/components/builder/canvas-render-context.js.map +1 -1
  131. package/dist/components/builder/canvas-runs.d.ts +45 -0
  132. package/dist/components/builder/canvas-runs.js +51 -0
  133. package/dist/components/builder/canvas-runs.js.map +1 -0
  134. package/dist/components/builder/canvas-theme-notice.js +2 -2
  135. package/dist/components/builder/canvas-theme-notice.js.map +1 -1
  136. package/dist/components/builder/canvas.js +35 -8
  137. package/dist/components/builder/canvas.js.map +1 -1
  138. package/dist/components/builder/clipboard-actions.d.ts +16 -1
  139. package/dist/components/builder/clipboard-actions.js +48 -11
  140. package/dist/components/builder/clipboard-actions.js.map +1 -1
  141. package/dist/components/builder/composition-field-editor-lazy.js +1 -1
  142. package/dist/components/builder/composition-field-editor-lazy.js.map +1 -1
  143. package/dist/components/builder/composition-field-editor.d.ts +4 -16
  144. package/dist/components/builder/composition-field-editor.js +32 -43
  145. package/dist/components/builder/composition-field-editor.js.map +1 -1
  146. package/dist/components/builder/delete-key.d.ts +5 -0
  147. package/dist/components/builder/delete-key.js +4 -0
  148. package/dist/components/builder/delete-key.js.map +1 -1
  149. package/dist/components/builder/drag-session.d.ts +1 -1
  150. package/dist/components/builder/drag-session.js.map +1 -1
  151. package/dist/components/builder/fixture-thumbnail.js +1 -1
  152. package/dist/components/builder/fixture-thumbnail.js.map +1 -1
  153. package/dist/components/builder/insert-context.d.ts +18 -26
  154. package/dist/components/builder/insert-context.js +47 -94
  155. package/dist/components/builder/insert-context.js.map +1 -1
  156. package/dist/components/builder/layout-inspector.js +1 -1
  157. package/dist/components/builder/layout-inspector.js.map +1 -1
  158. package/dist/components/builder/node-title.js +1 -1
  159. package/dist/components/builder/node-title.js.map +1 -1
  160. package/dist/components/builder/page-tree.d.ts +4 -4
  161. package/dist/components/builder/page-tree.js +8 -8
  162. package/dist/components/builder/page-tree.js.map +1 -1
  163. package/dist/components/builder/placement-refusal.d.ts +18 -0
  164. package/dist/components/builder/placement-refusal.js +46 -0
  165. package/dist/components/builder/placement-refusal.js.map +1 -0
  166. package/dist/components/builder/property-inspector.js +8 -4
  167. package/dist/components/builder/property-inspector.js.map +1 -1
  168. package/dist/components/builder/reuse-context.js +68 -30
  169. package/dist/components/builder/reuse-context.js.map +1 -1
  170. package/dist/components/builder/reuse.d.ts +79 -1
  171. package/dist/components/builder/reuse.js +70 -3
  172. package/dist/components/builder/reuse.js.map +1 -1
  173. package/dist/components/builder/rich-text-toolbar.js +1 -1
  174. package/dist/components/builder/rich-text-toolbar.js.map +1 -1
  175. package/dist/components/builder/run-inspector.d.ts +11 -0
  176. package/dist/components/builder/run-inspector.js +33 -0
  177. package/dist/components/builder/run-inspector.js.map +1 -0
  178. package/dist/components/builder/selection-overlay.js +3 -3
  179. package/dist/components/builder/selection-overlay.js.map +1 -1
  180. package/dist/components/builder/spacing-handles.d.ts +2 -2
  181. package/dist/components/builder/spacing-handles.js +3 -3
  182. package/dist/components/builder/spacing-handles.js.map +1 -1
  183. package/dist/components/cms/asset-field.d.ts +38 -1
  184. package/dist/components/cms/asset-field.js +184 -60
  185. package/dist/components/cms/asset-field.js.map +1 -1
  186. package/dist/components/cms/asset-picker-core.d.ts +16 -0
  187. package/dist/components/cms/asset-picker-core.js +20 -0
  188. package/dist/components/cms/asset-picker-core.js.map +1 -1
  189. package/dist/components/cms/asset-picker.js +14 -20
  190. package/dist/components/cms/asset-picker.js.map +1 -1
  191. package/dist/components/cms/bulk-actions.d.ts +4 -20
  192. package/dist/components/cms/bulk-actions.js +5 -26
  193. package/dist/components/cms/bulk-actions.js.map +1 -1
  194. package/dist/components/cms/bulk-bar.d.ts +2 -2
  195. package/dist/components/cms/bulk-bar.js +2 -2
  196. package/dist/components/cms/bulk-bar.js.map +1 -1
  197. package/dist/components/cms/cell-edit.d.ts +1 -1
  198. package/dist/components/cms/cell-edit.js +1 -1
  199. package/dist/components/cms/cell-edit.js.map +1 -1
  200. package/dist/components/cms/cell.d.ts +1 -1
  201. package/dist/components/cms/cell.js +3 -3
  202. package/dist/components/cms/cell.js.map +1 -1
  203. package/dist/components/cms/cms-index.js +2 -2
  204. package/dist/components/cms/cms-index.js.map +1 -1
  205. package/dist/components/cms/cms-labels.js +2 -2
  206. package/dist/components/cms/cms-labels.js.map +1 -1
  207. package/dist/components/cms/cms-store.d.ts +7 -6
  208. package/dist/components/cms/cms-store.js +23 -43
  209. package/dist/components/cms/cms-store.js.map +1 -1
  210. package/dist/components/cms/collection-table.js +7 -7
  211. package/dist/components/cms/collection-table.js.map +1 -1
  212. package/dist/components/cms/detail-editor.d.ts +5 -2
  213. package/dist/components/cms/detail-editor.js +20 -15
  214. package/dist/components/cms/detail-editor.js.map +1 -1
  215. package/dist/components/cms/details-pane.js +1 -1
  216. package/dist/components/cms/details-pane.js.map +1 -1
  217. package/dist/components/cms/doc-state.js +1 -1
  218. package/dist/components/cms/doc-state.js.map +1 -1
  219. package/dist/components/cms/document-diff-view.d.ts +3 -1
  220. package/dist/components/cms/document-diff-view.js +17 -12
  221. package/dist/components/cms/document-diff-view.js.map +1 -1
  222. package/dist/components/cms/document-editor-context.js +1 -1
  223. package/dist/components/cms/document-editor-context.js.map +1 -1
  224. package/dist/components/cms/document-staging.js +1 -1
  225. package/dist/components/cms/document-staging.js.map +1 -1
  226. package/dist/components/cms/document-url-path.js +2 -2
  227. package/dist/components/cms/document-url-path.js.map +1 -1
  228. package/dist/components/cms/duplicate-document.d.ts +1 -1
  229. package/dist/components/cms/duplicate-document.js.map +1 -1
  230. package/dist/components/cms/editable-cell.js +3 -3
  231. package/dist/components/cms/editable-cell.js.map +1 -1
  232. package/dist/components/cms/field-anchors.d.ts +7 -4
  233. package/dist/components/cms/field-anchors.js +9 -7
  234. package/dist/components/cms/field-anchors.js.map +1 -1
  235. package/dist/components/cms/field-collaboration.d.ts +1 -1
  236. package/dist/components/cms/field-collaboration.js +1 -1
  237. package/dist/components/cms/field-collaboration.js.map +1 -1
  238. package/dist/components/cms/field-connection.d.ts +1 -1
  239. package/dist/components/cms/field-connection.js +2 -2
  240. package/dist/components/cms/field-connection.js.map +1 -1
  241. package/dist/components/cms/field-control-lib.d.ts +15 -1
  242. package/dist/components/cms/field-control-lib.js +23 -4
  243. package/dist/components/cms/field-control-lib.js.map +1 -1
  244. package/dist/components/cms/field-control.js +4 -4
  245. package/dist/components/cms/field-control.js.map +1 -1
  246. package/dist/components/cms/field-renderer.d.ts +1 -1
  247. package/dist/components/cms/field-renderer.js +2 -2
  248. package/dist/components/cms/field-renderer.js.map +1 -1
  249. package/dist/components/cms/filter-builder.d.ts +1 -1
  250. package/dist/components/cms/filter-builder.js +3 -3
  251. package/dist/components/cms/filter-builder.js.map +1 -1
  252. package/dist/components/cms/gate-validation.d.ts +21 -30
  253. package/dist/components/cms/gate-validation.js +11 -32
  254. package/dist/components/cms/gate-validation.js.map +1 -1
  255. package/dist/components/cms/import-export.js +2 -2
  256. package/dist/components/cms/import-export.js.map +1 -1
  257. package/dist/components/cms/issue-copy.js +1 -1
  258. package/dist/components/cms/issue-copy.js.map +1 -1
  259. package/dist/components/cms/issues-pane.js +3 -3
  260. package/dist/components/cms/issues-pane.js.map +1 -1
  261. package/dist/components/cms/object-array-field.js +6 -6
  262. package/dist/components/cms/object-array-field.js.map +1 -1
  263. package/dist/components/cms/prop-fields.d.ts +7 -2
  264. package/dist/components/cms/prop-fields.js +14 -7
  265. package/dist/components/cms/prop-fields.js.map +1 -1
  266. package/dist/components/cms/reference-confirm.d.ts +1 -1
  267. package/dist/components/cms/reference-confirm.js +2 -2
  268. package/dist/components/cms/reference-confirm.js.map +1 -1
  269. package/dist/components/cms/relation-picker.js +2 -2
  270. package/dist/components/cms/relation-picker.js.map +1 -1
  271. package/dist/components/cms/rich-text/block-style.js +1 -1
  272. package/dist/components/cms/rich-text/block-style.js.map +1 -1
  273. package/dist/components/cms/rich-text/component-embed.js +4 -4
  274. package/dist/components/cms/rich-text/component-embed.js.map +1 -1
  275. package/dist/components/cms/rich-text/editor.js +3 -3
  276. package/dist/components/cms/rich-text/editor.js.map +1 -1
  277. package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
  278. package/dist/components/cms/rich-text/embed-picker.js +3 -3
  279. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  280. package/dist/components/cms/rich-text/embed-props.js +3 -3
  281. package/dist/components/cms/rich-text/embed-props.js.map +1 -1
  282. package/dist/components/cms/rich-text/extensions.js +1 -1
  283. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  284. package/dist/components/cms/rich-text/font-size-control.d.ts +1 -1
  285. package/dist/components/cms/rich-text/font-size-control.js +2 -2
  286. package/dist/components/cms/rich-text/font-size-control.js.map +1 -1
  287. package/dist/components/cms/rich-text/insert.d.ts +20 -9
  288. package/dist/components/cms/rich-text/insert.js +25 -17
  289. package/dist/components/cms/rich-text/insert.js.map +1 -1
  290. package/dist/components/cms/rich-text/lazy.js +1 -1
  291. package/dist/components/cms/rich-text/lazy.js.map +1 -1
  292. package/dist/components/cms/rich-text/link-control.js +2 -2
  293. package/dist/components/cms/rich-text/link-control.js.map +1 -1
  294. package/dist/components/cms/rich-text/slash-command.js +3 -3
  295. package/dist/components/cms/rich-text/slash-command.js.map +1 -1
  296. package/dist/components/cms/rich-text/toolbar.js +1 -1
  297. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  298. package/dist/components/cms/rich-text/unknown-node.js +2 -2
  299. package/dist/components/cms/rich-text/unknown-node.js.map +1 -1
  300. package/dist/components/cms/rich-text-preview.js +2 -2
  301. package/dist/components/cms/rich-text-preview.js.map +1 -1
  302. package/dist/components/cms/row-actions.d.ts +1 -1
  303. package/dist/components/cms/row-actions.js.map +1 -1
  304. package/dist/components/cms/scalar-value-editor.d.ts +1 -1
  305. package/dist/components/cms/scalar-value-editor.js +2 -2
  306. package/dist/components/cms/scalar-value-editor.js.map +1 -1
  307. package/dist/components/cms/use-discard-controller.d.ts +1 -1
  308. package/dist/components/cms/use-discard-controller.js +1 -1
  309. package/dist/components/cms/use-discard-controller.js.map +1 -1
  310. package/dist/components/cms/use-document-draft.d.ts +7 -1
  311. package/dist/components/cms/use-document-draft.js +16 -2
  312. package/dist/components/cms/use-document-draft.js.map +1 -1
  313. package/dist/components/cms/use-document-versions.js +1 -1
  314. package/dist/components/cms/use-document-versions.js.map +1 -1
  315. package/dist/components/cms/use-field-focus.d.ts +1 -1
  316. package/dist/components/cms/use-field-focus.js +2 -2
  317. package/dist/components/cms/use-field-focus.js.map +1 -1
  318. package/dist/components/cms/use-live-url.d.ts +1 -1
  319. package/dist/components/cms/use-live-url.js +3 -3
  320. package/dist/components/cms/use-live-url.js.map +1 -1
  321. package/dist/components/cms/use-publish-controller.d.ts +2 -3
  322. package/dist/components/cms/use-publish-controller.js +8 -20
  323. package/dist/components/cms/use-publish-controller.js.map +1 -1
  324. package/dist/components/cms/use-published-perspective.d.ts +1 -1
  325. package/dist/components/cms/use-published-perspective.js.map +1 -1
  326. package/dist/components/cms/use-restore-version.d.ts +1 -1
  327. package/dist/components/cms/use-restore-version.js +1 -1
  328. package/dist/components/cms/use-restore-version.js.map +1 -1
  329. package/dist/components/cms/version-compare-lazy.js +1 -1
  330. package/dist/components/cms/version-compare-lazy.js.map +1 -1
  331. package/dist/components/cms/version-compare.js +1 -1
  332. package/dist/components/cms/version-compare.js.map +1 -1
  333. package/dist/components/cms/versions-pane.js +11 -4
  334. package/dist/components/cms/versions-pane.js.map +1 -1
  335. package/dist/components/cms/view-bar.js +3 -3
  336. package/dist/components/cms/view-bar.js.map +1 -1
  337. package/dist/components/collection-view/cms-collection-view-source.d.ts +1 -1
  338. package/dist/components/collection-view/cms-collection-view-source.js +5 -3
  339. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  340. package/dist/components/collection-view/collection-view-context.d.ts +1 -1
  341. package/dist/components/collection-view/collection-view-context.js +1 -1
  342. package/dist/components/collection-view/collection-view-context.js.map +1 -1
  343. package/dist/components/collection-view/collection-view-surface.d.ts +13 -0
  344. package/dist/components/collection-view/collection-view-surface.js +18 -0
  345. package/dist/components/collection-view/collection-view-surface.js.map +1 -0
  346. package/dist/components/components/component-detail.js +10 -10
  347. package/dist/components/components/component-detail.js.map +1 -1
  348. package/dist/components/components/component-preview.d.ts +2 -2
  349. package/dist/components/components/component-preview.js +5 -5
  350. package/dist/components/components/component-preview.js.map +1 -1
  351. package/dist/components/components/component-status.d.ts +1 -1
  352. package/dist/components/components/component-status.js +1 -1
  353. package/dist/components/components/component-status.js.map +1 -1
  354. package/dist/components/components/components-index.js +2 -2
  355. package/dist/components/components/components-index.js.map +1 -1
  356. package/dist/components/components/field-inspector.js +1 -1
  357. package/dist/components/components/field-inspector.js.map +1 -1
  358. package/dist/components/components/registry-browser.js +3 -3
  359. package/dist/components/components/registry-browser.js.map +1 -1
  360. package/dist/components/components/taxonomy.d.ts +1 -1
  361. package/dist/components/components/taxonomy.js.map +1 -1
  362. package/dist/components/components/transfer-dialog.js +4 -3
  363. package/dist/components/components/transfer-dialog.js.map +1 -1
  364. package/dist/components/components/transfer.d.ts +10 -10
  365. package/dist/components/components/transfer.js +13 -12
  366. package/dist/components/components/transfer.js.map +1 -1
  367. package/dist/components/current-user-menu.js +1 -1
  368. package/dist/components/current-user-menu.js.map +1 -1
  369. package/dist/components/delivery-health-banner.js +2 -2
  370. package/dist/components/delivery-health-banner.js.map +1 -1
  371. package/dist/components/deployment-setup-gate.js +3 -3
  372. package/dist/components/deployment-setup-gate.js.map +1 -1
  373. package/dist/components/dev-login-badge.js +2 -2
  374. package/dist/components/dev-login-badge.js.map +1 -1
  375. package/dist/components/guards.js +1 -1
  376. package/dist/components/guards.js.map +1 -1
  377. package/dist/components/inspector/document-panes.d.ts +54 -0
  378. package/dist/components/inspector/document-panes.js +41 -0
  379. package/dist/components/inspector/document-panes.js.map +1 -0
  380. package/dist/components/inspector/inspector-sidebar.js +1 -1
  381. package/dist/components/inspector/inspector-sidebar.js.map +1 -1
  382. package/dist/components/nav-issues-registry.d.ts +53 -0
  383. package/dist/components/nav-issues-registry.js +69 -0
  384. package/dist/components/nav-issues-registry.js.map +1 -0
  385. package/dist/components/nav-issues.js +4 -4
  386. package/dist/components/nav-issues.js.map +1 -1
  387. package/dist/components/operation-error.d.ts +1 -1
  388. package/dist/components/operation-error.js +4 -4
  389. package/dist/components/operation-error.js.map +1 -1
  390. package/dist/components/playground-badge.js +2 -2
  391. package/dist/components/playground-badge.js.map +1 -1
  392. package/dist/components/project-list.js +3 -3
  393. package/dist/components/project-list.js.map +1 -1
  394. package/dist/components/project-session.d.ts +2 -2
  395. package/dist/components/project-session.js +5 -5
  396. package/dist/components/project-session.js.map +1 -1
  397. package/dist/components/project-switcher.js +2 -2
  398. package/dist/components/project-switcher.js.map +1 -1
  399. package/dist/components/rail-store-status.js +1 -1
  400. package/dist/components/rail-store-status.js.map +1 -1
  401. package/dist/components/rail-view-source.d.ts +6 -5
  402. package/dist/components/rail-view-source.js +4 -3
  403. package/dist/components/rail-view-source.js.map +1 -1
  404. package/dist/components/records/record-lens.d.ts +51 -0
  405. package/dist/components/records/record-lens.js +156 -0
  406. package/dist/components/records/record-lens.js.map +1 -0
  407. package/dist/components/records/record-rail.d.ts +86 -0
  408. package/dist/components/records/record-rail.js +241 -0
  409. package/dist/components/records/record-rail.js.map +1 -0
  410. package/dist/components/redirects/redirects-schema.js +1 -1
  411. package/dist/components/redirects/redirects-schema.js.map +1 -1
  412. package/dist/components/references/references-panel.js +2 -2
  413. package/dist/components/references/references-panel.js.map +1 -1
  414. package/dist/components/section-nav.js +27 -11
  415. package/dist/components/section-nav.js.map +1 -1
  416. package/dist/components/section-state.js +2 -2
  417. package/dist/components/section-state.js.map +1 -1
  418. package/dist/components/section-surface.js +2 -2
  419. package/dist/components/section-surface.js.map +1 -1
  420. package/dist/components/session-expired-overlay.js +3 -3
  421. package/dist/components/session-expired-overlay.js.map +1 -1
  422. package/dist/components/settings-form.js +1 -1
  423. package/dist/components/settings-form.js.map +1 -1
  424. package/dist/components/sign-in-needed-badge.js +1 -1
  425. package/dist/components/sign-in-needed-badge.js.map +1 -1
  426. package/dist/components/sign-in-view.d.ts +2 -2
  427. package/dist/components/sign-in-view.js +5 -5
  428. package/dist/components/sign-in-view.js.map +1 -1
  429. package/dist/components/studio-context.js +3 -3
  430. package/dist/components/studio-context.js.map +1 -1
  431. package/dist/components/studio-project-screen.js +30 -3
  432. package/dist/components/studio-project-screen.js.map +1 -1
  433. package/dist/components/studio-shell.js +6 -5
  434. package/dist/components/studio-shell.js.map +1 -1
  435. package/dist/components/theme-context.d.ts +1 -1
  436. package/dist/components/theme-context.js +2 -2
  437. package/dist/components/theme-context.js.map +1 -1
  438. package/dist/components/use-hydration-phase.d.ts +1 -1
  439. package/dist/components/use-hydration-phase.js +1 -1
  440. package/dist/components/use-hydration-phase.js.map +1 -1
  441. package/dist/components/users/invite-user-bar.d.ts +27 -0
  442. package/dist/components/users/invite-user-bar.js +25 -9
  443. package/dist/components/users/invite-user-bar.js.map +1 -1
  444. package/dist/components/users/role-policy-drift-issues.d.ts +38 -0
  445. package/dist/components/users/role-policy-drift-issues.js +71 -0
  446. package/dist/components/users/role-policy-drift-issues.js.map +1 -0
  447. package/dist/components/users/users-collection-view-source.d.ts +1 -1
  448. package/dist/components/users/users-collection-view-source.js +4 -2
  449. package/dist/components/users/users-collection-view-source.js.map +1 -1
  450. package/dist/components/users/users-module.js +1 -1
  451. package/dist/components/users/users-module.js.map +1 -1
  452. package/dist/components/users/users-schema.d.ts +10 -1
  453. package/dist/components/users/users-schema.js +45 -25
  454. package/dist/components/users/users-schema.js.map +1 -1
  455. package/dist/components/users/users-store.js +3 -3
  456. package/dist/components/users/users-store.js.map +1 -1
  457. package/dist/components/workspace-projects.d.ts +1 -1
  458. package/dist/components/workspace-projects.js +1 -1
  459. package/dist/components/workspace-projects.js.map +1 -1
  460. package/dist/lib/ai/apply-content.js +56 -116
  461. package/dist/lib/ai/apply-content.js.map +1 -1
  462. package/dist/lib/ai/chat-context.d.ts +8 -0
  463. package/dist/lib/ai/chat-context.js +37 -6
  464. package/dist/lib/ai/chat-context.js.map +1 -1
  465. package/dist/lib/ai/chat-state.d.ts +6 -0
  466. package/dist/lib/ai/chat-state.js.map +1 -1
  467. package/dist/lib/ai/constraints.d.ts +20 -5
  468. package/dist/lib/ai/constraints.js +125 -33
  469. package/dist/lib/ai/constraints.js.map +1 -1
  470. package/dist/lib/ai/fake-provider.d.ts +16 -3
  471. package/dist/lib/ai/fake-provider.js +22 -6
  472. package/dist/lib/ai/fake-provider.js.map +1 -1
  473. package/dist/lib/ai/index.d.ts +3 -1
  474. package/dist/lib/ai/index.js +3 -1
  475. package/dist/lib/ai/index.js.map +1 -1
  476. package/dist/lib/ai/leash.d.ts +34 -0
  477. package/dist/lib/ai/leash.js +28 -0
  478. package/dist/lib/ai/leash.js.map +1 -0
  479. package/dist/lib/ai/node-handler.d.ts +1 -1
  480. package/dist/lib/ai/node-handler.js.map +1 -1
  481. package/dist/lib/ai/rich-text-input.d.ts +8 -8
  482. package/dist/lib/ai/rich-text-input.js +2 -1
  483. package/dist/lib/ai/rich-text-input.js.map +1 -1
  484. package/dist/lib/ai/rich-text-translate.d.ts +27 -4
  485. package/dist/lib/ai/rich-text-translate.js +28 -10
  486. package/dist/lib/ai/rich-text-translate.js.map +1 -1
  487. package/dist/lib/ai/shape.d.ts +13 -13
  488. package/dist/lib/ai/shape.js +69 -93
  489. package/dist/lib/ai/shape.js.map +1 -1
  490. package/dist/lib/ai/tools.d.ts +36 -41
  491. package/dist/lib/ai/tools.js +83 -87
  492. package/dist/lib/ai/tools.js.map +1 -1
  493. package/dist/lib/ai/translate-document.d.ts +32 -4
  494. package/dist/lib/ai/translate-document.js +145 -47
  495. package/dist/lib/ai/translate-document.js.map +1 -1
  496. package/dist/lib/ai/types.d.ts +34 -23
  497. package/dist/lib/ai/types.js.map +1 -1
  498. package/dist/lib/ai/update-fields.d.ts +15 -12
  499. package/dist/lib/ai/update-fields.js +112 -188
  500. package/dist/lib/ai/update-fields.js.map +1 -1
  501. package/dist/lib/ai/writer-form.d.ts +116 -0
  502. package/dist/lib/ai/writer-form.js +429 -0
  503. package/dist/lib/ai/writer-form.js.map +1 -0
  504. package/dist/lib/assets/upload.d.ts +31 -2
  505. package/dist/lib/assets/upload.js +61 -6
  506. package/dist/lib/assets/upload.js.map +1 -1
  507. package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
  508. package/dist/lib/clipboard/os-clipboard.js +8 -4
  509. package/dist/lib/clipboard/os-clipboard.js.map +1 -1
  510. package/dist/lib/clipboard/paste-issues.js +1 -1
  511. package/dist/lib/clipboard/paste-issues.js.map +1 -1
  512. package/dist/lib/clipboard/paste-target.d.ts +18 -9
  513. package/dist/lib/clipboard/paste-target.js +10 -9
  514. package/dist/lib/clipboard/paste-target.js.map +1 -1
  515. package/dist/lib/cms/field-patch.d.ts +0 -10
  516. package/dist/lib/cms/field-patch.js +4 -30
  517. package/dist/lib/cms/field-patch.js.map +1 -1
  518. package/dist/lib/cms/published-documents.js +1 -1
  519. package/dist/lib/cms/published-documents.js.map +1 -1
  520. package/dist/lib/cms-views/view-model.d.ts +9 -9
  521. package/dist/lib/cms-views/view-model.js +2 -2
  522. package/dist/lib/cms-views/view-model.js.map +1 -1
  523. package/dist/lib/collection-view/collection-view-source.d.ts +14 -5
  524. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  525. package/dist/lib/data/collection-content.js +3 -2
  526. package/dist/lib/data/collection-content.js.map +1 -1
  527. package/dist/lib/data/content-session-refresh.d.ts +1 -1
  528. package/dist/lib/data/content-session-refresh.js.map +1 -1
  529. package/dist/lib/data/playground-unload-guard.d.ts +1 -1
  530. package/dist/lib/data/playground-unload-guard.js +1 -1
  531. package/dist/lib/data/playground-unload-guard.js.map +1 -1
  532. package/dist/lib/dnd/drop-rules.d.ts +25 -31
  533. package/dist/lib/dnd/drop-rules.js +53 -59
  534. package/dist/lib/dnd/drop-rules.js.map +1 -1
  535. package/dist/lib/engine-version.d.ts +1 -1
  536. package/dist/lib/engine-version.js +1 -1
  537. package/dist/lib/engine-version.js.map +1 -1
  538. package/dist/lib/i18n/i18n.js +1 -1
  539. package/dist/lib/i18n/i18n.js.map +1 -1
  540. package/dist/lib/i18n/messages.de.js +44 -3
  541. package/dist/lib/i18n/messages.de.js.map +1 -1
  542. package/dist/lib/i18n/messages.en.d.ts +38 -2
  543. package/dist/lib/i18n/messages.en.js +51 -3
  544. package/dist/lib/i18n/messages.en.js.map +1 -1
  545. package/dist/lib/operations/project-runtime-config.d.ts +34 -6
  546. package/dist/lib/operations/project-runtime-config.js +50 -10
  547. package/dist/lib/operations/project-runtime-config.js.map +1 -1
  548. package/dist/lib/operations/publish-with-revision.d.ts +33 -1
  549. package/dist/lib/operations/publish-with-revision.js +32 -0
  550. package/dist/lib/operations/publish-with-revision.js.map +1 -1
  551. package/dist/lib/operations/runtime-validator.d.ts +19 -68
  552. package/dist/lib/operations/runtime-validator.js +12 -138
  553. package/dist/lib/operations/runtime-validator.js.map +1 -1
  554. package/dist/lib/operations/studio-operations.d.ts +17 -2
  555. package/dist/lib/operations/studio-operations.js +18 -11
  556. package/dist/lib/operations/studio-operations.js.map +1 -1
  557. package/dist/lib/persistence/convex-adapter.d.ts +16 -3
  558. package/dist/lib/persistence/convex-adapter.js +83 -47
  559. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  560. package/dist/lib/persistence/index.js +14 -1
  561. package/dist/lib/persistence/index.js.map +1 -1
  562. package/dist/lib/persona/labels.js +4 -2
  563. package/dist/lib/persona/labels.js.map +1 -1
  564. package/dist/lib/query/operations-query.d.ts +1 -1
  565. package/dist/lib/query/operations-query.js +1 -1
  566. package/dist/lib/query/operations-query.js.map +1 -1
  567. package/dist/lib/references/reference-model.js +1 -1
  568. package/dist/lib/references/reference-model.js.map +1 -1
  569. package/dist/lib/references/references-store.js +1 -1
  570. package/dist/lib/references/references-store.js.map +1 -1
  571. package/dist/lib/workspace/index.d.ts +14 -11
  572. package/dist/lib/workspace/index.js +20 -24
  573. package/dist/lib/workspace/index.js.map +1 -1
  574. package/dist/mount.d.ts +28 -0
  575. package/dist/mount.js +27 -0
  576. package/dist/mount.js.map +1 -0
  577. package/dist/router/root.js +10 -10
  578. package/dist/router/root.js.map +1 -1
  579. package/dist/ui/badge.d.ts +1 -1
  580. package/dist/ui/button.d.ts +2 -2
  581. package/package.json +15 -11
  582. package/dist/components/cms/cms-composition-validators.d.ts +0 -16
  583. package/dist/components/cms/cms-composition-validators.js +0 -133
  584. package/dist/components/cms/cms-composition-validators.js.map +0 -1
  585. package/dist/components/cms/publish-clean.d.ts +0 -28
  586. package/dist/components/cms/publish-clean.js +0 -38
  587. package/dist/components/cms/publish-clean.js.map +0 -1
  588. package/dist/components/redirects/redirects-collection-view-source.d.ts +0 -53
  589. package/dist/components/redirects/redirects-collection-view-source.js +0 -146
  590. package/dist/components/redirects/redirects-collection-view-source.js.map +0 -1
  591. package/dist/components/redirects/redirects-module.d.ts +0 -21
  592. package/dist/components/redirects/redirects-module.js +0 -40
  593. package/dist/components/redirects/redirects-module.js.map +0 -1
  594. package/dist/components/redirects/redirects-store.d.ts +0 -68
  595. package/dist/components/redirects/redirects-store.js +0 -132
  596. package/dist/components/redirects/redirects-store.js.map +0 -1
  597. package/dist/lib/assets/asset-placements.d.ts +0 -28
  598. package/dist/lib/assets/asset-placements.js +0 -83
  599. package/dist/lib/assets/asset-placements.js.map +0 -1
@@ -0,0 +1,281 @@
1
+ import { httpActionGeneric, makeFunctionReference } from 'convex/server'
2
+ import type { FunctionReference, PublicHttpAction } from 'convex/server'
3
+ import { v } from 'convex/values'
4
+ import type { GenericId } from 'convex/values'
5
+ import { z } from 'zod'
6
+ import {
7
+ INGEST_MAX_BODY_BYTES,
8
+ INGEST_MAX_UPLOAD_BYTES,
9
+ INGEST_SIGNATURE_HEADER,
10
+ INGEST_UPLOAD_TTL_MS,
11
+ ingestPayloadSchema,
12
+ readIngestReceiverBody,
13
+ verifyIngestSignature,
14
+ } from '@elytracms/core/ingest'
15
+ import type { IngestHandlerResult, IngestPayload, IngestUploadClaim } from '@elytracms/core/ingest'
16
+ import { defineRecordKind, resolveRecordQuery } from '@elytracms/core/persistence'
17
+ import { createUuidIdFactory } from '@elytracms/core/determinism'
18
+ import type { MutationCtx, QueryCtx } from './functions'
19
+ import { findRecordDoc, listRecordWire, saveRecord } from './record-functions'
20
+
21
+ /**
22
+ * EC-617 — the ONE inbound receiver of the public ingest seam (ruling EC-565 § 6c), and the
23
+ * helpers an addon's ingest handler is written with. A factory module: it registers no
24
+ * function itself — `defineStudioHttp({ auth, ingest })` (`http.ts`) mounts
25
+ * {@link ingestReceiver} at `INGEST_RECEIVER_PATH` (`POST /elytra/ingest`, an exact path).
26
+ *
27
+ * The receiver, per request:
28
+ *
29
+ * 1. `ELYTRA_INGEST_SECRET` unset → 401 `{ configured: false }` — closed until configured;
30
+ * 2. body over `INGEST_MAX_BODY_BYTES` → 413;
31
+ * 3. the HMAC (`x-elytra-signature: sha256=…`) verified in constant time with the
32
+ * deployment's secret → 401 on anything else (the site's route signs with the same
33
+ * value, `INGEST_SECRET`);
34
+ * 4. the envelope (`@elytracms/core/ingest`) → 400 with its issues;
35
+ * 5. the doctor's `probe` → 200 `{ ok: true, probe: true }`, nothing dispatched;
36
+ * 6. dispatch by kind to the handler the PROJECT registered — `event` / `submission`, and
37
+ * `upload` to the submission handler (uploads are submission-scoped). A kind with no
38
+ * handler is REFUSED (422, naming the kind) — never dropped: an `event` with no
39
+ * analytics addon must fail visibly (EC-618's counsel gate relies on it);
40
+ * 7. an accepted `upload` gets ONE short-lived storage URL (`ctx.storage.generateUploadUrl`)
41
+ * — the bytes go browser → storage, cross-origin by design; the submission that follows
42
+ * claims the stored file with {@link claimIngestUpload}, which checks it against what the
43
+ * upload request declared.
44
+ *
45
+ * The engine never imports an addon: the handler is an internal-mutation REFERENCE the
46
+ * consumer passes in its one `http.ts` line (`defineStudioHttp({ auth, ingest: {
47
+ * submission: formsIngest } })`), exported by the addon's `./convex` entry.
48
+ */
49
+
50
+ /** What an ingest handler receives: the verified, parsed payload and the receipt time. */
51
+ export const ingestHandlerArgs = { payload: v.any(), receivedAt: v.string() }
52
+
53
+ /** An addon's ingest handler: an INTERNAL mutation (never client-callable). */
54
+ export type IngestHandlerRef = FunctionReference<
55
+ 'mutation',
56
+ 'internal',
57
+ { payload: IngestPayload; receivedAt: string },
58
+ IngestHandlerResult
59
+ >
60
+
61
+ /**
62
+ * Name an addon's ingest handler for `defineStudioHttp({ ingest })` — its internal mutation by
63
+ * its module path (`ingestHandler('forms:ingest')`), the way every engine function is
64
+ * addressed (a string ref resolved against the consumer's deployment, EC-223). An addon
65
+ * exports the result from its `./convex` entry so the consumer writes no string.
66
+ */
67
+ export function ingestHandler(name: `${string}:${string}`): IngestHandlerRef {
68
+ return makeFunctionReference<'mutation'>(name) as unknown as IngestHandlerRef
69
+ }
70
+
71
+ /** The handlers a project registers, by payload kind (`upload` goes to `submission`). */
72
+ export interface IngestHandlers {
73
+ event?: IngestHandlerRef
74
+ submission?: IngestHandlerRef
75
+ }
76
+
77
+ /** Re-validate a handler's `payload` argument (it crossed a function boundary as `v.any()`). */
78
+ export function parseIngestPayload(payload: unknown): IngestPayload {
79
+ return ingestPayloadSchema.parse(payload)
80
+ }
81
+
82
+ function json(status: number, body: Record<string, unknown>): Response {
83
+ return new Response(JSON.stringify(body), {
84
+ status,
85
+ headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
86
+ })
87
+ }
88
+
89
+ function ingestSecret(): string | undefined {
90
+ const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {}
91
+ const value = env['ELYTRA_INGEST_SECRET']?.trim()
92
+ return value ? value : undefined
93
+ }
94
+
95
+ function utf8Length(text: string): number {
96
+ return new TextEncoder().encode(text).byteLength
97
+ }
98
+
99
+ /** The receiver `httpAction` (see the module header). */
100
+ export function ingestReceiver(handlers: IngestHandlers = {}): PublicHttpAction {
101
+ return httpActionGeneric(async (ctx, request) => {
102
+ const secret = ingestSecret()
103
+ if (!secret) return json(401, { error: 'Ingest is not configured on this deployment.', configured: false })
104
+
105
+ const declared = Number(request.headers.get('content-length'))
106
+ if (Number.isFinite(declared) && declared > INGEST_MAX_BODY_BYTES) {
107
+ return json(413, { error: 'The payload is too large.' })
108
+ }
109
+ const raw = await request.text()
110
+ if (utf8Length(raw) > INGEST_MAX_BODY_BYTES) return json(413, { error: 'The payload is too large.' })
111
+
112
+ if (!(await verifyIngestSignature(raw, request.headers.get(INGEST_SIGNATURE_HEADER), secret))) {
113
+ return json(401, { error: 'Invalid or missing ingest signature.', configured: true })
114
+ }
115
+ const read = readIngestReceiverBody(raw)
116
+ if (!read.ok) return json(400, { error: read.message, ...(read.issues ? { issues: read.issues } : {}) })
117
+ const body = read.value
118
+ if (body.kind === 'probe') return json(200, { ok: true, probe: true })
119
+
120
+ const handlerKind = body.kind === 'upload' ? 'submission' : body.kind
121
+ const handler = handlers[handlerKind]
122
+ if (!handler) {
123
+ return json(422, {
124
+ error: `No handler is registered for "${body.kind}" ingest payloads on this deployment — install the addon that handles them and pass its handler to defineStudioHttp({ ingest }).`,
125
+ kind: body.kind,
126
+ handled: false,
127
+ })
128
+ }
129
+ const result = await ctx.runMutation(handler, { payload: body, receivedAt: new Date().toISOString() })
130
+ if (!result.ok) {
131
+ return json(result.status, { error: result.message, ...(result.issues ? { issues: result.issues } : {}) })
132
+ }
133
+ if (body.kind === 'upload') {
134
+ if (!result.uploadId) {
135
+ return json(500, { error: 'The submission handler accepted the upload but recorded no upload intent (recordIngestUpload).' })
136
+ }
137
+ const uploadUrl = await ctx.storage.generateUploadUrl()
138
+ return json(200, { ok: true, uploadId: result.uploadId, uploadUrl, expiresInSeconds: INGEST_UPLOAD_TTL_MS / 1000 })
139
+ }
140
+ return json(200, { ok: true, ...(result.id ? { id: result.id } : {}) })
141
+ })
142
+ }
143
+
144
+ // --- Submission-scoped uploads -------------------------------------------------------------
145
+ //
146
+ // The receiver issues a storage URL only for an upload a handler ACCEPTED and RECORDED as an
147
+ // intent (`recordIngestUpload`); a submission CLAIMS it (`claimIngestUpload`), which checks the
148
+ // stored file against the intent. Abuse is bounded by: the route's limiter and bot gate on every
149
+ // URL issued, `INGEST_MAX_UPLOAD_BYTES` per file, the URL's one-hour life, the post-upload check
150
+ // (an over-size or mistyped file is deleted, never kept), and the unclaimed intents an addon can
151
+ // list (`unclaimedIngestUploads`) and surface. Convex returns the storage id to the uploader
152
+ // only, so the bytes of an upload no submission ever claims cannot be located by the engine —
153
+ // that is the documented residue, bounded by the limits above.
154
+
155
+ /** The upload intent — an engine-internal record kind (no public functions). */
156
+ export const ingestUploadRecordKind = defineRecordKind({
157
+ kind: 'ingestUpload',
158
+ module: 'ingest',
159
+ keys: ['target', 'state'],
160
+ time: 'issuedAt',
161
+ fields: {
162
+ target: z.string().min(1),
163
+ field: z.string().min(1),
164
+ contentType: z.string().min(1),
165
+ size: z.number().int().positive(),
166
+ state: z.enum(['issued', 'claimed', 'rejected']),
167
+ issuedAt: z.string().min(1),
168
+ storageId: z.string().optional(),
169
+ },
170
+ })
171
+
172
+ let mintUploadUuid: (() => string) | undefined
173
+
174
+ export interface IngestUploadPolicy {
175
+ /** Largest accepted file in bytes (≤ `INGEST_MAX_UPLOAD_BYTES`). */
176
+ maxBytes?: number
177
+ /** Accepted content types (exact, or `image/*`-style prefixes). Absent → any. */
178
+ contentTypes?: readonly string[]
179
+ }
180
+
181
+ function typeAllowed(contentType: string, allowed: readonly string[] | undefined): boolean {
182
+ if (!allowed) return true
183
+ return allowed.some((entry) => (entry.endsWith('/*') ? contentType.startsWith(entry.slice(0, -1)) : entry === contentType))
184
+ }
185
+
186
+ /**
187
+ * Accept an `upload` payload under the handler's policy and record its intent. Returns the
188
+ * handler result to hand back to the receiver (`{ ok: true, uploadId }`, or a 413/415
189
+ * refusal). Call it from the submission handler's `upload` branch.
190
+ */
191
+ export async function recordIngestUpload(
192
+ ctx: MutationCtx,
193
+ payload: Extract<IngestPayload, { kind: 'upload' }>,
194
+ receivedAt: string,
195
+ policy: IngestUploadPolicy = {},
196
+ ): Promise<IngestHandlerResult> {
197
+ const maxBytes = Math.min(policy.maxBytes ?? INGEST_MAX_UPLOAD_BYTES, INGEST_MAX_UPLOAD_BYTES)
198
+ if (payload.size > maxBytes) return { ok: false, status: 413, message: `The file is larger than ${maxBytes} bytes.` }
199
+ if (!typeAllowed(payload.contentType, policy.contentTypes)) {
200
+ return { ok: false, status: 415, message: `Files of type “${payload.contentType}” are not accepted here.` }
201
+ }
202
+ // EC-578: the one id factory (UUID v4 from crypto.getRandomValues) — never a clock or a counter.
203
+ mintUploadUuid ??= createUuidIdFactory()
204
+ const uploadId = `upload_${mintUploadUuid()}`
205
+ await saveRecord(
206
+ ctx,
207
+ ingestUploadRecordKind,
208
+ uploadId,
209
+ { ...payloadColumns(payload), state: 'issued', issuedAt: receivedAt },
210
+ { createdAt: receivedAt, updatedAt: receivedAt },
211
+ )
212
+ return { ok: true, uploadId }
213
+ }
214
+
215
+ function payloadColumns(payload: Extract<IngestPayload, { kind: 'upload' }>) {
216
+ return { target: payload.target, field: payload.field, contentType: payload.contentType, size: payload.size }
217
+ }
218
+
219
+ /** What a claim yields: the stored file's facts, or a refusal for the handler to return. */
220
+ export type IngestUploadClaimResult =
221
+ | { ok: true; storageId: string; field: string; contentType: string; size: number }
222
+ | { ok: false; status: 409 | 413 | 415 | 422; message: string }
223
+
224
+ /**
225
+ * Claim one upload for a submission to `target`: the intent must exist, be unclaimed,
226
+ * belong to `target`, be younger than `INGEST_UPLOAD_TTL_MS`, and the STORED file must match
227
+ * it (size ≤ declared, same content type). A mismatched file is deleted and the intent
228
+ * marked `rejected` — never kept.
229
+ */
230
+ export async function claimIngestUpload(
231
+ ctx: MutationCtx,
232
+ claim: IngestUploadClaim,
233
+ target: string,
234
+ receivedAt: string,
235
+ ): Promise<IngestUploadClaimResult> {
236
+ const intent = await findRecordDoc(ctx, ingestUploadRecordKind, claim.uploadId)
237
+ const columns = intent?.data as
238
+ | { target: string; field: string; contentType: string; size: number; state: string; issuedAt: string }
239
+ | undefined
240
+ if (!columns || columns.target !== target) return { ok: false, status: 422, message: `Unknown upload “${claim.uploadId}”.` }
241
+ if (columns.state !== 'issued') return { ok: false, status: 409, message: `Upload “${claim.uploadId}” was already claimed.` }
242
+ if (Date.parse(receivedAt) - Date.parse(columns.issuedAt) > INGEST_UPLOAD_TTL_MS) {
243
+ return { ok: false, status: 422, message: `Upload “${claim.uploadId}” expired.` }
244
+ }
245
+ const storageId = claim.storageId as GenericId<'_storage'>
246
+ const file = await ctx.db.system.get(storageId).catch(() => null)
247
+ const stamps = { createdAt: receivedAt, updatedAt: receivedAt }
248
+ if (!file) return { ok: false, status: 422, message: `No stored file “${claim.storageId}”.` }
249
+ const refused =
250
+ file.size > columns.size
251
+ ? ({ status: 413, message: 'The stored file is larger than the upload declared.' } as const)
252
+ : (file.contentType ?? '') !== columns.contentType
253
+ ? ({ status: 415, message: 'The stored file is not of the declared type.' } as const)
254
+ : undefined
255
+ if (refused) {
256
+ await ctx.storage.delete(storageId)
257
+ await saveRecord(ctx, ingestUploadRecordKind, claim.uploadId, { state: 'rejected', storageId: claim.storageId }, stamps)
258
+ return { ok: false, ...refused }
259
+ }
260
+ await saveRecord(ctx, ingestUploadRecordKind, claim.uploadId, { state: 'claimed', storageId: claim.storageId }, stamps)
261
+ return { ok: true, storageId: claim.storageId, field: columns.field, contentType: columns.contentType, size: file.size }
262
+ }
263
+
264
+ /** Upload intents of `target` still unclaimed past their life — the visible orphan finding. */
265
+ export async function unclaimedIngestUploads(
266
+ ctx: QueryCtx | MutationCtx,
267
+ target: string,
268
+ now: string,
269
+ ): Promise<{ uploadId: string; field: string; issuedAt: string }[]> {
270
+ const before = new Date(Date.parse(now) - INGEST_UPLOAD_TTL_MS).toISOString()
271
+ const rows = await listRecordWire(
272
+ ctx,
273
+ ingestUploadRecordKind,
274
+ resolveRecordQuery(ingestUploadRecordKind, { where: { target, state: 'issued' }, before }),
275
+ )
276
+ return rows.map((row) => ({
277
+ uploadId: String(row[ingestUploadRecordKind.idKey]),
278
+ field: String(row['field']),
279
+ issuedAt: String(row['issuedAt']),
280
+ }))
281
+ }
package/convex/invites.ts CHANGED
@@ -3,6 +3,7 @@ import { selectOrphanCredentialPurges } from '@elytracms/core/persistence'
3
3
  import { mutation, query } from './functions'
4
4
  import type { MutationCtx } from './functions'
5
5
  import { requireCapability } from './guard'
6
+ import { assertAuthorityFloor, assertDeclaredRole } from './authority'
6
7
 
7
8
  /**
8
9
  * Invite allowlist management (EC-324). An admin invites an email + role; the
@@ -11,10 +12,14 @@ import { requireCapability } from './guard'
11
12
  * `requireCapability('project.administer')`, the same trust boundary as
12
13
  * member/user management; UI gating alone is never trusted.
13
14
  *
14
- * The invited `role` is validated against the active policy at the operations
15
- * write boundary (`invites.create` input); this store keeps it a lenient string
16
- * (schema-as-lens, EC-277). Keyed by normalized (lowercased) email; `create`
17
- * upserts so re-inviting the same email replaces the pending role.
15
+ * The invited `role` is validated against the project's policy at the operations
16
+ * write boundary (`invites.create`) AND here, against the DEPLOYED policy (EC-605 —
17
+ * `assertDeclaredRole`): an invite grants its role on consumption, so a role nobody
18
+ * declared must never be stored. Granting it is authority, so the integrity floor's
19
+ * "assign only roles you hold" applies too (`assertAuthorityFloor`, no target yet).
20
+ * The table keeps the role a lenient string (schema-as-lens, EC-277). Keyed by
21
+ * normalized (lowercased) email; `create` upserts so re-inviting the same email
22
+ * replaces the pending role.
18
23
  */
19
24
 
20
25
  const normalizeEmail = (email: string): string => email.trim().toLowerCase()
@@ -109,7 +114,10 @@ export const create = mutation({
109
114
  updatedAt: v.string(),
110
115
  },
111
116
  handler: async (ctx, args) => {
112
- await requireCapability(ctx, 'project.administer')
117
+ const callerId = await requireCapability(ctx, 'project.administer')
118
+ // EC-605: only a declared role, and only one the inviter could grant themselves.
119
+ assertDeclaredRole(args.role)
120
+ await assertAuthorityFloor(ctx, { callerId, rolesGranted: [args.role], removesMembership: false })
113
121
  const email = normalizeEmail(args.email)
114
122
  // EC-358: never adopt orphan credentials — purge member-less auth rows for
115
123
  // this email atomically with the invite, so the invitee sets their own password.
package/convex/members.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { v } from 'convex/values'
2
2
  import { mutation, query } from './functions'
3
3
  import { getOptionalUserId, requireCapability, requireUserId } from './guard'
4
- import { assertAuthorityFloor, recordAuthorityAudit } from './authority'
4
+ import { assertAuthorityFloor, assertDeclaredRole, recordAuthorityAudit } from './authority'
5
5
 
6
6
  /**
7
7
  * Deployment membership persistence (EC-150, single-tenant per EC-179). The
@@ -9,10 +9,13 @@ import { assertAuthorityFloor, recordAuthorityAudit } from './authority'
9
9
  * `ConvexMembersRepository` maps these onto `@elytracms/core/persistence` records.
10
10
  * Member *management* is admin-only, server-side — UI gating alone is never
11
11
  * trusted. One row per user grants their role on this deployment's one project.
12
+ *
13
+ * EC-605: a role is any string the DEPLOYED role policy declares (`./roles`) — the
14
+ * validator accepts a string and `set` refuses an undeclared one visibly
15
+ * (`assertDeclaredRole`), instead of the former literal admin|editor|viewer union,
16
+ * which made a project's own roles unwritable.
12
17
  */
13
18
 
14
- const roleValidator = v.union(v.literal('admin'), v.literal('editor'), v.literal('viewer'))
15
-
16
19
  export const list = query({
17
20
  args: {},
18
21
  handler: async (ctx) => {
@@ -62,7 +65,8 @@ export const mine = query({
62
65
  export const set = mutation({
63
66
  args: {
64
67
  userId: v.string(),
65
- role: roleValidator,
68
+ /** One of the deployed policy's role ids — refused visibly otherwise (EC-605). */
69
+ role: v.string(),
66
70
  /**
67
71
  * Studio-user custom fields (EC-240, 240a). Optional so the member-management
68
72
  * path (`ConvexMembersRepository.setMember`) omits it; the users rail
@@ -75,6 +79,8 @@ export const set = mutation({
75
79
  },
76
80
  handler: async (ctx, args) => {
77
81
  const callerId = await requireCapability(ctx, 'project.administer')
82
+ // EC-605: only a role the deployed policy declares can be stored.
83
+ assertDeclaredRole(args.role)
78
84
  // Integrity floor (EC-240): can't demote the last admin / assign above self.
79
85
  // Throws `unauthorized: …` on denial — a first-class, visible error (verified
80
86
  // live). NOTE (EC-240 H-tail item 6): a denied attempt is NOT audited here. A
@@ -0,0 +1,116 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { convexTest } from 'convex-test'
3
+ import type { ConvexHttpClient } from 'convex/browser'
4
+ import type { FunctionReference } from 'convex/server'
5
+ import type { CollectionDef } from '@elytracms/core/cms-core'
6
+ import { createSequentialIdFactory } from '@elytracms/core/determinism'
7
+ import { createOperations } from '@elytracms/core/operations'
8
+ import { replaceAtCurrentRevision } from '@elytracms/core/persistence'
9
+ import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
10
+ import { loadHydratedCms } from '../src/lib/operations/session-hydration'
11
+ import schema from './schema'
12
+
13
+ /**
14
+ * EC-606 close-out — the orphan count on the CONNECTED path, short of a signed-in
15
+ * connected studio (owner-run).
16
+ *
17
+ * The count itself is mode-independent: `loadHydratedCms` runs over whatever
18
+ * operations client the session has, and `project-session.test.tsx` already proves
19
+ * the content session carries its `orphans` to the rail foot. What only the connected
20
+ * mode adds is the backend between them — the real `cms:listDocuments` handler and the
21
+ * production `ConvexPersistenceAdapter`. This runs exactly that path on convex-test's
22
+ * in-memory backend (the harness of `contract-parity.test.ts`: the adapter's HTTP client
23
+ * is a call-through double onto `t.query` / `t.mutation`): a row of a collection the
24
+ * config does not declare must reach the session's orphan list, with its published
25
+ * count, and must not reach its documents.
26
+ */
27
+
28
+ const modules = {
29
+ ...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
30
+ './_generated/api.ts': async () => ({}),
31
+ }
32
+
33
+ const PROJECT_ID = 'orphan-hydration'
34
+ const SESSION_USER_ID = 'user_admin'
35
+
36
+ const DECLARED: CollectionDef[] = [{ id: 'post', kind: 'document', fields: [{ name: 'title', type: 'text' }] }]
37
+
38
+ interface AdapterBackend {
39
+ query: (ref: FunctionReference<'query'>, args: Record<string, unknown>) => Promise<unknown>
40
+ mutation: (ref: FunctionReference<'mutation'>, args: Record<string, unknown>) => Promise<unknown>
41
+ }
42
+
43
+ function asAdapterClient(backend: AdapterBackend): ConvexHttpClient {
44
+ return {
45
+ query: (ref: FunctionReference<'query'>, args?: Record<string, unknown>) => backend.query(ref, args ?? {}),
46
+ mutation: (ref: FunctionReference<'mutation'>, args?: Record<string, unknown>) => backend.mutation(ref, args ?? {}),
47
+ } as unknown as ConvexHttpClient
48
+ }
49
+
50
+ /** The production adapter over a fresh in-memory Convex backend, as the session admin. */
51
+ async function connectedAdapter() {
52
+ const t = convexTest(schema, modules)
53
+ const now = '2026-10-26T00:00:00.000Z'
54
+ await t.run(async (ctx) => {
55
+ await ctx.db.insert('members', { userId: SESSION_USER_ID, role: 'admin', createdAt: now, updatedAt: now })
56
+ })
57
+ const asAdmin = t.withIdentity({ subject: SESSION_USER_ID, issuer: 'https://convex.test' })
58
+ return new ConvexPersistenceAdapter(asAdapterClient(asAdmin), PROJECT_ID)
59
+ }
60
+
61
+ /** The operations client a headless embedder (or the session) builds over that adapter. */
62
+ function operationsOver(adapter: ConvexPersistenceAdapter) {
63
+ return createOperations({
64
+ adapter,
65
+ collections: () => DECLARED,
66
+ validateDocument: () => [],
67
+ idFactory: createSequentialIdFactory('doc'),
68
+ })
69
+ }
70
+
71
+ describe('EC-606 — orphans reach a connected session through the Convex functions', () => {
72
+ it('a stored row of an undeclared collection is counted (published too), and kept out of the documents', async () => {
73
+ const adapter = await connectedAdapter()
74
+ await replaceAtCurrentRevision(adapter.cms, PROJECT_ID, {
75
+ collection: 'post',
76
+ id: 'kept',
77
+ values: { title: 'Kept' },
78
+ })
79
+ await replaceAtCurrentRevision(adapter.cms, PROJECT_ID, {
80
+ collection: 'cms-data-sources',
81
+ id: 'legacy-draft',
82
+ values: { name: 'left behind by the data-binding subsystem' },
83
+ })
84
+ const published = await replaceAtCurrentRevision(adapter.cms, PROJECT_ID, {
85
+ collection: 'cms-data-sources',
86
+ id: 'legacy-live',
87
+ values: { name: 'and published' },
88
+ })
89
+ const pinned = await adapter.cms.publishDocument(
90
+ PROJECT_ID,
91
+ { collection: 'cms-data-sources', id: 'legacy-live' },
92
+ published.revision,
93
+ )
94
+ expect(pinned.status).toBe('ok')
95
+
96
+ const result = await loadHydratedCms(operationsOver(adapter), PROJECT_ID, DECLARED)
97
+ expect(result.ok).toBe(true)
98
+ if (!result.ok || !result.value) throw new Error('hydration must answer')
99
+ expect(result.value.orphans).toEqual([{ collection: 'cms-data-sources', count: 2, published: 1 }])
100
+ expect(result.value.documents.map((document) => `${document.collection}:${document.id}`)).toEqual(['post:kept'])
101
+ })
102
+
103
+ it('CONTROL: declared rows only → no orphans (the count is not a constant)', async () => {
104
+ const adapter = await connectedAdapter()
105
+ await replaceAtCurrentRevision(adapter.cms, PROJECT_ID, {
106
+ collection: 'post',
107
+ id: 'kept',
108
+ values: { title: 'Kept' },
109
+ })
110
+
111
+ const result = await loadHydratedCms(operationsOver(adapter), PROJECT_ID, DECLARED)
112
+ if (!result.ok || !result.value) throw new Error('hydration must answer')
113
+ expect(result.value.orphans).toEqual([])
114
+ expect(result.value.documents).toHaveLength(1)
115
+ })
116
+ })
@@ -3,7 +3,7 @@ import { convexTest } from 'convex-test'
3
3
  import { makeFunctionReference } from 'convex/server'
4
4
  import type { ConvexHttpClient } from 'convex/browser'
5
5
  import type { FunctionReference } from 'convex/server'
6
- import { InMemoryPersistenceAdapter } from '@elytracms/core/persistence'
6
+ import { InMemoryPersistenceAdapter, assetAlt } from '@elytracms/core/persistence'
7
7
  import type { PersistenceAdapter } from '@elytracms/core/persistence'
8
8
  import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
9
9
  import { updateAssetMetadata } from '../src/components/assets/asset-metadata'
@@ -60,7 +60,6 @@ async function seedHero(adapter: PersistenceAdapter) {
60
60
  byteSize: 10,
61
61
  width: 4000,
62
62
  height: 2000,
63
- alt: 'Harbour',
64
63
  title: 'Hero',
65
64
  focalPoint: { x: 0.2, y: 0.8 },
66
65
  thumb: THUMB,
@@ -69,17 +68,21 @@ async function seedHero(adapter: PersistenceAdapter) {
69
68
  })
70
69
  }
71
70
 
72
- /** "Reset to centre", then clear alt — the two connected clears that never persisted. */
71
+ /**
72
+ * "Reset to centre", then clear alt — the two connected clears that never persisted.
73
+ * EC-587: the alt is cleared the way the options dialog clears it, by writing the bag
74
+ * without it (`meta.alt` is its one home).
75
+ */
73
76
  async function metadataClears(adapter: PersistenceAdapter) {
74
77
  await seedHero(adapter)
75
78
  const assets = adapter.assets
76
79
  const reset = await updateAssetMetadata({ assets }, PROJECT_ID, 'asset_hero', { focalPoint: null })
77
- const clearedAlt = await updateAssetMetadata({ assets }, PROJECT_ID, 'asset_hero', { alt: '' })
80
+ const clearedAlt = await updateAssetMetadata({ assets }, PROJECT_ID, 'asset_hero', { meta: { legacyKey: 'kept' } })
78
81
  expect([reset.ok, clearedAlt.ok]).toEqual([true, true])
79
82
  const after = await adapter.assets.getAsset(PROJECT_ID, 'asset_hero')
80
83
  return {
81
84
  focalPoint: after.focalPoint ?? null,
82
- alt: after.alt ?? null,
85
+ alt: assetAlt(after),
83
86
  title: after.title ?? null,
84
87
  thumb: after.thumb ?? null,
85
88
  lqip: after.lqip ?? null,
@@ -97,7 +100,7 @@ describe('EC-577 — the asset writers mean the same thing on both adapters', ()
97
100
  title: 'Hero',
98
101
  thumb: THUMB,
99
102
  lqip: LQIP,
100
- meta: { alt: 'Harbour', legacyKey: 'kept' },
103
+ meta: { legacyKey: 'kept' },
101
104
  }
102
105
  expect({ memory, convex }).toEqual({ memory: expected, convex: expected })
103
106
  })
@@ -128,7 +131,7 @@ describe('EC-577 — the asset writers mean the same thing on both adapters', ()
128
131
  async get() {
129
132
  // An editor saves alt while the backfill is fetching this original — the
130
133
  // backfill's write lands after it, from a listing taken before it.
131
- await updateAssetMetadata({ assets: adapter.assets }, PROJECT_ID, 'asset_big', { alt: 'Mid-run alt' })
134
+ await updateAssetMetadata({ assets: adapter.assets }, PROJECT_ID, 'asset_big', { meta: { alt: 'Mid-run alt' } })
132
135
  return new Uint8Array([1])
133
136
  },
134
137
  }
@@ -142,7 +145,7 @@ describe('EC-577 — the asset writers mean the same thing on both adapters', ()
142
145
  )
143
146
  expect(progress.generated).toBe(1)
144
147
  const after = await adapter.assets.getAsset(PROJECT_ID, 'asset_big')
145
- expect([after.alt, after.thumb, after.lqip]).toEqual(['Mid-run alt', THUMB, LQIP])
148
+ expect([assetAlt(after), after.thumb, after.lqip]).toEqual(['Mid-run alt', THUMB, LQIP])
146
149
  }
147
150
  })
148
151
  })