@elytracms/studio 0.0.93 → 0.0.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (541) hide show
  1. package/convex/analytics-cost.test.ts +817 -0
  2. package/convex/asset-alt-migration.test.ts +2 -2
  3. package/convex/asset-meta-patch.test.ts +218 -0
  4. package/convex/asset-replace-delete-race.test.ts +127 -0
  5. package/convex/assets.ts +27 -2
  6. package/convex/auth.ts +5 -2
  7. package/convex/authority.ts +35 -11
  8. package/convex/contract-parity.test.ts +7 -0
  9. package/convex/delivery-redirects.test.ts +167 -0
  10. package/convex/delivery.ts +23 -10
  11. package/convex/guard.ts +9 -7
  12. package/convex/http-routes.test.ts +18 -8
  13. package/convex/http.ts +23 -17
  14. package/convex/ingest.test.ts +326 -0
  15. package/convex/ingest.ts +281 -0
  16. package/convex/invites.ts +13 -5
  17. package/convex/members.ts +10 -4
  18. package/convex/rail-write-parity.test.ts +6 -6
  19. package/convex/record-functions.ts +325 -0
  20. package/convex/records.test.ts +156 -0
  21. package/convex/redirect-migration.test.ts +205 -0
  22. package/convex/redirect-revalidate.test.ts +5 -1
  23. package/convex/redirects.ts +120 -79
  24. package/convex/role-policy.test.ts +315 -0
  25. package/convex/roles.ts +83 -0
  26. package/convex/schema.ts +36 -0
  27. package/convex/triggers.ts +21 -1
  28. package/dist/addon.d.ts +71 -0
  29. package/dist/addon.js +67 -0
  30. package/dist/addon.js.map +1 -0
  31. package/dist/components/account/account-settings.js +2 -2
  32. package/dist/components/account/account-settings.js.map +1 -1
  33. package/dist/components/account/credential-panel.js +2 -2
  34. package/dist/components/account/credential-panel.js.map +1 -1
  35. package/dist/components/account/self-account-schema.d.ts +6 -15
  36. package/dist/components/account/self-account-schema.js +52 -34
  37. package/dist/components/account/self-account-schema.js.map +1 -1
  38. package/dist/components/account/self-account-view-source.d.ts +1 -1
  39. package/dist/components/account/self-account-view-source.js +6 -4
  40. package/dist/components/account/self-account-view-source.js.map +1 -1
  41. package/dist/components/ai/ai-chat-panel-lazy.js +1 -1
  42. package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
  43. package/dist/components/ai/ai-chat-panel.js +6 -6
  44. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  45. package/dist/components/ai/ai-cms-panel.js +2 -2
  46. package/dist/components/ai/ai-cms-panel.js.map +1 -1
  47. package/dist/components/ai/ai-panel-toggle.js +4 -4
  48. package/dist/components/ai/ai-panel-toggle.js.map +1 -1
  49. package/dist/components/ai/chat-ui.js +2 -2
  50. package/dist/components/ai/chat-ui.js.map +1 -1
  51. package/dist/components/ai/proposal-card.d.ts +2 -2
  52. package/dist/components/ai/proposal-card.js +2 -2
  53. package/dist/components/ai/proposal-card.js.map +1 -1
  54. package/dist/components/ai/proposal-issues.js +2 -2
  55. package/dist/components/ai/proposal-issues.js.map +1 -1
  56. package/dist/components/ai/translate-document-action.js +2 -2
  57. package/dist/components/ai/translate-document-action.js.map +1 -1
  58. package/dist/components/ai/translate-document-dialog.js +6 -6
  59. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  60. package/dist/components/ai/translate-skipped.d.ts +1 -1
  61. package/dist/components/ai/translate-skipped.js +2 -2
  62. package/dist/components/ai/translate-skipped.js.map +1 -1
  63. package/dist/components/app-views/default-modules.d.ts +50 -2
  64. package/dist/components/app-views/default-modules.js +65 -20
  65. package/dist/components/app-views/default-modules.js.map +1 -1
  66. package/dist/components/app-views/module.d.ts +53 -3
  67. package/dist/components/app-views/module.js +14 -7
  68. package/dist/components/app-views/module.js.map +1 -1
  69. package/dist/components/app-views/sidebar.js +1 -1
  70. package/dist/components/app-views/sidebar.js.map +1 -1
  71. package/dist/components/app-views/studio-config.d.ts +11 -2
  72. package/dist/components/app-views/studio-config.js.map +1 -1
  73. package/dist/components/assets/asset-detail.js +28 -13
  74. package/dist/components/assets/asset-detail.js.map +1 -1
  75. package/dist/components/assets/asset-grid.js +2 -2
  76. package/dist/components/assets/asset-grid.js.map +1 -1
  77. package/dist/components/assets/asset-locale.d.ts +9 -0
  78. package/dist/components/assets/asset-locale.js +16 -0
  79. package/dist/components/assets/asset-locale.js.map +1 -0
  80. package/dist/components/assets/asset-meta-label.d.ts +12 -0
  81. package/dist/components/assets/asset-meta-label.js +15 -0
  82. package/dist/components/assets/asset-meta-label.js.map +1 -0
  83. package/dist/components/assets/asset-metadata.d.ts +69 -15
  84. package/dist/components/assets/asset-metadata.js +99 -11
  85. package/dist/components/assets/asset-metadata.js.map +1 -1
  86. package/dist/components/assets/asset-search.js +8 -2
  87. package/dist/components/assets/asset-search.js.map +1 -1
  88. package/dist/components/assets/asset-thumb.js +9 -5
  89. package/dist/components/assets/asset-thumb.js.map +1 -1
  90. package/dist/components/assets/assets-collection-view-source.d.ts +25 -11
  91. package/dist/components/assets/assets-collection-view-source.js +78 -26
  92. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  93. package/dist/components/assets/assets-module.js +1 -1
  94. package/dist/components/assets/assets-module.js.map +1 -1
  95. package/dist/components/assets/assets-store.d.ts +2 -2
  96. package/dist/components/assets/assets-store.js +22 -9
  97. package/dist/components/assets/assets-store.js.map +1 -1
  98. package/dist/components/assets/assets-view-preference.js +1 -1
  99. package/dist/components/assets/assets-view-preference.js.map +1 -1
  100. package/dist/components/assets/replace-file.js +1 -1
  101. package/dist/components/assets/replace-file.js.map +1 -1
  102. package/dist/components/auth-context.d.ts +1 -1
  103. package/dist/components/auth-context.js +6 -6
  104. package/dist/components/auth-context.js.map +1 -1
  105. package/dist/components/builder/blocks-model.d.ts +1 -1
  106. package/dist/components/builder/blocks-model.js +3 -3
  107. package/dist/components/builder/blocks-model.js.map +1 -1
  108. package/dist/components/builder/blocks-panel.d.ts +1 -1
  109. package/dist/components/builder/blocks-panel.js +4 -4
  110. package/dist/components/builder/blocks-panel.js.map +1 -1
  111. package/dist/components/builder/builder-store.d.ts +21 -0
  112. package/dist/components/builder/builder-store.js +24 -2
  113. package/dist/components/builder/builder-store.js.map +1 -1
  114. package/dist/components/builder/canvas-breakpoints.js +1 -1
  115. package/dist/components/builder/canvas-breakpoints.js.map +1 -1
  116. package/dist/components/builder/canvas-commit-boundary.d.ts +22 -0
  117. package/dist/components/builder/canvas-commit-boundary.js +27 -0
  118. package/dist/components/builder/canvas-commit-boundary.js.map +1 -0
  119. package/dist/components/builder/canvas-dnd.d.ts +2 -2
  120. package/dist/components/builder/canvas-dnd.js +1 -1
  121. package/dist/components/builder/canvas-dnd.js.map +1 -1
  122. package/dist/components/builder/canvas-inline-editing.d.ts +1 -1
  123. package/dist/components/builder/canvas-inline-editing.js.map +1 -1
  124. package/dist/components/builder/canvas-overlay-geometry.d.ts +1 -1
  125. package/dist/components/builder/canvas-overlay-geometry.js.map +1 -1
  126. package/dist/components/builder/canvas-rebase.d.ts +77 -0
  127. package/dist/components/builder/canvas-rebase.js +169 -0
  128. package/dist/components/builder/canvas-rebase.js.map +1 -0
  129. package/dist/components/builder/canvas-render-context.d.ts +7 -4
  130. package/dist/components/builder/canvas-render-context.js +21 -13
  131. package/dist/components/builder/canvas-render-context.js.map +1 -1
  132. package/dist/components/builder/canvas-theme-notice.js +2 -2
  133. package/dist/components/builder/canvas-theme-notice.js.map +1 -1
  134. package/dist/components/builder/canvas.js +3 -3
  135. package/dist/components/builder/canvas.js.map +1 -1
  136. package/dist/components/builder/clipboard-actions.d.ts +2 -2
  137. package/dist/components/builder/clipboard-actions.js +4 -4
  138. package/dist/components/builder/clipboard-actions.js.map +1 -1
  139. package/dist/components/builder/composition-field-editor-lazy.js +1 -1
  140. package/dist/components/builder/composition-field-editor-lazy.js.map +1 -1
  141. package/dist/components/builder/composition-field-editor.d.ts +21 -3
  142. package/dist/components/builder/composition-field-editor.js +48 -30
  143. package/dist/components/builder/composition-field-editor.js.map +1 -1
  144. package/dist/components/builder/drag-session.d.ts +1 -1
  145. package/dist/components/builder/drag-session.js.map +1 -1
  146. package/dist/components/builder/fixture-thumbnail.js +1 -1
  147. package/dist/components/builder/fixture-thumbnail.js.map +1 -1
  148. package/dist/components/builder/insert-context.d.ts +1 -1
  149. package/dist/components/builder/insert-context.js +3 -3
  150. package/dist/components/builder/insert-context.js.map +1 -1
  151. package/dist/components/builder/layout-inspector.js +1 -1
  152. package/dist/components/builder/layout-inspector.js.map +1 -1
  153. package/dist/components/builder/node-factory.js +15 -3
  154. package/dist/components/builder/node-factory.js.map +1 -1
  155. package/dist/components/builder/node-title.js +1 -1
  156. package/dist/components/builder/node-title.js.map +1 -1
  157. package/dist/components/builder/page-tree.d.ts +1 -1
  158. package/dist/components/builder/page-tree.js +5 -5
  159. package/dist/components/builder/page-tree.js.map +1 -1
  160. package/dist/components/builder/placement-refusal.js +3 -3
  161. package/dist/components/builder/placement-refusal.js.map +1 -1
  162. package/dist/components/builder/property-inspector.js +7 -5
  163. package/dist/components/builder/property-inspector.js.map +1 -1
  164. package/dist/components/builder/reuse-context.js +4 -4
  165. package/dist/components/builder/reuse-context.js.map +1 -1
  166. package/dist/components/builder/reuse.js +1 -1
  167. package/dist/components/builder/reuse.js.map +1 -1
  168. package/dist/components/builder/rich-text-toolbar.js +1 -1
  169. package/dist/components/builder/rich-text-toolbar.js.map +1 -1
  170. package/dist/components/builder/run-inspector.js +11 -7
  171. package/dist/components/builder/run-inspector.js.map +1 -1
  172. package/dist/components/builder/selection-overlay.js +3 -3
  173. package/dist/components/builder/selection-overlay.js.map +1 -1
  174. package/dist/components/builder/spacing-handles.d.ts +2 -2
  175. package/dist/components/builder/spacing-handles.js +3 -3
  176. package/dist/components/builder/spacing-handles.js.map +1 -1
  177. package/dist/components/cms/asset-field.d.ts +5 -2
  178. package/dist/components/cms/asset-field.js +36 -19
  179. package/dist/components/cms/asset-field.js.map +1 -1
  180. package/dist/components/cms/asset-picker.js +5 -3
  181. package/dist/components/cms/asset-picker.js.map +1 -1
  182. package/dist/components/cms/bulk-actions.d.ts +1 -1
  183. package/dist/components/cms/bulk-actions.js +2 -2
  184. package/dist/components/cms/bulk-actions.js.map +1 -1
  185. package/dist/components/cms/bulk-bar.d.ts +2 -2
  186. package/dist/components/cms/bulk-bar.js +2 -2
  187. package/dist/components/cms/bulk-bar.js.map +1 -1
  188. package/dist/components/cms/canvas-commit.d.ts +56 -0
  189. package/dist/components/cms/canvas-commit.js +35 -0
  190. package/dist/components/cms/canvas-commit.js.map +1 -0
  191. package/dist/components/cms/cell-edit.d.ts +1 -1
  192. package/dist/components/cms/cell-edit.js +1 -1
  193. package/dist/components/cms/cell-edit.js.map +1 -1
  194. package/dist/components/cms/cell.d.ts +1 -1
  195. package/dist/components/cms/cell.js +3 -3
  196. package/dist/components/cms/cell.js.map +1 -1
  197. package/dist/components/cms/cms-index.js +2 -2
  198. package/dist/components/cms/cms-index.js.map +1 -1
  199. package/dist/components/cms/cms-labels.js +2 -2
  200. package/dist/components/cms/cms-labels.js.map +1 -1
  201. package/dist/components/cms/cms-store.d.ts +2 -2
  202. package/dist/components/cms/cms-store.js +10 -10
  203. package/dist/components/cms/cms-store.js.map +1 -1
  204. package/dist/components/cms/collection-table.js +30 -14
  205. package/dist/components/cms/collection-table.js.map +1 -1
  206. package/dist/components/cms/commit-policy.d.ts +6 -2
  207. package/dist/components/cms/commit-policy.js +1 -1
  208. package/dist/components/cms/commit-policy.js.map +1 -1
  209. package/dist/components/cms/detail-editor.d.ts +5 -2
  210. package/dist/components/cms/detail-editor.js +31 -15
  211. package/dist/components/cms/detail-editor.js.map +1 -1
  212. package/dist/components/cms/details-pane.js +1 -1
  213. package/dist/components/cms/details-pane.js.map +1 -1
  214. package/dist/components/cms/doc-state.js +1 -1
  215. package/dist/components/cms/doc-state.js.map +1 -1
  216. package/dist/components/cms/document-diff-view.js +5 -5
  217. package/dist/components/cms/document-diff-view.js.map +1 -1
  218. package/dist/components/cms/document-editor-context.js +1 -1
  219. package/dist/components/cms/document-editor-context.js.map +1 -1
  220. package/dist/components/cms/document-staging.js +1 -1
  221. package/dist/components/cms/document-staging.js.map +1 -1
  222. package/dist/components/cms/document-url-path.js +2 -2
  223. package/dist/components/cms/document-url-path.js.map +1 -1
  224. package/dist/components/cms/duplicate-document.d.ts +1 -1
  225. package/dist/components/cms/duplicate-document.js.map +1 -1
  226. package/dist/components/cms/editable-cell.js +3 -3
  227. package/dist/components/cms/editable-cell.js.map +1 -1
  228. package/dist/components/cms/field-collaboration.d.ts +1 -1
  229. package/dist/components/cms/field-collaboration.js +1 -1
  230. package/dist/components/cms/field-collaboration.js.map +1 -1
  231. package/dist/components/cms/field-connection.d.ts +1 -1
  232. package/dist/components/cms/field-connection.js +2 -2
  233. package/dist/components/cms/field-connection.js.map +1 -1
  234. package/dist/components/cms/field-control-lib.js +1 -1
  235. package/dist/components/cms/field-control-lib.js.map +1 -1
  236. package/dist/components/cms/field-control.js +4 -4
  237. package/dist/components/cms/field-control.js.map +1 -1
  238. package/dist/components/cms/field-renderer.d.ts +1 -1
  239. package/dist/components/cms/field-renderer.js +2 -2
  240. package/dist/components/cms/field-renderer.js.map +1 -1
  241. package/dist/components/cms/filter-builder.d.ts +1 -1
  242. package/dist/components/cms/filter-builder.js +3 -3
  243. package/dist/components/cms/filter-builder.js.map +1 -1
  244. package/dist/components/cms/gate-validation.js +1 -1
  245. package/dist/components/cms/gate-validation.js.map +1 -1
  246. package/dist/components/cms/import-export.js +2 -2
  247. package/dist/components/cms/import-export.js.map +1 -1
  248. package/dist/components/cms/issue-copy.js +1 -1
  249. package/dist/components/cms/issue-copy.js.map +1 -1
  250. package/dist/components/cms/issues-pane.js +3 -3
  251. package/dist/components/cms/issues-pane.js.map +1 -1
  252. package/dist/components/cms/object-array-field.js +2 -2
  253. package/dist/components/cms/object-array-field.js.map +1 -1
  254. package/dist/components/cms/prop-fields.js +2 -2
  255. package/dist/components/cms/prop-fields.js.map +1 -1
  256. package/dist/components/cms/reference-confirm.d.ts +1 -1
  257. package/dist/components/cms/reference-confirm.js +2 -2
  258. package/dist/components/cms/reference-confirm.js.map +1 -1
  259. package/dist/components/cms/relation-picker.js +2 -2
  260. package/dist/components/cms/relation-picker.js.map +1 -1
  261. package/dist/components/cms/rich-text/block-style.js +1 -1
  262. package/dist/components/cms/rich-text/block-style.js.map +1 -1
  263. package/dist/components/cms/rich-text/component-embed.js +4 -4
  264. package/dist/components/cms/rich-text/component-embed.js.map +1 -1
  265. package/dist/components/cms/rich-text/editor.js +3 -3
  266. package/dist/components/cms/rich-text/editor.js.map +1 -1
  267. package/dist/components/cms/rich-text/embed-picker.js +3 -3
  268. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  269. package/dist/components/cms/rich-text/embed-props.js +3 -3
  270. package/dist/components/cms/rich-text/embed-props.js.map +1 -1
  271. package/dist/components/cms/rich-text/extensions.js +1 -1
  272. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  273. package/dist/components/cms/rich-text/font-size-control.d.ts +1 -1
  274. package/dist/components/cms/rich-text/font-size-control.js +2 -2
  275. package/dist/components/cms/rich-text/font-size-control.js.map +1 -1
  276. package/dist/components/cms/rich-text/lazy.js +1 -1
  277. package/dist/components/cms/rich-text/lazy.js.map +1 -1
  278. package/dist/components/cms/rich-text/link-control.js +2 -2
  279. package/dist/components/cms/rich-text/link-control.js.map +1 -1
  280. package/dist/components/cms/rich-text/slash-command.js +3 -3
  281. package/dist/components/cms/rich-text/slash-command.js.map +1 -1
  282. package/dist/components/cms/rich-text/toolbar.js +1 -1
  283. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  284. package/dist/components/cms/rich-text/unknown-node.js +2 -2
  285. package/dist/components/cms/rich-text/unknown-node.js.map +1 -1
  286. package/dist/components/cms/rich-text-preview.js +2 -2
  287. package/dist/components/cms/rich-text-preview.js.map +1 -1
  288. package/dist/components/cms/row-actions.d.ts +1 -1
  289. package/dist/components/cms/row-actions.js.map +1 -1
  290. package/dist/components/cms/scalar-value-editor.d.ts +1 -1
  291. package/dist/components/cms/scalar-value-editor.js +2 -2
  292. package/dist/components/cms/scalar-value-editor.js.map +1 -1
  293. package/dist/components/cms/use-discard-controller.d.ts +1 -1
  294. package/dist/components/cms/use-discard-controller.js +1 -1
  295. package/dist/components/cms/use-discard-controller.js.map +1 -1
  296. package/dist/components/cms/use-document-draft.d.ts +69 -5
  297. package/dist/components/cms/use-document-draft.js +118 -34
  298. package/dist/components/cms/use-document-draft.js.map +1 -1
  299. package/dist/components/cms/use-document-versions.js +1 -1
  300. package/dist/components/cms/use-document-versions.js.map +1 -1
  301. package/dist/components/cms/use-field-focus.d.ts +1 -1
  302. package/dist/components/cms/use-field-focus.js +2 -2
  303. package/dist/components/cms/use-field-focus.js.map +1 -1
  304. package/dist/components/cms/use-live-url.d.ts +7 -2
  305. package/dist/components/cms/use-live-url.js +15 -12
  306. package/dist/components/cms/use-live-url.js.map +1 -1
  307. package/dist/components/cms/use-publish-controller.d.ts +1 -1
  308. package/dist/components/cms/use-publish-controller.js +1 -1
  309. package/dist/components/cms/use-publish-controller.js.map +1 -1
  310. package/dist/components/cms/use-published-perspective.d.ts +1 -1
  311. package/dist/components/cms/use-published-perspective.js.map +1 -1
  312. package/dist/components/cms/use-restore-version.d.ts +1 -1
  313. package/dist/components/cms/use-restore-version.js +1 -1
  314. package/dist/components/cms/use-restore-version.js.map +1 -1
  315. package/dist/components/cms/version-compare-lazy.js +1 -1
  316. package/dist/components/cms/version-compare-lazy.js.map +1 -1
  317. package/dist/components/cms/version-compare.js +1 -1
  318. package/dist/components/cms/version-compare.js.map +1 -1
  319. package/dist/components/cms/versions-pane.js +3 -3
  320. package/dist/components/cms/versions-pane.js.map +1 -1
  321. package/dist/components/cms/view-bar.js +3 -3
  322. package/dist/components/cms/view-bar.js.map +1 -1
  323. package/dist/components/collection-view/cms-collection-view-source.d.ts +1 -1
  324. package/dist/components/collection-view/cms-collection-view-source.js +11 -7
  325. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  326. package/dist/components/collection-view/collection-view-context.d.ts +1 -1
  327. package/dist/components/collection-view/collection-view-context.js +1 -1
  328. package/dist/components/collection-view/collection-view-context.js.map +1 -1
  329. package/dist/components/collection-view/collection-view-surface.d.ts +13 -0
  330. package/dist/components/collection-view/collection-view-surface.js +18 -0
  331. package/dist/components/collection-view/collection-view-surface.js.map +1 -0
  332. package/dist/components/components/component-detail.js +10 -10
  333. package/dist/components/components/component-detail.js.map +1 -1
  334. package/dist/components/components/component-preview.d.ts +2 -2
  335. package/dist/components/components/component-preview.js +5 -5
  336. package/dist/components/components/component-preview.js.map +1 -1
  337. package/dist/components/components/component-status.d.ts +1 -1
  338. package/dist/components/components/component-status.js +1 -1
  339. package/dist/components/components/component-status.js.map +1 -1
  340. package/dist/components/components/components-index.js +2 -2
  341. package/dist/components/components/components-index.js.map +1 -1
  342. package/dist/components/components/field-inspector.js +1 -1
  343. package/dist/components/components/field-inspector.js.map +1 -1
  344. package/dist/components/components/registry-browser.js +3 -3
  345. package/dist/components/components/registry-browser.js.map +1 -1
  346. package/dist/components/components/taxonomy.d.ts +1 -1
  347. package/dist/components/components/taxonomy.js.map +1 -1
  348. package/dist/components/components/transfer-dialog.js +3 -3
  349. package/dist/components/components/transfer-dialog.js.map +1 -1
  350. package/dist/components/current-user-menu.js +1 -1
  351. package/dist/components/current-user-menu.js.map +1 -1
  352. package/dist/components/delivery-health-banner.js +2 -2
  353. package/dist/components/delivery-health-banner.js.map +1 -1
  354. package/dist/components/deployment-setup-gate.js +13 -4
  355. package/dist/components/deployment-setup-gate.js.map +1 -1
  356. package/dist/components/dev-login-badge.js +2 -2
  357. package/dist/components/dev-login-badge.js.map +1 -1
  358. package/dist/components/guards.js +1 -1
  359. package/dist/components/guards.js.map +1 -1
  360. package/dist/components/inspector/document-panes.d.ts +54 -0
  361. package/dist/components/inspector/document-panes.js +41 -0
  362. package/dist/components/inspector/document-panes.js.map +1 -0
  363. package/dist/components/inspector/inspector-sidebar.js +1 -1
  364. package/dist/components/inspector/inspector-sidebar.js.map +1 -1
  365. package/dist/components/nav-issues-registry.d.ts +53 -0
  366. package/dist/components/nav-issues-registry.js +69 -0
  367. package/dist/components/nav-issues-registry.js.map +1 -0
  368. package/dist/components/nav-issues.js +4 -4
  369. package/dist/components/nav-issues.js.map +1 -1
  370. package/dist/components/operation-error.d.ts +1 -1
  371. package/dist/components/operation-error.js +4 -4
  372. package/dist/components/operation-error.js.map +1 -1
  373. package/dist/components/playground-badge.js +2 -2
  374. package/dist/components/playground-badge.js.map +1 -1
  375. package/dist/components/project-list.js +3 -3
  376. package/dist/components/project-list.js.map +1 -1
  377. package/dist/components/project-session.d.ts +2 -2
  378. package/dist/components/project-session.js +5 -5
  379. package/dist/components/project-session.js.map +1 -1
  380. package/dist/components/project-switcher.js +2 -2
  381. package/dist/components/project-switcher.js.map +1 -1
  382. package/dist/components/rail-store-status.js +1 -1
  383. package/dist/components/rail-store-status.js.map +1 -1
  384. package/dist/components/rail-view-source.d.ts +6 -5
  385. package/dist/components/rail-view-source.js +4 -3
  386. package/dist/components/rail-view-source.js.map +1 -1
  387. package/dist/components/records/record-lens.d.ts +51 -0
  388. package/dist/components/records/record-lens.js +151 -0
  389. package/dist/components/records/record-lens.js.map +1 -0
  390. package/dist/components/records/record-rail.d.ts +86 -0
  391. package/dist/components/records/record-rail.js +241 -0
  392. package/dist/components/records/record-rail.js.map +1 -0
  393. package/dist/components/redirects/redirects-schema.js +1 -1
  394. package/dist/components/redirects/redirects-schema.js.map +1 -1
  395. package/dist/components/references/references-panel.js +2 -2
  396. package/dist/components/references/references-panel.js.map +1 -1
  397. package/dist/components/section-nav.js +27 -11
  398. package/dist/components/section-nav.js.map +1 -1
  399. package/dist/components/section-state.js +2 -2
  400. package/dist/components/section-state.js.map +1 -1
  401. package/dist/components/section-surface.js +2 -2
  402. package/dist/components/section-surface.js.map +1 -1
  403. package/dist/components/session-expired-overlay.js +3 -3
  404. package/dist/components/session-expired-overlay.js.map +1 -1
  405. package/dist/components/settings-form.js +1 -1
  406. package/dist/components/settings-form.js.map +1 -1
  407. package/dist/components/sign-in-needed-badge.js +1 -1
  408. package/dist/components/sign-in-needed-badge.js.map +1 -1
  409. package/dist/components/sign-in-view.d.ts +2 -2
  410. package/dist/components/sign-in-view.js +5 -5
  411. package/dist/components/sign-in-view.js.map +1 -1
  412. package/dist/components/studio-context.js +3 -3
  413. package/dist/components/studio-context.js.map +1 -1
  414. package/dist/components/studio-project-screen.js +30 -3
  415. package/dist/components/studio-project-screen.js.map +1 -1
  416. package/dist/components/studio-shell.js +6 -5
  417. package/dist/components/studio-shell.js.map +1 -1
  418. package/dist/components/theme-context.d.ts +1 -1
  419. package/dist/components/theme-context.js +2 -2
  420. package/dist/components/theme-context.js.map +1 -1
  421. package/dist/components/use-hydration-phase.d.ts +1 -1
  422. package/dist/components/use-hydration-phase.js +1 -1
  423. package/dist/components/use-hydration-phase.js.map +1 -1
  424. package/dist/components/users/invite-user-bar.d.ts +27 -0
  425. package/dist/components/users/invite-user-bar.js +25 -9
  426. package/dist/components/users/invite-user-bar.js.map +1 -1
  427. package/dist/components/users/role-policy-drift-issues.d.ts +38 -0
  428. package/dist/components/users/role-policy-drift-issues.js +71 -0
  429. package/dist/components/users/role-policy-drift-issues.js.map +1 -0
  430. package/dist/components/users/users-collection-view-source.d.ts +1 -1
  431. package/dist/components/users/users-collection-view-source.js +4 -2
  432. package/dist/components/users/users-collection-view-source.js.map +1 -1
  433. package/dist/components/users/users-module.js +1 -1
  434. package/dist/components/users/users-module.js.map +1 -1
  435. package/dist/components/users/users-schema.d.ts +10 -1
  436. package/dist/components/users/users-schema.js +45 -25
  437. package/dist/components/users/users-schema.js.map +1 -1
  438. package/dist/components/users/users-store.js +3 -3
  439. package/dist/components/users/users-store.js.map +1 -1
  440. package/dist/components/workspace-projects.d.ts +1 -1
  441. package/dist/components/workspace-projects.js +1 -1
  442. package/dist/components/workspace-projects.js.map +1 -1
  443. package/dist/lib/ai/index.d.ts +1 -1
  444. package/dist/lib/ai/index.js +1 -1
  445. package/dist/lib/ai/index.js.map +1 -1
  446. package/dist/lib/ai/node-handler.d.ts +1 -1
  447. package/dist/lib/ai/node-handler.js.map +1 -1
  448. package/dist/lib/ai/translate-document.js +1 -1
  449. package/dist/lib/ai/translate-document.js.map +1 -1
  450. package/dist/lib/assets/upload.d.ts +25 -7
  451. package/dist/lib/assets/upload.js +30 -21
  452. package/dist/lib/assets/upload.js.map +1 -1
  453. package/dist/lib/clipboard/paste-issues.js +1 -1
  454. package/dist/lib/clipboard/paste-issues.js.map +1 -1
  455. package/dist/lib/clipboard/paste-target.js +2 -2
  456. package/dist/lib/clipboard/paste-target.js.map +1 -1
  457. package/dist/lib/cms/published-documents.d.ts +24 -6
  458. package/dist/lib/cms/published-documents.js +39 -6
  459. package/dist/lib/cms/published-documents.js.map +1 -1
  460. package/dist/lib/cms-views/view-model.d.ts +12 -99
  461. package/dist/lib/cms-views/view-model.js +22 -103
  462. package/dist/lib/cms-views/view-model.js.map +1 -1
  463. package/dist/lib/collection-view/collection-view-source.d.ts +13 -3
  464. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  465. package/dist/lib/data/content-session-refresh.d.ts +1 -1
  466. package/dist/lib/data/content-session-refresh.js.map +1 -1
  467. package/dist/lib/data/playground-unload-guard.d.ts +1 -1
  468. package/dist/lib/data/playground-unload-guard.js +1 -1
  469. package/dist/lib/data/playground-unload-guard.js.map +1 -1
  470. package/dist/lib/engine-version.d.ts +1 -1
  471. package/dist/lib/engine-version.js +1 -1
  472. package/dist/lib/engine-version.js.map +1 -1
  473. package/dist/lib/i18n/i18n.js +1 -1
  474. package/dist/lib/i18n/i18n.js.map +1 -1
  475. package/dist/lib/i18n/messages.de.js +11 -3
  476. package/dist/lib/i18n/messages.de.js.map +1 -1
  477. package/dist/lib/i18n/messages.en.d.ts +9 -1
  478. package/dist/lib/i18n/messages.en.js +16 -4
  479. package/dist/lib/i18n/messages.en.js.map +1 -1
  480. package/dist/lib/operations/config-manifest-lookup.d.ts +1 -1
  481. package/dist/lib/operations/config-manifest-lookup.js +3 -3
  482. package/dist/lib/operations/config-manifest-lookup.js.map +1 -1
  483. package/dist/lib/operations/project-runtime-config.d.ts +9 -0
  484. package/dist/lib/operations/project-runtime-config.js +13 -0
  485. package/dist/lib/operations/project-runtime-config.js.map +1 -1
  486. package/dist/lib/operations/studio-operations.d.ts +9 -1
  487. package/dist/lib/operations/studio-operations.js +6 -1
  488. package/dist/lib/operations/studio-operations.js.map +1 -1
  489. package/dist/lib/persistence/convex-adapter.d.ts +16 -3
  490. package/dist/lib/persistence/convex-adapter.js +78 -47
  491. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  492. package/dist/lib/persistence/deployment-readiness.js +14 -11
  493. package/dist/lib/persistence/deployment-readiness.js.map +1 -1
  494. package/dist/lib/persistence/index.js +14 -1
  495. package/dist/lib/persistence/index.js.map +1 -1
  496. package/dist/lib/persona/labels.js +2 -1
  497. package/dist/lib/persona/labels.js.map +1 -1
  498. package/dist/lib/query/index.d.ts +2 -2
  499. package/dist/lib/query/index.js +1 -1
  500. package/dist/lib/query/index.js.map +1 -1
  501. package/dist/lib/query/operations-query.d.ts +27 -1
  502. package/dist/lib/query/operations-query.js +13 -1
  503. package/dist/lib/query/operations-query.js.map +1 -1
  504. package/dist/lib/references/reference-model.js +1 -1
  505. package/dist/lib/references/reference-model.js.map +1 -1
  506. package/dist/lib/references/references-store.js +1 -1
  507. package/dist/lib/references/references-store.js.map +1 -1
  508. package/dist/lib/registry/index.js +2 -2
  509. package/dist/lib/registry/index.js.map +1 -1
  510. package/dist/lib/registry/preview.d.ts +10 -0
  511. package/dist/lib/registry/preview.js +13 -4
  512. package/dist/lib/registry/preview.js.map +1 -1
  513. package/dist/lib/routes/route-issues.js +6 -3
  514. package/dist/lib/routes/route-issues.js.map +1 -1
  515. package/dist/lib/workspace/index.d.ts +6 -0
  516. package/dist/lib/workspace/index.js +8 -0
  517. package/dist/lib/workspace/index.js.map +1 -1
  518. package/dist/lib/workspace/workspace-source.js +6 -7
  519. package/dist/lib/workspace/workspace-source.js.map +1 -1
  520. package/dist/mount.d.ts +28 -0
  521. package/dist/mount.js +27 -0
  522. package/dist/mount.js.map +1 -0
  523. package/dist/previews/entry-node.d.ts +14 -0
  524. package/dist/previews/entry-node.js +22 -0
  525. package/dist/previews/entry-node.js.map +1 -0
  526. package/dist/previews/harness.js +2 -16
  527. package/dist/previews/harness.js.map +1 -1
  528. package/dist/router/root.js +10 -10
  529. package/dist/router/root.js.map +1 -1
  530. package/dist/ui/badge.d.ts +1 -1
  531. package/dist/ui/button.d.ts +2 -2
  532. package/package.json +15 -11
  533. package/dist/components/redirects/redirects-collection-view-source.d.ts +0 -53
  534. package/dist/components/redirects/redirects-collection-view-source.js +0 -146
  535. package/dist/components/redirects/redirects-collection-view-source.js.map +0 -1
  536. package/dist/components/redirects/redirects-module.d.ts +0 -21
  537. package/dist/components/redirects/redirects-module.js +0 -40
  538. package/dist/components/redirects/redirects-module.js.map +0 -1
  539. package/dist/components/redirects/redirects-store.d.ts +0 -68
  540. package/dist/components/redirects/redirects-store.js +0 -132
  541. package/dist/components/redirects/redirects-store.js.map +0 -1
@@ -0,0 +1,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
+ })
package/convex/schema.ts CHANGED
@@ -277,6 +277,12 @@ export default defineSchema({
277
277
  lqip: v.optional(v.string()),
278
278
  /** Schema-driven asset metadata bag (EC-301): alt, copyright, credit, …. */
279
279
  meta: v.optional(v.any()),
280
+ /**
281
+ * EC-632 — the per-locale values of the asset collection's `localized: true` fields,
282
+ * `{ [locale]: { [field]: value } }` (the document mechanism beside `meta`). Optional:
283
+ * every row without one reads its shared `meta`, exactly as before.
284
+ */
285
+ localizedMeta: v.optional(v.any()),
280
286
  createdAt: v.string(),
281
287
  updatedAt: v.string(),
282
288
  }).index('by_asset', ['assetId']),
@@ -299,6 +305,36 @@ export default defineSchema({
299
305
  updatedAt: v.string(),
300
306
  }).index('by_redirect', ['redirectId']),
301
307
 
308
+ /**
309
+ * The generic RECORD STORE (EC-616) — one row per record of a DECLARED record kind
310
+ * (`defineRecordKind` in `@elytracms/core/persistence`): the engine's own kinds and
311
+ * every addon's. `data` holds the kind's columns + its open `meta` bag, validated by
312
+ * the kind's schema in the write path (`defineRecordFunctions`, `record-functions.ts`)
313
+ * — the data SHAPE, never project structure (EC-585 1e).
314
+ *
315
+ * The index slots are STATIC (a consumer re-exports this schema, so an addon can never
316
+ * add an index); a kind fills them from its own declared columns: `key1`/`key2` from
317
+ * its `keys`, `at` from its `time` column (else `createdAt`). The indexes are the
318
+ * known access paths: one record by id; a kind by time (recent-first lists, a TTL
319
+ * sweep); by either key + time (submissions by form or by status, newest first); by
320
+ * both keys + time (an aggregate by day + document).
321
+ */
322
+ records: defineTable({
323
+ kind: v.string(),
324
+ recordId: v.string(),
325
+ data: v.any(),
326
+ key1: v.optional(v.string()),
327
+ key2: v.optional(v.string()),
328
+ at: v.string(),
329
+ createdAt: v.string(),
330
+ updatedAt: v.string(),
331
+ })
332
+ .index('by_kind_record', ['kind', 'recordId'])
333
+ .index('by_kind_at', ['kind', 'at'])
334
+ .index('by_kind_key1', ['kind', 'key1', 'at'])
335
+ .index('by_kind_key2', ['kind', 'key2', 'at'])
336
+ .index('by_kind_keys', ['kind', 'key1', 'key2', 'at']),
337
+
302
338
  /**
303
339
  * Reference index entries (EC-155, vision AD-6) — derived data, recomputed
304
340
  * replace-all-for-source on every write. Indexed for efficient lookup in both
@@ -1,5 +1,6 @@
1
1
  import { unfoldDocument } from '@elytracms/core/cms-core'
2
2
  import type { CmsDocument } from '@elytracms/core/cms-core'
3
+ import { redirectRecordKind } from '@elytracms/core/persistence'
3
4
  import { Triggers } from 'convex-helpers/server/triggers'
4
5
  import type { DataModel } from './functions'
5
6
  import { maintainDocumentReferences, removeDocumentReferences } from './referenceMaintenance'
@@ -33,7 +34,8 @@ import { scheduleRevalidate } from './revalidate'
33
34
  *
34
35
  * - `cmsDocuments` — only when the live pin (`publishedVersion`) moves
35
36
  * (`publish` / `unpublish`), or a pinned row is deleted (`delete`);
36
- * - `redirects` (EC-510) — every write (`redirects`; the row IS the live state);
37
+ * - `records` of the redirect kind (EC-616; EC-510 before it on the `redirects` table) —
38
+ * every write (`redirects`; the row IS the live state); no other record kind;
37
39
  * - `assets` (EC-579) — every edit and delete (`asset`); a create fires nothing.
38
40
  *
39
41
  * The snapshot reads one more table, `documentVersions`, but only the version a
@@ -129,6 +131,24 @@ triggers.register('redirects', async (ctx) => {
129
131
  await scheduleRevalidate(ctx, { kind: 'redirects' })
130
132
  })
131
133
 
134
+ /**
135
+ * The generic record store (EC-616). Of all record kinds, ONLY the engine's redirect
136
+ * kind is projected onto the live wire (`delivery.ts` reads it through
137
+ * `loadRedirectEntries`), so only a write to a `redirect` row revalidates — with exactly
138
+ * the event the legacy `redirects` table fired above (EC-510: every write, the receiver
139
+ * answers with the sweep). Every other kind — an addon's submissions, analytics rows —
140
+ * is never on the wire and fires nothing; a kind that starts reaching the snapshot
141
+ * belongs in this check.
142
+ *
143
+ * Since the move, a redirect write is a `records` write; the legacy table's trigger
144
+ * above still fires for the transitional deletes (a save lifting a legacy-only redirect
145
+ * deletes its legacy row: two sweeps for that one save, until the migration ran).
146
+ */
147
+ triggers.register('records', async (ctx, change) => {
148
+ const kind = (change.newDoc ?? change.oldDoc)?.kind
149
+ if (kind === redirectRecordKind.kind) await scheduleRevalidate(ctx, { kind: 'redirects' })
150
+ })
151
+
132
152
  /**
133
153
  * Asset records (EC-579). The delivery snapshot projects every asset row onto
134
154
  * the live wire field by field (url, dimensions, alt, focal point, lqip, the
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `@elytracms/studio/addon` — THE public addon API (EC-616, ruling EC-565 § 6b item 2).
3
+ *
4
+ * An addon (`@elytracms/forms`, `@elytracms/analytics`, or your own) is an optional
5
+ * package whose studio half contributes navigation items to a project's
6
+ * `defineStudio({ sidebar })` — a rail bound to a record kind, or a view with its own
7
+ * component — and whose backend half is a record kind in the generic record store
8
+ * (`defineRecordKind` / `adapter.records(kind)` in `@elytracms/core/persistence`, its
9
+ * Convex functions from `defineRecordFunctions` in `@elytracms/studio/convex/
10
+ * record-functions`). Everything an addon's studio code touches is exported HERE; the
11
+ * studio's internals (`lib/*`, `components/*`) are not part of the published package.
12
+ * `@elytracms/studio/ui` stays the primitive set (Button, Panel, Text, …).
13
+ *
14
+ * ## The chrome contract
15
+ *
16
+ * The SHELL draws the top bar, the breadcrumb (its section crumb is the view's `label`;
17
+ * deeper crumbs are contributed with {@link useSetBreadcrumb}), the rail with its issues
18
+ * foot (findings contributed with {@link useNavIssues}, or always-on through a view's
19
+ * `useIssues`), the banners and the theme. A VIEW owns the whole scrollable content area
20
+ * under it — including its own title header: the shell adds no frame, title or padding.
21
+ * A view reaches the studio through its {@link AppViewContext}: the project, its deep
22
+ * link and search, the operations client (every read and write goes through the one
23
+ * authorized chokepoint), the query hooks over the one cache, the content locale, and
24
+ * the signed-in user with their role.
25
+ *
26
+ * ## The two navigation items
27
+ *
28
+ * ```tsx
29
+ * import { nav } from '@elytracms/studio/addon'
30
+ * import { submissionRecordKind } from './kinds'
31
+ *
32
+ * // a rail bound to a record kind — the portable list/edit surfaces, no component to write
33
+ * nav.rail(submissionRecordKind, { label: 'Submissions' })
34
+ * // a custom component — the whole content area is yours
35
+ * nav.view({ id: 'forms-stats', label: 'Form stats', render: (ctx) => <Stats ctx={ctx} /> })
36
+ * ```
37
+ */
38
+ export { nav } from './components/app-views/default-modules.js';
39
+ export type { CustomViewConfig, RailViewConfig, BuiltinViewConfig, } from './components/app-views/default-modules.js';
40
+ export type { AppViewContext, AppViewDeepLink, AppViewModule, AppViewQueryHooks, NavIssuesHook, NavPermission, NavPermissionContext, SidebarNode, SidebarViewNode, } from './components/app-views/module.js';
41
+ /** A record rail's pieces, for an addon composing its own view around one. */
42
+ export { RECORD_RAIL_CAPABILITIES, RecordRailModule, RecordRailProvider, useRecordCollectionViewSource, useRecordList, useRecordRail, } from './components/records/record-rail.js';
43
+ export type { RecordRailModuleProps, RecordRailPaths, RecordRailStore } from './components/records/record-rail.js';
44
+ export { lensDocumentToRecordWrite, recordLensDef, recordToLensDocument } from './components/records/record-lens.js';
45
+ /** The portable list/edit surfaces over any source, and the scaffold that assembles one. */
46
+ export { CollectionViewSurface } from './components/collection-view/collection-view-surface.js';
47
+ export { assembleRailViewSource, metaBagProjection, normalizeOkResult, useRailViewSource, } from './components/rail-view-source.js';
48
+ export type { OkResult, RailAdapter } from './components/rail-view-source.js';
49
+ export type { CollectionViewCapabilities, CollectionViewSource, FieldAccess, } from './lib/collection-view/collection-view-source.js';
50
+ export { useOperationCache, useOperationQuery } from './lib/query/index.js';
51
+ export type { OperationCache, ReadState, UseOperationQueryOptions } from './lib/query/index.js';
52
+ export { useStudioLocale } from './lib/locale/index.js';
53
+ export type { StudioLocale } from './lib/locale/index.js';
54
+ export { useSetBreadcrumb } from './components/breadcrumb-context.js';
55
+ export type { BreadcrumbItem } from './components/breadcrumb-context.js';
56
+ export { useNavIssues } from './components/nav-issues-registry.js';
57
+ export type { NavIssue } from './components/nav-issues-registry.js';
58
+ /** A pane in the document editor's inspector — `defineStudio({ documentPanes: [documentPane({ … })] })`. */
59
+ export { documentPane } from './components/inspector/document-panes.js';
60
+ export type { DocumentPane, DocumentPaneContext } from './components/inspector/document-panes.js';
61
+ /**
62
+ * The portable edit shell alone (AD-19), with its form-column appendix seam (EC-396): an addon
63
+ * that assembles its own source (`useRailViewSource`) renders one record's detail with
64
+ * read-only sections below the editable fields — the forms inbox shows a submission's answers,
65
+ * files and delivery log there.
66
+ */
67
+ export { PortableDetailEditor } from './components/cms/detail-editor.js';
68
+ /** The one query key of a kind's records in a project — patch it after a write (`useOperationCache`). */
69
+ export { recordListKey } from './components/records/record-rail.js';
70
+ /** Resolve a `LocalizedString` (`'Text'` or `{ en, de, … }`) in the studio's UI language. */
71
+ export { useLabel } from './lib/i18n/index.js';
package/dist/addon.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `@elytracms/studio/addon` — THE public addon API (EC-616, ruling EC-565 § 6b item 2).
3
+ *
4
+ * An addon (`@elytracms/forms`, `@elytracms/analytics`, or your own) is an optional
5
+ * package whose studio half contributes navigation items to a project's
6
+ * `defineStudio({ sidebar })` — a rail bound to a record kind, or a view with its own
7
+ * component — and whose backend half is a record kind in the generic record store
8
+ * (`defineRecordKind` / `adapter.records(kind)` in `@elytracms/core/persistence`, its
9
+ * Convex functions from `defineRecordFunctions` in `@elytracms/studio/convex/
10
+ * record-functions`). Everything an addon's studio code touches is exported HERE; the
11
+ * studio's internals (`lib/*`, `components/*`) are not part of the published package.
12
+ * `@elytracms/studio/ui` stays the primitive set (Button, Panel, Text, …).
13
+ *
14
+ * ## The chrome contract
15
+ *
16
+ * The SHELL draws the top bar, the breadcrumb (its section crumb is the view's `label`;
17
+ * deeper crumbs are contributed with {@link useSetBreadcrumb}), the rail with its issues
18
+ * foot (findings contributed with {@link useNavIssues}, or always-on through a view's
19
+ * `useIssues`), the banners and the theme. A VIEW owns the whole scrollable content area
20
+ * under it — including its own title header: the shell adds no frame, title or padding.
21
+ * A view reaches the studio through its {@link AppViewContext}: the project, its deep
22
+ * link and search, the operations client (every read and write goes through the one
23
+ * authorized chokepoint), the query hooks over the one cache, the content locale, and
24
+ * the signed-in user with their role.
25
+ *
26
+ * ## The two navigation items
27
+ *
28
+ * ```tsx
29
+ * import { nav } from '@elytracms/studio/addon'
30
+ * import { submissionRecordKind } from './kinds'
31
+ *
32
+ * // a rail bound to a record kind — the portable list/edit surfaces, no component to write
33
+ * nav.rail(submissionRecordKind, { label: 'Submissions' })
34
+ * // a custom component — the whole content area is yours
35
+ * nav.view({ id: 'forms-stats', label: 'Form stats', render: (ctx) => <Stats ctx={ctx} /> })
36
+ * ```
37
+ */
38
+ // --- Navigation items and the view contract --------------------------------------------
39
+ export { nav } from './components/app-views/default-modules.js';
40
+ // --- Rails: a record kind, or your own source -------------------------------------------
41
+ /** A record rail's pieces, for an addon composing its own view around one. */
42
+ export { RECORD_RAIL_CAPABILITIES, RecordRailModule, RecordRailProvider, useRecordCollectionViewSource, useRecordList, useRecordRail, } from './components/records/record-rail.js';
43
+ export { lensDocumentToRecordWrite, recordLensDef, recordToLensDocument } from './components/records/record-lens.js';
44
+ /** The portable list/edit surfaces over any source, and the scaffold that assembles one. */
45
+ export { CollectionViewSurface } from './components/collection-view/collection-view-surface.js';
46
+ export { assembleRailViewSource, metaBagProjection, normalizeOkResult, useRailViewSource, } from './components/rail-view-source.js';
47
+ // --- Hooks: the cache, the locale, the breadcrumb, the rail foot -------------------------
48
+ export { useOperationCache, useOperationQuery } from './lib/query/index.js';
49
+ export { useStudioLocale } from './lib/locale/index.js';
50
+ export { useSetBreadcrumb } from './components/breadcrumb-context.js';
51
+ export { useNavIssues } from './components/nav-issues-registry.js';
52
+ // --- The document editor: contributed inspector panes (EC-618) ---------------------------
53
+ /** A pane in the document editor's inspector — `defineStudio({ documentPanes: [documentPane({ … })] })`. */
54
+ export { documentPane } from './components/inspector/document-panes.js';
55
+ // --- EC-613 (the forms addon): an own source's edit shell, its list key, the UI language -----
56
+ /**
57
+ * The portable edit shell alone (AD-19), with its form-column appendix seam (EC-396): an addon
58
+ * that assembles its own source (`useRailViewSource`) renders one record's detail with
59
+ * read-only sections below the editable fields — the forms inbox shows a submission's answers,
60
+ * files and delivery log there.
61
+ */
62
+ export { PortableDetailEditor } from './components/cms/detail-editor.js';
63
+ /** The one query key of a kind's records in a project — patch it after a write (`useOperationCache`). */
64
+ export { recordListKey } from './components/records/record-rail.js';
65
+ /** Resolve a `LocalizedString` (`'Text'` or `{ en, de, … }`) in the studio's UI language. */
66
+ export { useLabel } from './lib/i18n/index.js';
67
+ //# sourceMappingURL=addon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"addon.js","sourceRoot":"","sources":["../src/addon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,0FAA0F;AAE1F,OAAO,EAAE,GAAG,EAAE,MAAM,wCAAwC,CAAA;AAkB5D,2FAA2F;AAE3F,8EAA8E;AAC9E,OAAO,EACL,wBAAwB,EACxB,gBAAgB,EAChB,kBAAkB,EAClB,6BAA6B,EAC7B,aAAa,EACb,aAAa,GACd,MAAM,kCAAkC,CAAA;AAEzC,OAAO,EAAE,yBAAyB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAA;AAEjH,4FAA4F;AAC5F,OAAO,EAAE,qBAAqB,EAAE,MAAM,sDAAsD,CAAA;AAC5F,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,+BAA+B,CAAA;AAQtC,4FAA4F;AAE5F,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAE9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAA;AAElE,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAG/D,4FAA4F;AAE5F,4GAA4G;AAC5G,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAA;AAGpE,gGAAgG;AAEhG;;;;;GAKG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAA;AACrE,yGAAyG;AACzG,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAA;AAChE,6FAA6F;AAC7F,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA","sourcesContent":["/**\n * `@elytracms/studio/addon` — THE public addon API (EC-616, ruling EC-565 § 6b item 2).\n *\n * An addon (`@elytracms/forms`, `@elytracms/analytics`, or your own) is an optional\n * package whose studio half contributes navigation items to a project's\n * `defineStudio({ sidebar })` — a rail bound to a record kind, or a view with its own\n * component — and whose backend half is a record kind in the generic record store\n * (`defineRecordKind` / `adapter.records(kind)` in `@elytracms/core/persistence`, its\n * Convex functions from `defineRecordFunctions` in `@elytracms/studio/convex/\n * record-functions`). Everything an addon's studio code touches is exported HERE; the\n * studio's internals (`lib/*`, `components/*`) are not part of the published package.\n * `@elytracms/studio/ui` stays the primitive set (Button, Panel, Text, …).\n *\n * ## The chrome contract\n *\n * The SHELL draws the top bar, the breadcrumb (its section crumb is the view's `label`;\n * deeper crumbs are contributed with {@link useSetBreadcrumb}), the rail with its issues\n * foot (findings contributed with {@link useNavIssues}, or always-on through a view's\n * `useIssues`), the banners and the theme. A VIEW owns the whole scrollable content area\n * under it — including its own title header: the shell adds no frame, title or padding.\n * A view reaches the studio through its {@link AppViewContext}: the project, its deep\n * link and search, the operations client (every read and write goes through the one\n * authorized chokepoint), the query hooks over the one cache, the content locale, and\n * the signed-in user with their role.\n *\n * ## The two navigation items\n *\n * ```tsx\n * import { nav } from '@elytracms/studio/addon'\n * import { submissionRecordKind } from './kinds'\n *\n * // a rail bound to a record kind — the portable list/edit surfaces, no component to write\n * nav.rail(submissionRecordKind, { label: 'Submissions' })\n * // a custom component — the whole content area is yours\n * nav.view({ id: 'forms-stats', label: 'Form stats', render: (ctx) => <Stats ctx={ctx} /> })\n * ```\n */\n\n// --- Navigation items and the view contract --------------------------------------------\n\nexport { nav } from './components/app-views/default-modules'\nexport type {\n CustomViewConfig,\n RailViewConfig,\n BuiltinViewConfig,\n} from './components/app-views/default-modules'\nexport type {\n AppViewContext,\n AppViewDeepLink,\n AppViewModule,\n AppViewQueryHooks,\n NavIssuesHook,\n NavPermission,\n NavPermissionContext,\n SidebarNode,\n SidebarViewNode,\n} from './components/app-views/module'\n\n// --- Rails: a record kind, or your own source -------------------------------------------\n\n/** A record rail's pieces, for an addon composing its own view around one. */\nexport {\n RECORD_RAIL_CAPABILITIES,\n RecordRailModule,\n RecordRailProvider,\n useRecordCollectionViewSource,\n useRecordList,\n useRecordRail,\n} from './components/records/record-rail'\nexport type { RecordRailModuleProps, RecordRailPaths, RecordRailStore } from './components/records/record-rail'\nexport { lensDocumentToRecordWrite, recordLensDef, recordToLensDocument } from './components/records/record-lens'\n\n/** The portable list/edit surfaces over any source, and the scaffold that assembles one. */\nexport { CollectionViewSurface } from './components/collection-view/collection-view-surface'\nexport {\n assembleRailViewSource,\n metaBagProjection,\n normalizeOkResult,\n useRailViewSource,\n} from './components/rail-view-source'\nexport type { OkResult, RailAdapter } from './components/rail-view-source'\nexport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n FieldAccess,\n} from './lib/collection-view/collection-view-source'\n\n// --- Hooks: the cache, the locale, the breadcrumb, the rail foot -------------------------\n\nexport { useOperationCache, useOperationQuery } from './lib/query'\nexport type { OperationCache, ReadState, UseOperationQueryOptions } from './lib/query'\nexport { useStudioLocale } from './lib/locale'\nexport type { StudioLocale } from './lib/locale'\nexport { useSetBreadcrumb } from './components/breadcrumb-context'\nexport type { BreadcrumbItem } from './components/breadcrumb-context'\nexport { useNavIssues } from './components/nav-issues-registry'\nexport type { NavIssue } from './components/nav-issues-registry'\n\n// --- The document editor: contributed inspector panes (EC-618) ---------------------------\n\n/** A pane in the document editor's inspector — `defineStudio({ documentPanes: [documentPane({ … })] })`. */\nexport { documentPane } from './components/inspector/document-panes'\nexport type { DocumentPane, DocumentPaneContext } from './components/inspector/document-panes'\n\n// --- EC-613 (the forms addon): an own source's edit shell, its list key, the UI language -----\n\n/**\n * The portable edit shell alone (AD-19), with its form-column appendix seam (EC-396): an addon\n * that assembles its own source (`useRailViewSource`) renders one record's detail with\n * read-only sections below the editable fields — the forms inbox shows a submission's answers,\n * files and delivery log there.\n */\nexport { PortableDetailEditor } from './components/cms/detail-editor'\n/** The one query key of a kind's records in a project — patch it after a write (`useOperationCache`). */\nexport { recordListKey } from './components/records/record-rail'\n/** Resolve a `LocalizedString` (`'Text'` or `{ en, de, … }`) in the studio's UI language. */\nexport { useLabel } from './lib/i18n'\n"]}