@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
@@ -135,8 +135,12 @@ describe('EC-510 — the write chokepoint revalidates on every redirect write',
135
135
  // is the gate working rather than the instrument being blind.
136
136
  expect(await scheduledRevalidations(t)).toEqual([])
137
137
  // The write itself still happened — silence is at the webhook, not the save.
138
+ // EC-616: a redirect is stored as a `records` row of kind `redirect` now.
138
139
  expect(
139
- await t.run(async (ctx) => (await ctx.db.query('redirects').collect()).length),
140
+ await t.run(
141
+ async (ctx) =>
142
+ (await ctx.db.query('records').collect()).filter((row) => row.kind === 'redirect').length,
143
+ ),
140
144
  ).toBe(1)
141
145
  })
142
146
  })
@@ -1,92 +1,133 @@
1
- import { ConvexError, v } from 'convex/values'
2
- import type { DocumentByName, WithoutSystemFields } from 'convex/server'
3
- import { redirectRowSchema, rejectedWriteMessage, resolveRecordWrite } from '@elytracms/core/persistence'
4
- import type { DataModel } from './functions'
5
- import { mutation, query } from './functions'
6
- import { requireCapability } from './guard'
7
-
8
- type RedirectDoc = DocumentByName<DataModel, 'redirects'>
1
+ import { internalMutationGeneric } from 'convex/server'
2
+ import type { MutationBuilder } from 'convex/server'
3
+ import {
4
+ PersistenceValidationError,
5
+ recordColumnsOf,
6
+ recordSlots,
7
+ redirectRecordKind,
8
+ resolveRecordQuery,
9
+ resolveRecordSave,
10
+ } from '@elytracms/core/persistence'
11
+ import type { RedirectEntry } from '@elytracms/core/persistence'
12
+ import type { DataModel, MutationCtx, QueryCtx } from './functions'
13
+ import { defineRecordFunctions, findRecordDoc, listRecordWire } from './record-functions'
14
+ import type { LegacyRecord, LegacyRecordSource } from './record-functions'
15
+ import { recordEntryFromWire } from '@elytracms/core/persistence'
9
16
 
10
17
  /**
11
- * Editorial redirects persistence (EC-303). Redirects are data an editor owns —
12
- * saved+enabled entries go live dynamically (no deploy) via the delivery
13
- * snapshot. Reachable only through the app's `ConvexRedirectRepository` adapter.
14
- * Content-gated (a viewer reads, an editor writes), NOT admin-only — redirects
15
- * are content, not administration.
18
+ * Editorial redirects persistence (EC-303) — since EC-616 the redirect RECORD KIND over
19
+ * the generic record store (`redirectRecordKind`, one `records` row per redirect). The
20
+ * functions keep their names and wire (`redirects:save/get/list/remove`, `redirectId` +
21
+ * the columns flat), so the studio's adapter and every consumer's
22
+ * `export * from '@elytracms/studio/convex/redirects'` line are unchanged. Content-gated
23
+ * (a viewer reads, an editor writes), NOT admin-only — redirects are content.
24
+ *
25
+ * ## The move from the `redirects` table (stored data — KON and NK hold real redirects)
26
+ *
27
+ * The EC-587 precedent, with no gap:
28
+ *
29
+ * - **writes go to `records`** (kind `redirect`); nothing writes the legacy table again;
30
+ * - **reads union the legacy rows no record shadows** — the TOLERANT READ below, marked:
31
+ * a deployment with these functions pushed and the migration NOT yet run serves the
32
+ * same redirects it served before (`list`, `get`, the delivery snapshot);
33
+ * - a save of a legacy-only redirect lifts it (record from its columns, legacy row
34
+ * deleted, one transaction); a remove deletes both;
35
+ * - **`redirects:migrateLegacy`** moves every legacy row in one idempotent internal
36
+ * mutation and reports counts — run per deployment (both consumers + the prod harness:
37
+ * owner-run); the legacy table and this tolerant read are dropped together once it is
38
+ * reported run everywhere (a gated item, like EC-587's alt column).
16
39
  */
17
40
 
18
- /**
19
- * Create or modify a redirect (EC-577). The ONE write rule (`resolveRecordWrite`,
20
- * shared with the in-memory adapter) runs here, inside the transaction: a column
21
- * absent from the args keeps its stored value — `permanent`/`enabled` included,
22
- * whose defaults (the record schema's `.default(true)`) apply only when this save
23
- * creates the redirect — `null` clears the optional `meta` bag, a value sets. ONE
24
- * `db.patch` or ONE `db.insert`, so the EC-510 revalidation trigger fires once.
25
- */
26
- export const save = mutation({
27
- args: {
28
- redirectId: v.string(),
29
- from: v.optional(v.string()),
30
- to: v.optional(v.string()),
31
- permanent: v.optional(v.boolean()),
32
- enabled: v.optional(v.boolean()),
33
- // Schema-driven additive-field bag (EC-311 Tier 2): present replaces the bag,
34
- // `null` clears it, absent keeps it.
35
- meta: v.optional(v.any()),
36
- createdAt: v.string(),
37
- updatedAt: v.string(),
38
- },
39
- handler: async (ctx, { redirectId, createdAt, updatedAt, ...write }) => {
40
- await requireCapability(ctx, 'content.write')
41
- const existing = await ctx.db
42
- .query('redirects')
43
- .withIndex('by_redirect', (q) => q.eq('redirectId', redirectId))
44
- .first()
45
- const resolved = resolveRecordWrite(redirectRowSchema, existing ?? undefined, write)
46
- if (!resolved.ok) {
47
- throw new ConvexError({
48
- code: 'validation',
49
- message: rejectedWriteMessage('redirect', redirectId, resolved.issues),
50
- issues: resolved.issues,
51
- })
52
- }
53
- if (existing) {
54
- await ctx.db.patch(existing._id, { ...(resolved.patch as Partial<RedirectDoc>), updatedAt })
55
- return ctx.db.get(existing._id)
56
- }
57
- const row: WithoutSystemFields<RedirectDoc> = { ...resolved.row, redirectId, createdAt, updatedAt }
58
- const id = await ctx.db.insert('redirects', row)
59
- return ctx.db.get(id)
60
- },
61
- })
41
+ type LegacyRedirectDoc = DataModel['redirects']['document']
62
42
 
63
- export const get = query({
64
- args: { redirectId: v.string() },
65
- handler: async (ctx, args) => {
66
- await requireCapability(ctx, 'content.read')
67
- return ctx.db
43
+ /** TOLERANT READ (marked) — the legacy `redirects` table as the kind's legacy source. */
44
+ const legacyRedirects: LegacyRecordSource = {
45
+ find: async (ctx, recordId) => {
46
+ const row = await ctx.db
68
47
  .query('redirects')
69
- .withIndex('by_redirect', (q) => q.eq('redirectId', args.redirectId))
48
+ .withIndex('by_redirect', (q) => q.eq('redirectId', recordId))
70
49
  .first()
50
+ return row ? legacyRecord(row) : null
71
51
  },
72
- })
52
+ list: async (ctx) => (await ctx.db.query('redirects').withIndex('by_redirect').collect()).map(legacyRecord),
53
+ remove: async (ctx, record) => {
54
+ await ctx.db.delete(record.handle as LegacyRedirectDoc['_id'])
55
+ },
56
+ }
57
+
58
+ function legacyRecord(row: LegacyRedirectDoc): LegacyRecord {
59
+ return {
60
+ recordId: row.redirectId,
61
+ columns: recordColumnsOf(redirectRecordKind, row),
62
+ createdAt: row.createdAt,
63
+ updatedAt: row.updatedAt,
64
+ handle: row._id,
65
+ }
66
+ }
73
67
 
74
- export const list = query({
68
+ export const { save, get, list, remove } = defineRecordFunctions(redirectRecordKind, { legacy: legacyRedirects })
69
+
70
+ /**
71
+ * Every stored redirect of this deployment (records + the tolerant legacy read), ordered
72
+ * by `from` — the ONE server-side reader; the delivery snapshot projects it through
73
+ * `liveRedirectRecords` (EC-590 / EC-627: one enabled-redirect reader).
74
+ */
75
+ export async function loadRedirectEntries(ctx: QueryCtx, projectId: string): Promise<RedirectEntry[]> {
76
+ const rows = await listRecordWire(ctx, redirectRecordKind, resolveRecordQuery(redirectRecordKind), legacyRedirects)
77
+ return rows.map((row) => recordEntryFromWire(redirectRecordKind, row, projectId))
78
+ }
79
+
80
+ /**
81
+ * EC-616 — move every legacy `redirects` row into `records` (kind `redirect`), the
82
+ * redirect move's migration. Per row: a record of the same id already exists (a save
83
+ * lifted it, or an earlier run) → the legacy row is dropped as shadowed; otherwise the
84
+ * row's columns are written as a record CREATE through the one rule (`resolveRecordSave`
85
+ * — a legacy row the current schema refuses is REPORTED and left where it is, still
86
+ * served by the tolerant read, never dropped silently) and the legacy row deleted.
87
+ * Idempotent: a second run scans nothing.
88
+ *
89
+ * Internal: an admin runs it from the CLI, once per deployment —
90
+ * `npx convex run redirects:migrateLegacy` (no args). Built on the RAW internal-mutation
91
+ * builder, deliberately outside the write-chokepoint triggers: the served redirect set is
92
+ * identical before and after by construction (the tolerant read), so the move needs no
93
+ * revalidation — through the triggers it would schedule two sweeps per row (an insert
94
+ * into `records`, a delete from `redirects`). One transaction: Convex bounds it at 8192
95
+ * written rows, far above any editorial redirect set (KON's import holds 192).
96
+ */
97
+ export const migrateLegacy = (internalMutationGeneric as MutationBuilder<DataModel, 'internal'>)({
75
98
  args: {},
76
99
  handler: async (ctx) => {
77
- await requireCapability(ctx, 'content.read')
78
- return ctx.db.query('redirects').withIndex('by_redirect').collect()
79
- },
80
- })
81
-
82
- export const remove = mutation({
83
- args: { redirectId: v.string() },
84
- handler: async (ctx, args) => {
85
- await requireCapability(ctx, 'content.write')
86
- const existing = await ctx.db
87
- .query('redirects')
88
- .withIndex('by_redirect', (q) => q.eq('redirectId', args.redirectId))
89
- .first()
90
- if (existing) await ctx.db.delete(existing._id)
100
+ const legacy = await ctx.db.query('redirects').withIndex('by_redirect').collect()
101
+ const moved: string[] = []
102
+ const shadowed: string[] = []
103
+ const refused: Array<{ redirectId: string; message: string }> = []
104
+ for (const row of legacy) {
105
+ const record = legacyRecord(row)
106
+ if (await findRecordDoc(ctx as unknown as MutationCtx, redirectRecordKind, record.recordId)) {
107
+ await ctx.db.delete(row._id)
108
+ shadowed.push(record.recordId)
109
+ continue
110
+ }
111
+ let columns: Record<string, unknown>
112
+ try {
113
+ columns = resolveRecordSave(redirectRecordKind, record.recordId, undefined, record.columns)
114
+ } catch (error) {
115
+ if (!(error instanceof PersistenceValidationError)) throw error
116
+ refused.push({ redirectId: record.recordId, message: error.message })
117
+ continue
118
+ }
119
+ const slots = recordSlots(redirectRecordKind, columns, record.createdAt)
120
+ await ctx.db.insert('records', {
121
+ kind: redirectRecordKind.kind,
122
+ recordId: record.recordId,
123
+ data: columns,
124
+ at: slots.at,
125
+ createdAt: record.createdAt,
126
+ updatedAt: record.updatedAt,
127
+ })
128
+ await ctx.db.delete(row._id)
129
+ moved.push(record.recordId)
130
+ }
131
+ return { scanned: legacy.length, moved: moved.length, shadowed: shadowed.length, refused, movedIds: moved }
91
132
  },
92
133
  })
@@ -0,0 +1,315 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { convexTest } from 'convex-test'
3
+ import type { ConvexHttpClient } from 'convex/browser'
4
+ import { makeFunctionReference } from 'convex/server'
5
+ import type { FunctionReference } from 'convex/server'
6
+ import type { GenericId } from 'convex/values'
7
+ import {
8
+ ROLE_POLICY_ENV,
9
+ defaultRolePolicy,
10
+ rolePolicyFingerprint,
11
+ serializeRolePolicy,
12
+ } from '@elytracms/core/persistence'
13
+ import type { RolePolicy } from '@elytracms/core/persistence'
14
+ import { resolveProject } from '@elytracms/core/studio-core'
15
+ import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
16
+ import { gateSessionCreation } from './auth'
17
+ import { requireCapability } from './guard'
18
+ import { deployedRolePolicy } from './roles'
19
+ import schema from './schema'
20
+
21
+ /**
22
+ * EC-605 — the Convex trust boundary reads the project's role policy from the copy the
23
+ * CLI provisions into `ELYTRA_ROLE_POLICY`, and only that copy.
24
+ *
25
+ * The REAL `members` / `invites` / `roles` handlers and the REAL guard + integrity floor
26
+ * run on convex-test's in-memory backend; the env is stubbed per test (set, unset,
27
+ * malformed). Two member rows hold DIFFERENT custom roles, so a decision that silently
28
+ * fell back to the default policy (where both roles are unknown) is visible.
29
+ */
30
+
31
+ const modules = {
32
+ ...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
33
+ './_generated/api.ts': async () => ({}),
34
+ }
35
+
36
+ const ISSUER = 'https://convex.test'
37
+
38
+ /** The fourth-role proof: a `reviewer` that reads, between viewer and editor. */
39
+ const FOUR_ROLE: RolePolicy = {
40
+ roles: [
41
+ { id: 'viewer', label: 'Viewer', capabilities: ['content.read'] },
42
+ { id: 'reviewer', label: 'Reviewer', capabilities: ['content.read'] },
43
+ { id: 'editor', label: 'Editor', capabilities: ['content.read', 'content.write'] },
44
+ { id: 'admin', label: 'Admin', capabilities: ['content.read', 'content.write', 'project.administer'] },
45
+ ],
46
+ }
47
+
48
+ /** An administering role BELOW the strongest — what the floor's rank invariant guards. */
49
+ const RANKED: RolePolicy = {
50
+ roles: [
51
+ { id: 'author', capabilities: ['content.read', 'content.write'] },
52
+ { id: 'manager', capabilities: ['content.read', 'content.write', 'project.administer'] },
53
+ { id: 'owner', capabilities: ['content.read', 'content.write', 'project.administer'] },
54
+ ],
55
+ }
56
+
57
+ const membersSet = makeFunctionReference<'mutation'>('members:set')
58
+ const membersList = makeFunctionReference<'query'>('members:list')
59
+ const invitesCreate = makeFunctionReference<'mutation'>('invites:create')
60
+ const fingerprintQuery = makeFunctionReference<'query'>('roles:fingerprint')
61
+
62
+ const NOW = '2026-10-26T00:00:00.000Z'
63
+
64
+ async function backendWith(rows: Array<{ userId: string; role: string }>) {
65
+ const t = convexTest(schema, modules)
66
+ await t.run(async (ctx) => {
67
+ for (const row of rows) {
68
+ await ctx.db.insert('members', { ...row, createdAt: NOW, updatedAt: NOW })
69
+ }
70
+ })
71
+ return t
72
+ }
73
+
74
+ const as = (t: ReturnType<typeof convexTest>, userId: string) =>
75
+ t.withIdentity({ subject: userId, issuer: ISSUER })
76
+
77
+ /** Run the guard itself as `userId` — the one primitive every engine handler gates on. */
78
+ async function canDo(
79
+ t: ReturnType<typeof convexTest>,
80
+ userId: string,
81
+ capability: 'content.read' | 'content.write' | 'project.administer',
82
+ ): Promise<boolean> {
83
+ try {
84
+ await as(t, userId).run((ctx) => requireCapability(ctx, capability))
85
+ return true
86
+ } catch {
87
+ return false
88
+ }
89
+ }
90
+
91
+ beforeEach(() => {
92
+ vi.unstubAllEnvs()
93
+ })
94
+ afterEach(() => {
95
+ vi.unstubAllEnvs()
96
+ })
97
+
98
+ describe('the deployed role policy (EC-605) — absent → the default, byte-identical', () => {
99
+ it('reads the default policy when ELYTRA_ROLE_POLICY is unset or blank', () => {
100
+ expect(deployedRolePolicy({})).toBe(defaultRolePolicy)
101
+ expect(deployedRolePolicy({ [ROLE_POLICY_ENV]: ' ' })).toBe(defaultRolePolicy)
102
+ })
103
+
104
+ it('keeps the historical tiers with no env: viewer reads, editor writes, admin administers', async () => {
105
+ const t = await backendWith([
106
+ { userId: 'u_viewer', role: 'viewer' },
107
+ { userId: 'u_editor', role: 'editor' },
108
+ { userId: 'u_admin', role: 'admin' },
109
+ ])
110
+ expect(await canDo(t, 'u_viewer', 'content.read')).toBe(true)
111
+ expect(await canDo(t, 'u_viewer', 'content.write')).toBe(false)
112
+ expect(await canDo(t, 'u_editor', 'content.write')).toBe(true)
113
+ expect(await canDo(t, 'u_editor', 'project.administer')).toBe(false)
114
+ expect(await canDo(t, 'u_admin', 'project.administer')).toBe(true)
115
+ })
116
+
117
+ it('refuses to store a role the default policy does not declare (members:set, invites:create)', async () => {
118
+ const t = await backendWith([{ userId: 'u_admin', role: 'admin' }])
119
+ await expect(
120
+ as(t, 'u_admin').mutation(membersSet, { userId: 'u_x', role: 'reviewer', createdAt: NOW, updatedAt: NOW }),
121
+ ).rejects.toThrow(/invalid-role: role "reviewer" is not declared/)
122
+ await expect(
123
+ as(t, 'u_admin').mutation(invitesCreate, {
124
+ email: 'x@example.com',
125
+ role: 'reviewer',
126
+ createdAt: NOW,
127
+ updatedAt: NOW,
128
+ }),
129
+ ).rejects.toThrow(/invalid-role/)
130
+ // Control: a declared role is stored.
131
+ const stored = await as(t, 'u_admin').mutation(membersSet, {
132
+ userId: 'u_x',
133
+ role: 'editor',
134
+ createdAt: NOW,
135
+ updatedAt: NOW,
136
+ })
137
+ expect(stored).toMatchObject({ userId: 'u_x', role: 'editor' })
138
+ })
139
+
140
+ it('answers the default fingerprint, sourced from the default', async () => {
141
+ const t = convexTest(schema, modules)
142
+ expect(await t.query(fingerprintQuery, {})).toEqual({
143
+ status: 'ok',
144
+ fingerprint: rolePolicyFingerprint(defaultRolePolicy),
145
+ source: 'default',
146
+ })
147
+ })
148
+ })
149
+
150
+ describe('a provisioned four-role policy — honoured by the guard with two custom-role members', () => {
151
+ beforeEach(() => {
152
+ vi.stubEnv(ROLE_POLICY_ENV, serializeRolePolicy(FOUR_ROLE))
153
+ })
154
+
155
+ it('stores the custom role (members:set) and the guard grants by it', async () => {
156
+ const t = await backendWith([{ userId: 'u_admin', role: 'admin' }])
157
+ for (const [userId, role] of [
158
+ ['u_reviewer', 'reviewer'],
159
+ ['u_editor', 'editor'],
160
+ ] as const) {
161
+ const stored = await as(t, 'u_admin').mutation(membersSet, { userId, role, createdAt: NOW, updatedAt: NOW })
162
+ expect(stored).toMatchObject({ userId, role })
163
+ }
164
+ // The reviewer reads (a real handler) but cannot write; the editor writes.
165
+ expect(await as(t, 'u_reviewer').query(membersList, {})).toHaveLength(3)
166
+ expect(await canDo(t, 'u_reviewer', 'content.read')).toBe(true)
167
+ expect(await canDo(t, 'u_reviewer', 'content.write')).toBe(false)
168
+ expect(await canDo(t, 'u_editor', 'content.write')).toBe(true)
169
+ expect(await canDo(t, 'u_editor', 'project.administer')).toBe(false)
170
+ })
171
+
172
+ it('control: without the env the same reviewer row grants NOTHING (unknown role fails closed)', async () => {
173
+ vi.unstubAllEnvs()
174
+ const t = await backendWith([{ userId: 'u_reviewer', role: 'reviewer' }])
175
+ expect(await canDo(t, 'u_reviewer', 'content.read')).toBe(false)
176
+ })
177
+
178
+ it('answers the provisioned policy’s fingerprint, sourced from the env', async () => {
179
+ const t = convexTest(schema, modules)
180
+ expect(await t.query(fingerprintQuery, {})).toEqual({
181
+ status: 'ok',
182
+ fingerprint: rolePolicyFingerprint(FOUR_ROLE),
183
+ source: 'env',
184
+ })
185
+ expect(rolePolicyFingerprint(FOUR_ROLE)).not.toBe(rolePolicyFingerprint(defaultRolePolicy))
186
+ })
187
+
188
+ it('stores a custom role through the production adapter unchanged (no silent viewer floor)', async () => {
189
+ const t = await backendWith([{ userId: 'u_admin', role: 'admin' }])
190
+ const asAdmin = as(t, 'u_admin')
191
+ const client = {
192
+ query: (ref: FunctionReference<'query'>, args?: Record<string, unknown>) => asAdmin.query(ref, args ?? {}),
193
+ mutation: (ref: FunctionReference<'mutation'>, args?: Record<string, unknown>) =>
194
+ asAdmin.mutation(ref, args ?? {}),
195
+ } as unknown as ConvexHttpClient
196
+ // users:get joins the auth `users` row; give the member an identity row first.
197
+ const userId = await t.run(async (ctx) => {
198
+ const id = await ctx.db.insert('users', { email: 'rev@example.com' })
199
+ return String(id)
200
+ })
201
+ const adapter = new ConvexPersistenceAdapter(client, 'role-policy')
202
+ await adapter.users.saveUser('role-policy', { id: userId, roles: ['reviewer'] })
203
+ const row = await t.run((ctx) =>
204
+ ctx.db
205
+ .query('members')
206
+ .withIndex('by_user', (q) => q.eq('userId', userId))
207
+ .first(),
208
+ )
209
+ expect(row?.role).toBe('reviewer')
210
+ })
211
+ })
212
+
213
+ describe('the integrity floor under a ranked custom policy (an administering role below the strongest)', () => {
214
+ beforeEach(() => {
215
+ vi.stubEnv(ROLE_POLICY_ENV, serializeRolePolicy(RANKED))
216
+ })
217
+
218
+ it('a manager assigns roles up to its own rank, never above (members:set and invites:create)', async () => {
219
+ const t = await backendWith([
220
+ { userId: 'u_owner', role: 'owner' },
221
+ { userId: 'u_manager', role: 'manager' },
222
+ ])
223
+ const manager = as(t, 'u_manager')
224
+ await expect(
225
+ manager.mutation(membersSet, { userId: 'u_new', role: 'author', createdAt: NOW, updatedAt: NOW }),
226
+ ).resolves.toMatchObject({ role: 'author' })
227
+ await expect(
228
+ manager.mutation(membersSet, { userId: 'u_new', role: 'manager', createdAt: NOW, updatedAt: NOW }),
229
+ ).resolves.toMatchObject({ role: 'manager' })
230
+ await expect(
231
+ manager.mutation(membersSet, { userId: 'u_new', role: 'owner', createdAt: NOW, updatedAt: NOW }),
232
+ ).rejects.toThrow(/cannot assign the “owner” role/)
233
+ await expect(
234
+ manager.mutation(invitesCreate, { email: 'boss@example.com', role: 'owner', createdAt: NOW, updatedAt: NOW }),
235
+ ).rejects.toThrow(/cannot assign the “owner” role/)
236
+ // Control: the owner (strongest) may invite an owner.
237
+ await expect(
238
+ as(t, 'u_owner').mutation(invitesCreate, { email: 'boss@example.com', role: 'owner', createdAt: NOW, updatedAt: NOW }),
239
+ ).resolves.toMatchObject({ role: 'owner' })
240
+ })
241
+
242
+ it('counts every administering role for "the last administrator" — demoting one of two is allowed, the last is not', async () => {
243
+ const t = await backendWith([
244
+ { userId: 'u_owner', role: 'owner' },
245
+ { userId: 'u_manager', role: 'manager' },
246
+ ])
247
+ // Two administrators: the owner demotes the manager to author.
248
+ await expect(
249
+ as(t, 'u_owner').mutation(membersSet, { userId: 'u_manager', role: 'author', createdAt: NOW, updatedAt: NOW }),
250
+ ).resolves.toMatchObject({ role: 'author' })
251
+ // Now the owner is the last administrator: demoting themselves is refused.
252
+ await expect(
253
+ as(t, 'u_owner').mutation(membersSet, { userId: 'u_owner', role: 'manager', createdAt: NOW, updatedAt: NOW }),
254
+ ).resolves.toMatchObject({ role: 'manager' })
255
+ await expect(
256
+ as(t, 'u_owner').mutation(membersSet, { userId: 'u_owner', role: 'author', createdAt: NOW, updatedAt: NOW }),
257
+ ).rejects.toThrow(/last admin/)
258
+ })
259
+
260
+ it('bootstraps the first user into the strongest role (owner), not a literal "admin"', async () => {
261
+ const t = convexTest(schema, modules)
262
+ const userId = await t.run((ctx) => ctx.db.insert('users', { email: 'first@example.com' }))
263
+ await t.run((ctx) => gateSessionCreation(ctx as never, { userId: userId as GenericId<'users'> }))
264
+ const row = await t.run((ctx) => ctx.db.query('members').first())
265
+ expect(row?.role).toBe('owner')
266
+ })
267
+ })
268
+
269
+ describe('a malformed or invalid deployed copy throws — never a silent default', () => {
270
+ it('every guarded function fails, naming ELYTRA_ROLE_POLICY', async () => {
271
+ vi.stubEnv(ROLE_POLICY_ENV, '{not json')
272
+ const t = await backendWith([{ userId: 'u_admin', role: 'admin' }])
273
+ await expect(as(t, 'u_admin').query(membersList, {})).rejects.toThrow(/ELYTRA_ROLE_POLICY is not valid JSON/)
274
+ })
275
+
276
+ it('an invalid policy (no administering role) is refused with the located rule', () => {
277
+ const invalid = JSON.stringify({ v: 1, roles: [{ id: 'viewer', capabilities: ['content.read'] }] })
278
+ expect(() => deployedRolePolicy({ [ROLE_POLICY_ENV]: invalid })).toThrow(/No role grants “project.administer”/)
279
+ })
280
+
281
+ it('the fingerprint query answers `invalid` with the reason instead of throwing', async () => {
282
+ vi.stubEnv(ROLE_POLICY_ENV, '{"v":2,"roles":[]}')
283
+ const t = convexTest(schema, modules)
284
+ const answer = (await t.query(fingerprintQuery, {})) as { status: string; detail: string }
285
+ expect(answer.status).toBe('invalid')
286
+ expect(answer.detail).toMatch(/ELYTRA_ROLE_POLICY is not a serialised role policy/)
287
+ })
288
+
289
+ it('a changed env is read on the next call (the parse is cached per string, not per process)', () => {
290
+ const four = serializeRolePolicy(FOUR_ROLE)
291
+ expect(deployedRolePolicy({ [ROLE_POLICY_ENV]: four }).roles.map((r) => r.id)).toContain('reviewer')
292
+ expect(deployedRolePolicy({ [ROLE_POLICY_ENV]: serializeRolePolicy(RANKED) }).roles.map((r) => r.id)).toEqual([
293
+ 'author',
294
+ 'manager',
295
+ 'owner',
296
+ ])
297
+ })
298
+ })
299
+
300
+ describe('one declaration, two readers — the config entry reaches the guard through the provisioned copy', () => {
301
+ it('resolveProject → serializeRolePolicy → ELYTRA_ROLE_POLICY → the guard grants the declared reviewer', async () => {
302
+ const resolved = resolveProject({
303
+ name: 'Acme',
304
+ slug: 'acme',
305
+ locales: { default: 'en', locales: ['en'] },
306
+ roles: FOUR_ROLE.roles.map((role) => ({ ...role, capabilities: [...role.capabilities] })),
307
+ })
308
+ expect(resolved.ok).toBe(true)
309
+ if (!resolved.ok) return
310
+ vi.stubEnv(ROLE_POLICY_ENV, serializeRolePolicy(resolved.project.roles!))
311
+ const t = await backendWith([{ userId: 'u_reviewer', role: 'reviewer' }])
312
+ expect(await canDo(t, 'u_reviewer', 'content.read')).toBe(true)
313
+ expect(await canDo(t, 'u_reviewer', 'content.write')).toBe(false)
314
+ })
315
+ })
@@ -0,0 +1,83 @@
1
+ import {
2
+ ROLE_POLICY_ENV,
3
+ defaultRolePolicy,
4
+ parseRolePolicy,
5
+ rolePolicyFingerprint,
6
+ } from '@elytracms/core/persistence'
7
+ import type { RolePolicy } from '@elytracms/core/persistence'
8
+ import { query } from './functions'
9
+
10
+ /**
11
+ * The role policy this deployment ENFORCES (EC-605) — the Convex trust boundary's
12
+ * reader of the project's `roles` declaration.
13
+ *
14
+ * Convex functions cannot import `elytra.config.ts`, and the nine engine modules that
15
+ * gate on a capability import `./guard` inside the package, so the declaration reaches
16
+ * the deployment as a serialised copy in its env: `ELYTRA_ROLE_POLICY`, provisioned by
17
+ * the `elytra` CLI from the resolved project (`elytra init`, and `elytra build` before
18
+ * every Convex deploy). `guard.ts`, `authority.ts`, `members.ts`, `invites.ts` and the
19
+ * first-user bootstrap (`auth.ts`) all read it through {@link deployedRolePolicy}.
20
+ *
21
+ * - **Absent (or blank)** → the shipped default policy, byte-identical to before EC-605.
22
+ * - **Present but malformed or invalid** → THROWS, naming every reason
23
+ * (`parseRolePolicy`): every guarded function then fails loudly rather than enforcing
24
+ * a policy nobody declared. Never a silent default.
25
+ *
26
+ * The env is read per CALL (a provisioned change takes effect without a redeploy) and
27
+ * the parse is cached per serialised string, so the steady state costs one string
28
+ * comparison.
29
+ *
30
+ * The public {@link fingerprint} query is how a drift between this copy and the config
31
+ * becomes visible: `elytra doctor` and the studio compare it with the config's own
32
+ * fingerprint. Consumers reach it through a one-line re-export (`convex/roles.ts`); the
33
+ * readers above work without it.
34
+ */
35
+
36
+ let cached: { readonly serialized: string; readonly policy: RolePolicy } | undefined
37
+
38
+ /** The deployment env, reached via `globalThis` so this module needs no Node types at runtime. */
39
+ function deploymentEnv(): Record<string, string | undefined> {
40
+ return (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {}
41
+ }
42
+
43
+ /**
44
+ * The deployed role policy: the parsed `ELYTRA_ROLE_POLICY`, or the default when it is
45
+ * unset. Throws on a malformed or invalid value (see the module comment). `env` is
46
+ * injectable for tests.
47
+ */
48
+ export function deployedRolePolicy(env: Record<string, string | undefined> = deploymentEnv()): RolePolicy {
49
+ const serialized = env[ROLE_POLICY_ENV]
50
+ if (serialized === undefined || serialized.trim() === '') return defaultRolePolicy
51
+ if (cached?.serialized === serialized) return cached.policy
52
+ const policy = parseRolePolicy(serialized)
53
+ cached = { serialized, policy }
54
+ return policy
55
+ }
56
+
57
+ /** What {@link fingerprint} answers — the deployed policy's identity, never its content. */
58
+ export type DeployedRolePolicyFingerprint =
59
+ | { readonly status: 'ok'; readonly fingerprint: string; readonly source: 'env' | 'default' }
60
+ | { readonly status: 'invalid'; readonly detail: string }
61
+
62
+ /**
63
+ * PUBLIC, unauthenticated: the fingerprint of the role policy this deployment enforces
64
+ * (`rolePolicyFingerprint` — 16 hex chars over ids, rank and capabilities; labels and
65
+ * the policy itself never leave the deployment) and whether it comes from
66
+ * `ELYTRA_ROLE_POLICY` or the default. `elytra doctor` reads it without a session (the
67
+ * doctor's env rule — names only, values never printed — stands: this is not a value),
68
+ * and the studio reads it at boot. A malformed or invalid deployed copy answers
69
+ * `invalid` with the located reasons instead of throwing, so the drift check can say
70
+ * exactly what is wrong.
71
+ */
72
+ export const fingerprint = query({
73
+ args: {},
74
+ handler: async (): Promise<DeployedRolePolicyFingerprint> => {
75
+ const env = deploymentEnv()
76
+ const source = env[ROLE_POLICY_ENV]?.trim() ? 'env' : 'default'
77
+ try {
78
+ return { status: 'ok', fingerprint: rolePolicyFingerprint(deployedRolePolicy(env)), source }
79
+ } catch (error) {
80
+ return { status: 'invalid', detail: error instanceof Error ? error.message : String(error) }
81
+ }
82
+ },
83
+ })