@elytracms/studio 0.0.51 → 0.0.53

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 (311) hide show
  1. package/convex/auth.ts +135 -62
  2. package/convex/invites.ts +77 -0
  3. package/dist/components/account/account-settings.js +4 -4
  4. package/dist/components/account/credential-panel.js +1 -1
  5. package/dist/components/account/self-account-view-source.js +3 -3
  6. package/dist/components/ai/ai-chat-panel-lazy.js +1 -1
  7. package/dist/components/ai/ai-chat-panel.js +4 -4
  8. package/dist/components/ai/ai-cms-panel.js +1 -1
  9. package/dist/components/ai/proposal-card.js +4 -4
  10. package/dist/components/ai/translate-document-action.js +1 -1
  11. package/dist/components/ai/translate-document-dialog.d.ts +1 -1
  12. package/dist/components/app-views/default-modules.d.ts +3 -3
  13. package/dist/components/app-views/default-modules.js +20 -20
  14. package/dist/components/app-views/sidebar.d.ts +1 -1
  15. package/dist/components/app-views/sidebar.js +2 -2
  16. package/dist/components/app-views/studio-config.d.ts +1 -1
  17. package/dist/components/assets/asset-detail.js +9 -9
  18. package/dist/components/assets/asset-grid.js +7 -7
  19. package/dist/components/assets/asset-thumb.d.ts +1 -1
  20. package/dist/components/assets/asset-thumb.js +2 -2
  21. package/dist/components/assets/assets-collection-view-source.js +3 -3
  22. package/dist/components/assets/assets-module.js +8 -8
  23. package/dist/components/assets/assets-store.d.ts +1 -1
  24. package/dist/components/assets/assets-store.js +1 -1
  25. package/dist/components/builder/blocks-model.js +1 -1
  26. package/dist/components/builder/blocks-panel.d.ts +1 -1
  27. package/dist/components/builder/blocks-panel.js +6 -6
  28. package/dist/components/builder/canvas.js +11 -11
  29. package/dist/components/builder/clipboard-actions.js +2 -2
  30. package/dist/components/builder/composition-field-editor-lazy.d.ts +1 -1
  31. package/dist/components/builder/composition-field-editor-lazy.js +1 -1
  32. package/dist/components/builder/composition-field-editor.d.ts +1 -1
  33. package/dist/components/builder/composition-field-editor.js +9 -9
  34. package/dist/components/builder/insert-context.d.ts +2 -2
  35. package/dist/components/builder/insert-context.js +2 -2
  36. package/dist/components/builder/insert-menu.d.ts +2 -2
  37. package/dist/components/builder/insert-menu.js +1 -1
  38. package/dist/components/builder/layout-inspector.js +1 -1
  39. package/dist/components/builder/page-tree.js +5 -5
  40. package/dist/components/builder/property-inspector.js +3 -3
  41. package/dist/components/builder/reuse-context.d.ts +1 -1
  42. package/dist/components/builder/reuse-context.js +3 -3
  43. package/dist/components/builder/spacing-handles.js +2 -2
  44. package/dist/components/cms/asset-picker.js +4 -4
  45. package/dist/components/cms/bulk-actions.js +1 -1
  46. package/dist/components/cms/bulk-bar.d.ts +1 -1
  47. package/dist/components/cms/bulk-bar.js +3 -3
  48. package/dist/components/cms/cell-edit.js +1 -1
  49. package/dist/components/cms/cell.js +2 -2
  50. package/dist/components/cms/cms-index.js +1 -1
  51. package/dist/components/cms/cms-store.d.ts +2 -2
  52. package/dist/components/cms/cms-store.js +6 -6
  53. package/dist/components/cms/collection-table.js +17 -17
  54. package/dist/components/cms/detail-editor.js +31 -31
  55. package/dist/components/cms/document-diff-view.js +4 -4
  56. package/dist/components/cms/editable-cell.d.ts +1 -1
  57. package/dist/components/cms/editable-cell.js +6 -6
  58. package/dist/components/cms/field-control.js +15 -15
  59. package/dist/components/cms/field-renderer.d.ts +2 -2
  60. package/dist/components/cms/filter-builder.js +3 -3
  61. package/dist/components/cms/import-export.js +1 -1
  62. package/dist/components/cms/issues-pane.js +1 -1
  63. package/dist/components/cms/prop-fields.js +3 -3
  64. package/dist/components/cms/reference-confirm.js +1 -1
  65. package/dist/components/cms/relation-picker.js +1 -1
  66. package/dist/components/cms/rich-text/block-commands.d.ts +1 -1
  67. package/dist/components/cms/rich-text/component-embed.js +2 -2
  68. package/dist/components/cms/rich-text/editor.js +8 -8
  69. package/dist/components/cms/rich-text/embed-inspector.d.ts +1 -1
  70. package/dist/components/cms/rich-text/embed-inspector.js +2 -2
  71. package/dist/components/cms/rich-text/embed-picker.js +1 -1
  72. package/dist/components/cms/rich-text/embed-props.js +1 -1
  73. package/dist/components/cms/rich-text/extensions.d.ts +1 -1
  74. package/dist/components/cms/rich-text/extensions.js +4 -4
  75. package/dist/components/cms/rich-text/index.d.ts +1 -1
  76. package/dist/components/cms/rich-text/index.js +1 -1
  77. package/dist/components/cms/rich-text/insert.d.ts +1 -1
  78. package/dist/components/cms/rich-text/lazy.d.ts +1 -1
  79. package/dist/components/cms/rich-text/lazy.js +1 -1
  80. package/dist/components/cms/rich-text/slash-command.js +1 -1
  81. package/dist/components/cms/rich-text/toolbar.d.ts +1 -1
  82. package/dist/components/cms/rich-text/toolbar.js +4 -4
  83. package/dist/components/cms/use-cell-edit.d.ts +1 -1
  84. package/dist/components/cms/use-cell-edit.js +2 -2
  85. package/dist/components/cms/use-compare-selection.d.ts +1 -1
  86. package/dist/components/cms/use-discard-controller.d.ts +1 -1
  87. package/dist/components/cms/use-discard-controller.js +2 -2
  88. package/dist/components/cms/use-document-versions.d.ts +1 -1
  89. package/dist/components/cms/use-document-versions.js +1 -1
  90. package/dist/components/cms/use-embed-inspector-host.d.ts +1 -1
  91. package/dist/components/cms/use-published-perspective.d.ts +2 -2
  92. package/dist/components/cms/use-published-perspective.js +2 -2
  93. package/dist/components/cms/use-restore-version.js +1 -1
  94. package/dist/components/cms/version-compare-lazy.js +1 -1
  95. package/dist/components/cms/version-compare.js +6 -6
  96. package/dist/components/cms/versions-model.d.ts +1 -1
  97. package/dist/components/cms/versions-pane.js +5 -5
  98. package/dist/components/cms/view-bar.js +2 -2
  99. package/dist/components/collab/collab-context.js +1 -1
  100. package/dist/components/collection-view/cms-collection-view-source.js +2 -2
  101. package/dist/components/components/component-preview.js +3 -3
  102. package/dist/components/components/components-index.js +2 -2
  103. package/dist/components/components/registry-browser.js +2 -2
  104. package/dist/components/components/transfer-dialog.js +5 -5
  105. package/dist/components/components/transfer.js +4 -4
  106. package/dist/components/current-user-menu.js +2 -2
  107. package/dist/components/delivery-health-banner.js +2 -2
  108. package/dist/components/operation-error.js +1 -1
  109. package/dist/components/project-list.js +5 -5
  110. package/dist/components/project-session.js +1 -1
  111. package/dist/components/project-switcher.js +3 -3
  112. package/dist/components/rail-store-status.js +1 -1
  113. package/dist/components/redirects/redirects-collection-view-source.js +3 -3
  114. package/dist/components/redirects/redirects-module.js +6 -6
  115. package/dist/components/section-nav.js +3 -3
  116. package/dist/components/section-surface.js +1 -1
  117. package/dist/components/studio-project-screen.js +7 -7
  118. package/dist/components/studio-shell.js +9 -9
  119. package/dist/components/use-hydration-phase.js +1 -1
  120. package/dist/components/users/users-collection-view-source.js +4 -4
  121. package/dist/components/users/users-module.js +9 -9
  122. package/dist/components/workspace-projects.js +1 -1
  123. package/dist/customization.d.ts +9 -9
  124. package/dist/customization.js +5 -5
  125. package/dist/lib/ai/anthropic-provider.d.ts +1 -1
  126. package/dist/lib/ai/apply-content.d.ts +1 -1
  127. package/dist/lib/ai/apply-content.js +3 -3
  128. package/dist/lib/ai/chat-state.d.ts +2 -2
  129. package/dist/lib/ai/config.d.ts +1 -1
  130. package/dist/lib/ai/create-flow.d.ts +2 -2
  131. package/dist/lib/ai/create-flow.js +3 -3
  132. package/dist/lib/ai/fake-provider.d.ts +1 -1
  133. package/dist/lib/ai/index.d.ts +15 -15
  134. package/dist/lib/ai/index.js +15 -15
  135. package/dist/lib/ai/knowledge.d.ts +1 -1
  136. package/dist/lib/ai/provenance.d.ts +1 -1
  137. package/dist/lib/ai/route-provider.d.ts +1 -1
  138. package/dist/lib/ai/route.d.ts +1 -1
  139. package/dist/lib/ai/shape.d.ts +1 -1
  140. package/dist/lib/ai/translate-document.d.ts +1 -1
  141. package/dist/lib/ai/translate-document.js +3 -3
  142. package/dist/lib/ai-panel.js +1 -1
  143. package/dist/lib/assets/index.d.ts +4 -4
  144. package/dist/lib/assets/index.js +4 -4
  145. package/dist/lib/assets/upload.d.ts +2 -2
  146. package/dist/lib/assets/upload.js +2 -2
  147. package/dist/lib/auth/convex-auth-adapter.d.ts +53 -2
  148. package/dist/lib/auth/convex-auth-adapter.js +149 -68
  149. package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
  150. package/dist/lib/auth/dev-auto-login-adapter.d.ts +1 -1
  151. package/dist/lib/auth/index.d.ts +9 -9
  152. package/dist/lib/auth/index.js +10 -10
  153. package/dist/lib/auth/local-auth-adapter.d.ts +1 -1
  154. package/dist/lib/auth/route-guard.d.ts +1 -1
  155. package/dist/lib/auth-errors.d.ts +1 -1
  156. package/dist/lib/clipboard/index.d.ts +3 -3
  157. package/dist/lib/clipboard/index.js +3 -3
  158. package/dist/lib/cms/published-documents.d.ts +1 -1
  159. package/dist/lib/cms-views/index.d.ts +2 -2
  160. package/dist/lib/cms-views/index.js +2 -2
  161. package/dist/lib/collection-view/collection-view-source.d.ts +4 -4
  162. package/dist/lib/data/collection-content.js +1 -1
  163. package/dist/lib/data/data-store.js +6 -6
  164. package/dist/lib/data/index.d.ts +4 -4
  165. package/dist/lib/data/index.js +4 -4
  166. package/dist/lib/data/sample-capture.js +4 -4
  167. package/dist/lib/data/sources-store.d.ts +1 -1
  168. package/dist/lib/diff-view-preference.js +1 -1
  169. package/dist/lib/dnd/gap-drag.d.ts +1 -1
  170. package/dist/lib/i18n/i18n.d.ts +1 -1
  171. package/dist/lib/i18n/i18n.js +1 -1
  172. package/dist/lib/i18n/index.d.ts +4 -4
  173. package/dist/lib/i18n/index.js +3 -3
  174. package/dist/lib/i18n/messages.en.d.ts +1 -1
  175. package/dist/lib/i18n/messages.en.js +1 -1
  176. package/dist/lib/locale/binding-fallback.d.ts +1 -1
  177. package/dist/lib/locale/binding-fallback.js +2 -2
  178. package/dist/lib/locale/index.d.ts +6 -6
  179. package/dist/lib/locale/index.js +6 -6
  180. package/dist/lib/locale/locale-switcher.js +3 -3
  181. package/dist/lib/locale/use-studio-locale.js +1 -1
  182. package/dist/lib/operations/asset-repository.d.ts +1 -1
  183. package/dist/lib/operations/cms-persistence.d.ts +1 -1
  184. package/dist/lib/operations/cms-persistence.js +1 -1
  185. package/dist/lib/operations/index.d.ts +6 -6
  186. package/dist/lib/operations/index.js +6 -6
  187. package/dist/lib/operations/session-hydration.d.ts +1 -1
  188. package/dist/lib/operations/session-hydration.js +1 -1
  189. package/dist/lib/operations/studio-operations.d.ts +1 -1
  190. package/dist/lib/operations/studio-operations.js +3 -3
  191. package/dist/lib/persistence/convex-adapter.js +2 -2
  192. package/dist/lib/persistence/convex-blob-storage.d.ts +1 -1
  193. package/dist/lib/persistence/convex-blob-storage.js +1 -1
  194. package/dist/lib/persistence/deployment-readiness.d.ts +2 -2
  195. package/dist/lib/persistence/index.d.ts +6 -6
  196. package/dist/lib/persistence/index.js +7 -7
  197. package/dist/lib/persona/labels.js +1 -1
  198. package/dist/lib/query/index.d.ts +2 -2
  199. package/dist/lib/query/index.js +1 -1
  200. package/dist/lib/rail.js +1 -1
  201. package/dist/lib/references/impact.d.ts +1 -1
  202. package/dist/lib/references/impact.js +1 -1
  203. package/dist/lib/references/index.d.ts +3 -3
  204. package/dist/lib/references/index.js +3 -3
  205. package/dist/lib/references/references-store.d.ts +2 -2
  206. package/dist/lib/references/references-store.js +3 -3
  207. package/dist/lib/registry/index.js +2 -2
  208. package/dist/lib/theme.js +1 -1
  209. package/dist/lib/webhooks/config.d.ts +1 -1
  210. package/dist/lib/webhooks/config.js +2 -2
  211. package/dist/lib/webhooks/dispatcher.js +2 -2
  212. package/dist/lib/webhooks/index.d.ts +6 -6
  213. package/dist/lib/webhooks/index.js +4 -4
  214. package/dist/lib/workspace/index.d.ts +7 -7
  215. package/dist/lib/workspace/index.js +3 -3
  216. package/dist/lib/workspace/switcher-source.d.ts +1 -1
  217. package/dist/router/index.d.ts +1 -1
  218. package/dist/router/index.js +1 -1
  219. package/dist/ui/avatar.js +1 -1
  220. package/dist/ui/badge.js +1 -1
  221. package/dist/ui/button.js +1 -1
  222. package/dist/ui/card.js +1 -1
  223. package/dist/ui/collapsible-section.js +2 -2
  224. package/dist/ui/dialog.js +1 -1
  225. package/dist/ui/dropdown-menu.js +1 -1
  226. package/dist/ui/field.js +1 -1
  227. package/dist/ui/index.d.ts +19 -19
  228. package/dist/ui/index.js +19 -19
  229. package/dist/ui/input.js +1 -1
  230. package/dist/ui/inspector.js +1 -1
  231. package/dist/ui/layout.js +1 -1
  232. package/dist/ui/panel.js +2 -2
  233. package/dist/ui/popover.js +1 -1
  234. package/dist/ui/row-item.js +2 -2
  235. package/dist/ui/segmented-control.js +1 -1
  236. package/dist/ui/separator.js +1 -1
  237. package/dist/ui/toolbar.js +2 -2
  238. package/dist/ui/typography.js +1 -1
  239. package/package.json +3 -3
  240. package/dist/components/assets/asset-library.d.ts +0 -14
  241. package/dist/components/assets/asset-library.js +0 -202
  242. package/dist/components/assets/asset-library.js.map +0 -1
  243. package/dist/components/backend-gate.d.ts +0 -19
  244. package/dist/components/backend-gate.js +0 -64
  245. package/dist/components/backend-gate.js.map +0 -1
  246. package/dist/components/builder/composition-scope.d.ts +0 -16
  247. package/dist/components/builder/composition-scope.js +0 -7
  248. package/dist/components/builder/composition-scope.js.map +0 -1
  249. package/dist/components/cms/alias-selector.d.ts +0 -20
  250. package/dist/components/cms/alias-selector.js +0 -34
  251. package/dist/components/cms/alias-selector.js.map +0 -1
  252. package/dist/components/cms/collections-sidebar.d.ts +0 -18
  253. package/dist/components/cms/collections-sidebar.js +0 -36
  254. package/dist/components/cms/collections-sidebar.js.map +0 -1
  255. package/dist/components/cms/new-page-dialog.d.ts +0 -22
  256. package/dist/components/cms/new-page-dialog.js +0 -42
  257. package/dist/components/cms/new-page-dialog.js.map +0 -1
  258. package/dist/components/cms/rich-text/embed-control.d.ts +0 -17
  259. package/dist/components/cms/rich-text/embed-control.js +0 -62
  260. package/dist/components/cms/rich-text/embed-control.js.map +0 -1
  261. package/dist/components/cms/rich-text/image-control.d.ts +0 -16
  262. package/dist/components/cms/rich-text/image-control.js +0 -36
  263. package/dist/components/cms/rich-text/image-control.js.map +0 -1
  264. package/dist/components/components/fixture-runner.d.ts +0 -10
  265. package/dist/components/components/fixture-runner.js +0 -27
  266. package/dist/components/components/fixture-runner.js.map +0 -1
  267. package/dist/components/references/builder-deep-link.d.ts +0 -12
  268. package/dist/components/references/builder-deep-link.js +0 -53
  269. package/dist/components/references/builder-deep-link.js.map +0 -1
  270. package/dist/components/routes/redirect-chain.d.ts +0 -9
  271. package/dist/components/routes/redirect-chain.js +0 -16
  272. package/dist/components/routes/redirect-chain.js.map +0 -1
  273. package/dist/components/routes/redirect-manager.d.ts +0 -13
  274. package/dist/components/routes/redirect-manager.js +0 -24
  275. package/dist/components/routes/redirect-manager.js.map +0 -1
  276. package/dist/components/routes/route-list.d.ts +0 -35
  277. package/dist/components/routes/route-list.js +0 -69
  278. package/dist/components/routes/route-list.js.map +0 -1
  279. package/dist/components/routes/routes-shell.d.ts +0 -14
  280. package/dist/components/routes/routes-shell.js +0 -68
  281. package/dist/components/routes/routes-shell.js.map +0 -1
  282. package/dist/components/sections.d.ts +0 -37
  283. package/dist/components/sections.js +0 -62
  284. package/dist/components/sections.js.map +0 -1
  285. package/dist/components/users/user-search.d.ts +0 -8
  286. package/dist/components/users/user-search.js +0 -13
  287. package/dist/components/users/user-search.js.map +0 -1
  288. package/dist/components/workspace-gate.d.ts +0 -12
  289. package/dist/components/workspace-gate.js +0 -28
  290. package/dist/components/workspace-gate.js.map +0 -1
  291. package/dist/lib/cms-views/views-store.d.ts +0 -70
  292. package/dist/lib/cms-views/views-store.js +0 -129
  293. package/dist/lib/cms-views/views-store.js.map +0 -1
  294. package/dist/lib/operations/graph-commands.d.ts +0 -136
  295. package/dist/lib/operations/graph-commands.js +0 -85
  296. package/dist/lib/operations/graph-commands.js.map +0 -1
  297. package/dist/lib/operations/graph-persistence.d.ts +0 -27
  298. package/dist/lib/operations/graph-persistence.js +0 -35
  299. package/dist/lib/operations/graph-persistence.js.map +0 -1
  300. package/dist/lib/routing/index.d.ts +0 -10
  301. package/dist/lib/routing/index.js +0 -11
  302. package/dist/lib/routing/index.js.map +0 -1
  303. package/dist/lib/routing/routing-analysis.d.ts +0 -26
  304. package/dist/lib/routing/routing-analysis.js +0 -37
  305. package/dist/lib/routing/routing-analysis.js.map +0 -1
  306. package/dist/lib/routing/routing-store.d.ts +0 -50
  307. package/dist/lib/routing/routing-store.js +0 -72
  308. package/dist/lib/routing/routing-store.js.map +0 -1
  309. package/dist/lib/working-graph.d.ts +0 -82
  310. package/dist/lib/working-graph.js +0 -113
  311. package/dist/lib/working-graph.js.map +0 -1
package/convex/auth.ts CHANGED
@@ -1,11 +1,140 @@
1
1
  import { convexAuth } from '@convex-dev/auth/server'
2
2
  import { Password } from '@convex-dev/auth/providers/Password'
3
+ import type { AnyDataModel, GenericMutationCtx } from 'convex/server'
3
4
  import { ConvexError } from 'convex/values'
5
+ import type { GenericId } from 'convex/values'
4
6
  import { classifyInviteSignup, normalizeInviteEmail } from '@elytracms/core/persistence'
7
+ import type { InviteSignupDecision } from '@elytracms/core/persistence'
5
8
 
6
9
  /** The provider list `convexAuth` accepts, derived from its own signature. */
7
10
  type ConvexAuthProviders = Parameters<typeof convexAuth>[0]['providers']
8
11
 
12
+ /** The (non-schema-bound) mutation ctx the `convexAuth` callbacks receive. */
13
+ type AuthCallbackCtx = GenericMutationCtx<AnyDataModel>
14
+
15
+ /**
16
+ * Gather the invite-gate facts for a principal and classify them (EC-324).
17
+ * The ONE decision lives in the pure `classifyInviteSignup`
18
+ * (`@elytracms/core/persistence`, shared with its tests):
19
+ *
20
+ * - **existing member** → normal re-sign-in; no-op (also recovers a member-less
21
+ * account it created earlier and never lets the deployment lock itself out).
22
+ * - **first user** (no members yet) → bootstrap as `admin`.
23
+ * - **invited** → grant the invited role (+ consume the invite).
24
+ * - **uninvited** → REJECT (closes the EC-304 open-sign-up hole).
25
+ */
26
+ async function decideInviteGate(
27
+ ctx: AuthCallbackCtx,
28
+ args: { userId: GenericId<'users'>; email: string | null },
29
+ ): Promise<{ decision: InviteSignupDecision; invite: { _id: string; role: string } | null }> {
30
+ // The convexAuth callback ctx is `AnyDataModel` (not schema-bound), so use
31
+ // `.filter()` rather than `.withIndex()` here. The members/invites tables are
32
+ // tiny (single-tenant), and this runs only at account creation / sign-in.
33
+ const existingMember = await ctx.db
34
+ .query('members')
35
+ .filter((q) => q.eq(q.field('userId'), args.userId))
36
+ .first()
37
+ const anyMember = existingMember ?? (await ctx.db.query('members').first())
38
+ const email = args.email ? normalizeInviteEmail(args.email) : null
39
+ const invite = email
40
+ ? await ctx.db
41
+ .query('invites')
42
+ .filter((q) => q.eq(q.field('email'), email))
43
+ .first()
44
+ : null
45
+
46
+ const decision = classifyInviteSignup({
47
+ hasAnyMember: anyMember !== null,
48
+ existingRole: existingMember?.role ?? null,
49
+ invite: invite ? { role: invite.role } : null,
50
+ })
51
+ return { decision, invite }
52
+ }
53
+
54
+ /**
55
+ * EC-358 — the ACCOUNT-CREATION gate. Wired as `afterUserCreatedOrUpdated`,
56
+ * which Convex Auth calls inside the SAME `auth:store` mutation that creates
57
+ * the principal's rows — after the `users` insert, **before** the
58
+ * `authAccounts` insert (`upsertUserAndAccount`). A Convex mutation is one
59
+ * atomic transaction, so throwing here rolls the `users` insert back and the
60
+ * `authAccounts` row is never written: an uninvited sign-up leaves ZERO rows.
61
+ *
62
+ * Why this hook: the EC-324 gate lived only in `beforeSessionCreation`, which
63
+ * runs in a LATER store mutation — by then the password `authAccounts` + `users`
64
+ * rows were already committed, so a rejected uninvited sign-up left an orphan
65
+ * account carrying an attacker-chosen password that a later invite would
66
+ * silently promote (proven live 2026-07-08). The alternative pre-create hook,
67
+ * `createOrUpdateUser`, REPLACES the library's user-creation/linking mechanics
68
+ * wholesale — a far bigger seam than this scope allows; this hook keeps the
69
+ * default mechanics and only adds the veto.
70
+ *
71
+ * Only REJECT throws here; membership granting/consuming stays in
72
+ * `beforeSessionCreation` (the account-creation callback also fires for
73
+ * verification/OAuth account flows where a session may not follow immediately).
74
+ * For credentials providers this fires exactly on `createAccount` (sign-UP),
75
+ * not on re-sign-in.
76
+ */
77
+ export async function gateAccountCreation(
78
+ ctx: AuthCallbackCtx,
79
+ args: { userId: GenericId<'users'>; profile: { email?: string } },
80
+ ): Promise<void> {
81
+ const email = typeof args.profile.email === 'string' ? args.profile.email : null
82
+ const { decision } = await decideInviteGate(ctx, { userId: args.userId, email })
83
+ if (decision.kind === 'reject') throw new ConvexError(decision.reason)
84
+ }
85
+
86
+ /**
87
+ * Invite-based onboarding gate + first-principal bootstrap (EC-324 / EC-150).
88
+ * Wired as `beforeSessionCreation`: fires on EVERY sign-in (not just account
89
+ * creation), right before the session is persisted — throwing here rejects the
90
+ * sign-in. See {@link decideInviteGate} for the decision table.
91
+ *
92
+ * The `ConvexError` message ("not-invited: …") survives to the client even in
93
+ * production, so the sign-in screen tells the truth instead of the old
94
+ * "password too short". The `ConvexAuthAdapter` classifies it to a distinct
95
+ * `not-invited` `SignInResult` reason.
96
+ *
97
+ * Single-tenant: one `members` table per deployment. Invite mechanism: a plain
98
+ * email allowlist (`invites` table) — the simplest server-enforced design, no
99
+ * token infrastructure.
100
+ */
101
+ export async function gateSessionCreation(
102
+ ctx: AuthCallbackCtx,
103
+ args: { userId: GenericId<'users'> },
104
+ ): Promise<void> {
105
+ const now = new Date().toISOString()
106
+ const user = await ctx.db.get(args.userId)
107
+ const rawEmail = (user as { email?: string } | null)?.email
108
+ const { decision, invite } = await decideInviteGate(ctx, {
109
+ userId: args.userId,
110
+ email: rawEmail ?? null,
111
+ })
112
+
113
+ switch (decision.kind) {
114
+ case 'existing-member':
115
+ return
116
+ case 'bootstrap-admin':
117
+ await ctx.db.insert('members', {
118
+ userId: args.userId,
119
+ role: 'admin',
120
+ createdAt: now,
121
+ updatedAt: now,
122
+ })
123
+ return
124
+ case 'grant-role':
125
+ await ctx.db.insert('members', {
126
+ userId: args.userId,
127
+ role: decision.role,
128
+ createdAt: now,
129
+ updatedAt: now,
130
+ })
131
+ if (invite) await ctx.db.delete(invite._id as GenericId<'invites'>)
132
+ return
133
+ case 'reject':
134
+ throw new ConvexError(decision.reason)
135
+ }
136
+ }
137
+
9
138
  /**
10
139
  * EC-252 — the studio's Convex Auth (EC-150) as a **default + override seam**. Only the
11
140
  * PROVIDER choice is legitimately consumer-specific; the first-principal admin bootstrap
@@ -30,69 +159,13 @@ export function defineStudioAuth(options?: { providers?: ConvexAuthProviders })
30
159
  return convexAuth({
31
160
  providers: options?.providers ?? [Password],
32
161
  callbacks: {
33
- /**
34
- * Invite-based onboarding gate + first-principal bootstrap (EC-324 / EC-150).
35
- * Fires on EVERY sign-in (not just account creation), right before the session
36
- * is persisted — throwing here rejects the sign-in. The ONE decision lives in
37
- * the pure `classifyInviteSignup` (shared with its tests):
38
- *
39
- * - **existing member** → normal re-sign-in; no-op (also recovers a member-less
40
- * account it created earlier and never lets the deployment lock itself out).
41
- * - **first user** (no members yet) → bootstrap as `admin` (was the only case).
42
- * - **invited** → grant the invited role + CONSUME the invite (delete the row).
43
- * - **uninvited** → REJECT. This closes the open-sign-up hole (EC-304): an
44
- * anonymous person can no longer self-register into the deployment. The
45
- * `ConvexError` message ("not-invited: …") survives to the client even in
46
- * production, so the sign-in screen tells the truth.
47
- *
48
- * Single-tenant: one `members` table per deployment. Invite mechanism: a plain
49
- * email allowlist (`invites` table) — the simplest server-enforced design, no
50
- * token infrastructure.
51
- */
162
+ /** EC-358: reject uninvited account creation BEFORE any row commits. */
163
+ async afterUserCreatedOrUpdated(ctx, { userId, profile }) {
164
+ await gateAccountCreation(ctx, { userId, profile })
165
+ },
166
+ /** EC-324: the every-sign-in gate — bootstrap / grant / consume / reject. */
52
167
  async beforeSessionCreation(ctx, { userId }) {
53
- const now = new Date().toISOString()
54
- // The convexAuth callback ctx is `AnyDataModel` (not schema-bound), so use
55
- // `.filter()` rather than `.withIndex()` here. The members/invites tables are
56
- // tiny (single-tenant), and this runs only at sign-in.
57
- const existingMember = await ctx.db
58
- .query('members')
59
- .filter((q) => q.eq(q.field('userId'), userId))
60
- .first()
61
- const anyMember = existingMember ?? (await ctx.db.query('members').first())
62
- const user = await ctx.db.get(userId)
63
- const rawEmail = (user as { email?: string } | null)?.email
64
- const email = rawEmail ? normalizeInviteEmail(rawEmail) : null
65
- const invite = email
66
- ? await ctx.db
67
- .query('invites')
68
- .filter((q) => q.eq(q.field('email'), email))
69
- .first()
70
- : null
71
-
72
- const decision = classifyInviteSignup({
73
- hasAnyMember: anyMember !== null,
74
- existingRole: existingMember?.role ?? null,
75
- invite: invite ? { role: invite.role } : null,
76
- })
77
-
78
- switch (decision.kind) {
79
- case 'existing-member':
80
- return
81
- case 'bootstrap-admin':
82
- await ctx.db.insert('members', { userId, role: 'admin', createdAt: now, updatedAt: now })
83
- return
84
- case 'grant-role':
85
- await ctx.db.insert('members', {
86
- userId,
87
- role: decision.role,
88
- createdAt: now,
89
- updatedAt: now,
90
- })
91
- if (invite) await ctx.db.delete(invite._id)
92
- return
93
- case 'reject':
94
- throw new ConvexError(decision.reason)
95
- }
168
+ await gateSessionCreation(ctx, { userId })
96
169
  },
97
170
  },
98
171
  })
package/convex/invites.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { v } from 'convex/values'
2
+ import { selectOrphanCredentialPurges } from '@elytracms/core/persistence'
2
3
  import { mutation, query } from './functions'
4
+ import type { MutationCtx } from './functions'
3
5
  import { requireCapability } from './guard'
4
6
 
5
7
  /**
@@ -17,6 +19,78 @@ import { requireCapability } from './guard'
17
19
 
18
20
  const normalizeEmail = (email: string): string => email.trim().toLowerCase()
19
21
 
22
+ /**
23
+ * EC-358 — an invite must never adopt pre-existing orphan credentials.
24
+ *
25
+ * Threat: an attacker pre-registers a password for someone's email; before
26
+ * EC-358's account-creation gate that left an orphan (member-less)
27
+ * `users` + `authAccounts` pair, and a later invite for that email would have
28
+ * silently promoted the ATTACKER's credentials at their next sign-in. Creating
29
+ * an invite is therefore the purge chokepoint: purging at gate-promotion time
30
+ * would be too late by construction — the promotion is *triggered by* whoever
31
+ * holds the password. Purged here, the invited person always sets their own.
32
+ *
33
+ * WHAT is purged, per member-less user matching the invited email (selection is
34
+ * the pure `selectOrphanCredentialPurges`, normalized-email matched): their
35
+ * `authSessions` (+ `authRefreshTokens`), `authAccounts`
36
+ * (+ `authVerificationCodes`), and the `users` row itself — so no stale session
37
+ * or reset code survives the credential purge. A user with a `members` row is
38
+ * NEVER touched, whatever their email. Runs in the invite-creation mutation, so
39
+ * purge + invite land in one atomic transaction.
40
+ *
41
+ * The `users` table is tiny (single-tenant studio principals), so the
42
+ * collect-and-match scan is fine — and required, because the auth provider
43
+ * stores emails as typed at sign-up (an index lookup on the normalized email
44
+ * would miss case-varied orphans the sign-in gate would still promote).
45
+ */
46
+ export async function purgeOrphanCredentials(
47
+ ctx: Pick<MutationCtx, 'db'>,
48
+ invitedEmail: string,
49
+ ): Promise<number> {
50
+ const members = await ctx.db.query('members').collect()
51
+ const memberIds = new Set(members.map((member) => member.userId))
52
+ const users = await ctx.db.query('users').collect()
53
+ const purgeIds = new Set(
54
+ selectOrphanCredentialPurges(
55
+ invitedEmail,
56
+ users.map((user) => ({
57
+ id: String(user._id),
58
+ email: user.email ?? null,
59
+ isMember: memberIds.has(String(user._id)),
60
+ })),
61
+ ),
62
+ )
63
+ const orphans = users.filter((user) => purgeIds.has(String(user._id)))
64
+ for (const orphan of orphans) {
65
+ const sessions = await ctx.db
66
+ .query('authSessions')
67
+ .withIndex('userId', (q) => q.eq('userId', orphan._id))
68
+ .collect()
69
+ for (const session of sessions) {
70
+ const tokens = await ctx.db
71
+ .query('authRefreshTokens')
72
+ .withIndex('sessionId', (q) => q.eq('sessionId', session._id))
73
+ .collect()
74
+ for (const token of tokens) await ctx.db.delete(token._id)
75
+ await ctx.db.delete(session._id)
76
+ }
77
+ const accounts = await ctx.db
78
+ .query('authAccounts')
79
+ .withIndex('userIdAndProvider', (q) => q.eq('userId', orphan._id))
80
+ .collect()
81
+ for (const account of accounts) {
82
+ const codes = await ctx.db
83
+ .query('authVerificationCodes')
84
+ .withIndex('accountId', (q) => q.eq('accountId', account._id))
85
+ .collect()
86
+ for (const code of codes) await ctx.db.delete(code._id)
87
+ await ctx.db.delete(account._id)
88
+ }
89
+ await ctx.db.delete(orphan._id)
90
+ }
91
+ return orphans.length
92
+ }
93
+
20
94
  export const list = query({
21
95
  args: {},
22
96
  handler: async (ctx) => {
@@ -37,6 +111,9 @@ export const create = mutation({
37
111
  handler: async (ctx, args) => {
38
112
  await requireCapability(ctx, 'project.administer')
39
113
  const email = normalizeEmail(args.email)
114
+ // EC-358: never adopt orphan credentials — purge member-less auth rows for
115
+ // this email atomically with the invite, so the invitee sets their own password.
116
+ await purgeOrphanCredentials(ctx, email)
40
117
  const existing = await ctx.db
41
118
  .query('invites')
42
119
  .withIndex('by_email', (q) => q.eq('email', email))
@@ -3,10 +3,10 @@ import { UserCog } from 'lucide-react';
3
3
  import { Heading, Stack, Text } from '@elytracms/studio/ui';
4
4
  import { listStoreState, useOperationQuery } from '@elytracms/studio/lib/query';
5
5
  import { translate } from '@elytracms/studio/lib/i18n';
6
- import { useAuth } from '../auth-context';
7
- import { PortableDetailEditor } from '../cms/detail-editor';
8
- import { CredentialPanel } from './credential-panel';
9
- import { useSelfAccountViewSource } from './self-account-view-source';
6
+ import { useAuth } from '../auth-context.js';
7
+ import { PortableDetailEditor } from '../cms/detail-editor.js';
8
+ import { CredentialPanel } from './credential-panel.js';
9
+ import { useSelfAccountViewSource } from './self-account-view-source.js';
10
10
  /**
11
11
  * The **account-settings** surface (EC-240 H-tail items 1–4). A member — admin or
12
12
  * not — opens their OWN record here (via the user menu → `users/<selfId>`). It is
@@ -3,7 +3,7 @@ import { useState } from 'react';
3
3
  import { KeyRound, ShieldCheck } from 'lucide-react';
4
4
  import { Button, Card, CardContent, Field, Heading, Input, Row, Stack, Text, toast, } from '@elytracms/studio/ui';
5
5
  import { translate } from '@elytracms/studio/lib/i18n';
6
- import { useAuth } from '../auth-context';
6
+ import { useAuth } from '../auth-context.js';
7
7
  /**
8
8
  * The credential seam (EC-240 H-tail item 3) — part of the account **property
9
9
  * surface**, never a modal. Two blocks:
@@ -1,8 +1,8 @@
1
1
  import { useCallback } from 'react';
2
2
  import { studioPath } from '@elytracms/studio/router';
3
- import { useAuth } from '../auth-context';
4
- import { selfAccountCollectionDef } from './self-account-schema';
5
- import { normalizeOkResult, useRailViewSource } from '../rail-view-source';
3
+ import { useAuth } from '../auth-context.js';
4
+ import { selfAccountCollectionDef } from './self-account-schema.js';
5
+ import { normalizeOkResult, useRailViewSource } from '../rail-view-source.js';
6
6
  /** The account surface is not localized — one implicit locale keeps the lens honest. */
7
7
  const ACCOUNT_LOCALE = 'en';
8
8
  const ACCOUNT_LOCALE_CONFIG = { default: ACCOUNT_LOCALE, locales: [ACCOUNT_LOCALE] };
@@ -11,7 +11,7 @@ import { translate } from '@elytracms/studio/lib/i18n';
11
11
  * critical path (pinned by the `lib/ai` bundle test). Same pattern as
12
12
  * `version-compare-lazy` / `rich-text/lazy`.
13
13
  */
14
- const AiChatPanelImpl = lazy(() => import('./ai-chat-panel').then((module) => ({ default: module.AiChatPanel })));
14
+ const AiChatPanelImpl = lazy(() => import('./ai-chat-panel.js').then((module) => ({ default: module.AiChatPanel })));
15
15
  function AiPanelFallback() {
16
16
  return (_jsx("aside", { className: "flex h-full min-h-0 w-[360px] shrink-0 flex-col border-l border-border bg-card", children: _jsx("div", { className: "grid min-h-0 flex-1 place-items-center p-6", children: _jsx(Text, { size: "sm", tone: "muted", children: translate('ai.panel.loading') }) }) }));
17
17
  }
@@ -8,10 +8,10 @@ import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio
8
8
  import { resolveDeploymentAi } from '@elytracms/studio/lib/workspace';
9
9
  import { getStudioOperations } from '@elytracms/studio/lib/operations';
10
10
  import { AiRouteError, chatReducer, initialChatState, resolveAiConfig, resolveKnowledge, runCreateFlow, } from '@elytracms/studio/lib/ai';
11
- import { useCms } from '../cms/cms-store';
12
- import { validateComposition, validateRichTextEmbeds } from '../cms/cms-composition-validators';
13
- import { AssistantTurn, Conversation, ConversationEmpty, PromptInput, ToolStatus, UserTurn, } from './chat-ui';
14
- import { ProposalCard } from './proposal-card';
11
+ import { useCms } from '../cms/cms-store.js';
12
+ import { validateComposition, validateRichTextEmbeds } from '../cms/cms-composition-validators.js';
13
+ import { AssistantTurn, Conversation, ConversationEmpty, PromptInput, ToolStatus, UserTurn, } from './chat-ui.js';
14
+ import { ProposalCard } from './proposal-card.js';
15
15
  import { setAiPanelOpen } from '@elytracms/studio/lib/ai-panel';
16
16
  /**
17
17
  * The AI assistant chat panel (EC-199) — the right-hand collapsible surface in the CMS
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useMemo } from 'react';
3
3
  import { resolveDeploymentAi } from '@elytracms/studio/lib/workspace';
4
4
  import { useAiPanelOpen } from '@elytracms/studio/lib/ai-panel';
5
- import { AiChatPanel } from './ai-chat-panel-lazy';
5
+ import { AiChatPanel } from './ai-chat-panel-lazy.js';
6
6
  /**
7
7
  * The CMS-area mount of the AI assistant (EC-199): a right-hand sibling of the CMS
8
8
  * content, shown on BOTH the collection list and the document editor. Gated twice —
@@ -6,10 +6,10 @@ import { resolveLabel } from '@elytracms/core/cms-core';
6
6
  import { Badge, Button, Row, Spacer, Stack, Text } from '@elytracms/studio/ui';
7
7
  import { translate } from '@elytracms/studio/lib/i18n';
8
8
  import { proposalDocuments } from '@elytracms/studio/lib/ai';
9
- import { useCms } from '../cms/cms-store';
10
- import { useCmsCollectionViewSource } from '../collection-view/cms-collection-view-source';
11
- import { CollectionViewProvider } from '../collection-view/collection-view-context';
12
- import { DocumentDiffView } from '../cms/document-diff-view';
9
+ import { useCms } from '../cms/cms-store.js';
10
+ import { useCmsCollectionViewSource } from '../collection-view/cms-collection-view-source.js';
11
+ import { CollectionViewProvider } from '../collection-view/collection-view-context.js';
12
+ import { DocumentDiffView } from '../cms/document-diff-view.js';
13
13
  /**
14
14
  * The AI proposal review (EC-199): a valid, leash-compliant draft the editor reviews
15
15
  * as a DIFF (the shared EC-317 {@link DocumentDiffView} — never a second renderer)
@@ -16,7 +16,7 @@ import { resolveDeploymentAi } from '@elytracms/studio/lib/workspace';
16
16
  * the translation engine + operations write) loads ONLY when the editor opens it, via a
17
17
  * dynamic `import()`, so an unconfigured studio pulls none of it onto the critical path.
18
18
  */
19
- const TranslateDocumentDialog = lazy(() => import('./translate-document-dialog').then((m) => ({ default: m.TranslateDocumentDialog })));
19
+ const TranslateDocumentDialog = lazy(() => import('./translate-document-dialog.js').then((m) => ({ default: m.TranslateDocumentDialog })));
20
20
  export function TranslateDocumentAction(props) {
21
21
  const [open, setOpen] = useState(false);
22
22
  const { collection, localeConfig, localesEnabled, projectId } = props;
@@ -1,4 +1,4 @@
1
- import type { TranslateDocumentActionProps } from './translate-document-action';
1
+ import type { TranslateDocumentActionProps } from './translate-document-action.js';
2
2
  export declare function TranslateDocumentDialog({ projectId, collection, docRef, document, localeConfig, onApplied, open, onOpenChange, }: TranslateDocumentActionProps & {
3
3
  open: boolean;
4
4
  onOpenChange: (open: boolean) => void;
@@ -4,7 +4,7 @@ import type { CollectionDef, CollectionFieldOverride, CollectionFormMeta } from
4
4
  import type { SectionId } from '@elytracms/core/studio-core';
5
5
  import type { InstantiatedKit } from '@elytracms/core/starter-kits';
6
6
  import type { HydratedCms } from '@elytracms/studio/lib/operations';
7
- import { type AppViewContext, type AppViewDeepLink, type AppViewModule, type NavPermission, type SidebarViewNode } from './module';
7
+ import { type AppViewContext, type AppViewDeepLink, type AppViewModule, type NavPermission, type SidebarViewNode } from './module.js';
8
8
  /**
9
9
  * EC-237/239 — the studio's BUILT-IN views + the consumer-facing `nav` namespace,
10
10
  * an instance of the EC-236 customization seam.
@@ -188,9 +188,9 @@ export declare const nav: {
188
188
  /** A fully custom view — pass your own `render` (and optional `parsePath`). */
189
189
  readonly view: (config: CustomViewConfig) => SidebarViewNode;
190
190
  /** A labelled group of views (a header with sub-entries). */
191
- readonly group: (label: string, children: SidebarViewNode[]) => import("./module").SidebarGroupNode;
191
+ readonly group: (label: string, children: SidebarViewNode[]) => import("./module.js").SidebarGroupNode;
192
192
  /** A divider between rail sections. */
193
- readonly separator: () => import("./module").SidebarSeparatorNode;
193
+ readonly separator: () => import("./module.js").SidebarSeparatorNode;
194
194
  };
195
195
  /**
196
196
  * The CMS schema source for a project mount (bug fix, playground-collections):
@@ -8,26 +8,26 @@ import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
8
8
  import { getInstantiatedKit } from '@elytracms/studio/lib/starters/kit-session';
9
9
  import { isPlaygroundMode } from '@elytracms/studio/lib/persistence';
10
10
  import { resolveDeploymentCollections } from '@elytracms/studio/lib/workspace';
11
- import { cmsDeepLink, componentsDeepLink, usersDeepLink, redirectsDeepLink, assetsDeepLink, groupNode, separatorNode, viewNode, } from './module';
12
- import { SectionSurface } from '../section-surface';
13
- import { SectionStateView } from '../section-state';
14
- import { SettingsForm } from '../settings-form';
15
- import { AssetsModule } from '../assets/assets-module';
16
- import { UsersModule } from '../users/users-module';
17
- import { RedirectsModule } from '../redirects/redirects-module';
18
- import { redirectsCollectionDef } from '../redirects/redirects-schema';
19
- import { ExtensionsView } from '../extensions/extensions-view';
20
- import { RegistryBrowser } from '../components/registry-browser';
21
- import { ComponentsIndex } from '../components/components-index';
22
- import { ComponentDetail } from '../components/component-detail';
23
- import { CmsSessionGate } from '../project-session';
24
- import { CmsProvider, useCms } from '../cms/cms-store';
25
- import { AiCmsPanel } from '../ai/ai-cms-panel';
26
- import { focusCollectionList } from '../cms/collection-focus';
27
- import { CmsIndex } from '../cms/cms-index';
28
- import { CollectionTable } from '../cms/collection-table';
29
- import { DetailEditor } from '../cms/detail-editor';
30
- import { OperationErrorBanner } from '../operation-error';
11
+ import { cmsDeepLink, componentsDeepLink, usersDeepLink, redirectsDeepLink, assetsDeepLink, groupNode, separatorNode, viewNode, } from './module.js';
12
+ import { SectionSurface } from '../section-surface.js';
13
+ import { SectionStateView } from '../section-state.js';
14
+ import { SettingsForm } from '../settings-form.js';
15
+ import { AssetsModule } from '../assets/assets-module.js';
16
+ import { UsersModule } from '../users/users-module.js';
17
+ import { RedirectsModule } from '../redirects/redirects-module.js';
18
+ import { redirectsCollectionDef } from '../redirects/redirects-schema.js';
19
+ import { ExtensionsView } from '../extensions/extensions-view.js';
20
+ import { RegistryBrowser } from '../components/registry-browser.js';
21
+ import { ComponentsIndex } from '../components/components-index.js';
22
+ import { ComponentDetail } from '../components/component-detail.js';
23
+ import { CmsSessionGate } from '../project-session.js';
24
+ import { CmsProvider, useCms } from '../cms/cms-store.js';
25
+ import { AiCmsPanel } from '../ai/ai-cms-panel.js';
26
+ import { focusCollectionList } from '../cms/collection-focus.js';
27
+ import { CmsIndex } from '../cms/cms-index.js';
28
+ import { CollectionTable } from '../cms/collection-table.js';
29
+ import { DetailEditor } from '../cms/detail-editor.js';
30
+ import { OperationErrorBanner } from '../operation-error.js';
31
31
  function slugify(label) {
32
32
  return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'view';
33
33
  }
@@ -1,7 +1,7 @@
1
1
  import type { LucideIcon } from 'lucide-react';
2
2
  import type { CustomizationIssue, Project, SectionAvailability } from '@elytracms/core/studio-core';
3
3
  import type { CollectionDef } from '@elytracms/core/cms-core';
4
- import { type AppViewModule, type NavPermission, type NavPermissionContext, type SidebarNode } from './module';
4
+ import { type AppViewModule, type NavPermission, type NavPermissionContext, type SidebarNode } from './module.js';
5
5
  /**
6
6
  * EC-237 — resolving the configurable section rail, an instance of the EC-236
7
7
  * customization seam.
@@ -1,8 +1,8 @@
1
1
  import { useMemo } from 'react';
2
2
  import { resolveOverrides } from '@elytracms/core/studio-core';
3
3
  import { resolveDeploymentCollections, resolveDeploymentSidebar } from '@elytracms/studio/lib/workspace';
4
- import { firstAvailableView, moduleAvailability, viewNode, } from './module';
5
- import { DEFAULT_VIEWS } from './default-modules';
4
+ import { firstAvailableView, moduleAvailability, viewNode, } from './module.js';
5
+ import { DEFAULT_VIEWS } from './default-modules.js';
6
6
  /** The default rail: every non-hidden built-in view as a plain entry, in canonical order. */
7
7
  function defaultRail() {
8
8
  return DEFAULT_VIEWS.filter((view) => !view.hidden).map(viewNode);
@@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
2
2
  import type { FieldDef } from '@elytracms/core/cms-core';
3
3
  import type { StudioI18nConfig } from '@elytracms/studio/lib/i18n';
4
4
  import type { StudioAiConfig } from '@elytracms/studio/lib/ai';
5
- import type { SidebarNode } from './module';
5
+ import type { SidebarNode } from './module.js';
6
6
  /**
7
7
  * EC-236 — the studio customization config: the single, studio-only home for
8
8
  * everything a consuming repo customizes about the builder UI.
@@ -5,15 +5,15 @@ import { Link, STUDIO_PROJECT_ROUTE, useNavigate } from '@elytracms/studio/route
5
5
  import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
6
6
  import { buildAssetReferenceImpact, loadInboundAssetReferenceViews } from '@elytracms/studio/lib/references';
7
7
  import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Field, Heading, Panel, PanelBody, PanelHeader, Row, Spacer, Stack, Text, } from '@elytracms/studio/ui';
8
- import { FieldControl } from '../cms/field-control';
9
- import { fieldLabel } from '../cms/cms-labels';
10
- import { ReferenceImpactRows } from '../cms/reference-confirm';
11
- import { ReferencesPanel } from '../references/references-panel';
12
- import { useSetBreadcrumb } from '../breadcrumb-context';
13
- import { useCollectionView } from '../collection-view/collection-view-context';
14
- import { FocalCropPreviews, FocalPointPicker, useAssetRecord } from './asset-thumb';
15
- import { assetMetaFieldsOf, formatBytes, pruneMeta } from './asset-metadata';
16
- import { useAssets } from './assets-store';
8
+ import { FieldControl } from '../cms/field-control.js';
9
+ import { fieldLabel } from '../cms/cms-labels.js';
10
+ import { ReferenceImpactRows } from '../cms/reference-confirm.js';
11
+ import { ReferencesPanel } from '../references/references-panel.js';
12
+ import { useSetBreadcrumb } from '../breadcrumb-context.js';
13
+ import { useCollectionView } from '../collection-view/collection-view-context.js';
14
+ import { FocalCropPreviews, FocalPointPicker, useAssetRecord } from './asset-thumb.js';
15
+ import { assetMetaFieldsOf, formatBytes, pruneMeta } from './asset-metadata.js';
16
+ import { useAssets } from './assets-store.js';
17
17
  /**
18
18
  * The assets rail's DETAIL surface (EC-319). Pragmatic call: assets don't fit
19
19
  * the portable `PortableDetailEditor` shell cleanly — there's no dirty/Save/
@@ -5,13 +5,13 @@ import { Link, STUDIO_PROJECT_ROUTE } from '@elytracms/studio/router';
5
5
  import { applyViewState } from '@elytracms/studio/lib/cms-views';
6
6
  import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
7
7
  import { Code, Grid, Panel, Row, Separator, Spacer, Stack, Text } from '@elytracms/studio/ui';
8
- import { FilterControl } from '../cms/filter-builder';
9
- import { ViewTabs } from '../cms/view-bar';
10
- import { useSetBreadcrumb } from '../breadcrumb-context';
11
- import { useCollectionView } from '../collection-view/collection-view-context';
12
- import { AssetThumb } from './asset-thumb';
13
- import { formatBytes } from './asset-metadata';
14
- import { useAssets } from './assets-store';
8
+ import { FilterControl } from '../cms/filter-builder.js';
9
+ import { ViewTabs } from '../cms/view-bar.js';
10
+ import { useSetBreadcrumb } from '../breadcrumb-context.js';
11
+ import { useCollectionView } from '../collection-view/collection-view-context.js';
12
+ import { AssetThumb } from './asset-thumb.js';
13
+ import { formatBytes } from './asset-metadata.js';
14
+ import { useAssets } from './assets-store.js';
15
15
  /**
16
16
  * The assets rail's GRID surface (EC-319) — the thumbnail-first counterpart to
17
17
  * `LoadedCollectionTable`, over the SAME portable `CollectionViewSource` (the
@@ -1,5 +1,5 @@
1
1
  import type { AssetRecord } from '@elytracms/core/persistence';
2
- import type { FocalPoint } from './focal-point';
2
+ import type { FocalPoint } from './focal-point.js';
3
3
  /** Resolve an asset record by id from the ambient library, if one is mounted. */
4
4
  export declare function useAssetRecord(assetId: string | null | undefined): AssetRecord | undefined;
5
5
  /**
@@ -4,8 +4,8 @@ import { Image } from 'lucide-react';
4
4
  import { Text } from '@elytracms/studio/ui';
5
5
  import { translate } from '@elytracms/studio/lib/i18n';
6
6
  import { cn } from '@elytracms/studio/lib/utils';
7
- import { useOptionalAssets } from './assets-store';
8
- import { CENTER_FOCAL_POINT, focalPointFromPoint, focalPointObjectPosition, } from './focal-point';
7
+ import { useOptionalAssets } from './assets-store.js';
8
+ import { CENTER_FOCAL_POINT, focalPointFromPoint, focalPointObjectPosition, } from './focal-point.js';
9
9
  /** Resolve an asset record by id from the ambient library, if one is mounted. */
10
10
  export function useAssetRecord(assetId) {
11
11
  const store = useOptionalAssets();
@@ -1,9 +1,9 @@
1
1
  import { useCallback, useMemo } from 'react';
2
2
  import { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core';
3
3
  import { studioPath } from '@elytracms/studio/router';
4
- import { useAssets } from './assets-store';
5
- import { SYSTEM_ASSET_FIELDS } from './asset-metadata';
6
- import { normalizeOkResult, useRailViewSource } from '../rail-view-source';
4
+ import { useAssets } from './assets-store.js';
5
+ import { SYSTEM_ASSET_FIELDS } from './asset-metadata.js';
6
+ import { normalizeOkResult, useRailViewSource } from '../rail-view-source.js';
7
7
  /** The assets rail is not localized in v1 (EC-301: asset metadata is non-localized). */
8
8
  const ASSETS_LOCALE = 'en';
9
9
  const ASSETS_LOCALE_CONFIG = {
@@ -3,14 +3,14 @@ import { useRef, useState } from 'react';
3
3
  import { LayoutGrid, List, RotateCcw, Upload, X } from 'lucide-react';
4
4
  import { Badge, Button, Panel, PanelBody, PanelHeader, Row, SegmentedControl, Stack, Text, } from '@elytracms/studio/ui';
5
5
  import { translate } from '@elytracms/studio/lib/i18n';
6
- import { useAssets } from './assets-store';
7
- import { useAssetsCollectionViewSource } from './assets-collection-view-source';
8
- import { CollectionViewProvider } from '../collection-view/collection-view-context';
9
- import { RailStoreStatus } from '../rail-store-status';
10
- import { LoadedCollectionTable } from '../cms/collection-table';
11
- import { AssetGridView } from './asset-grid';
12
- import { AssetDetail } from './asset-detail';
13
- import { readAssetsViewMode, storeAssetsViewMode, } from './assets-view-preference';
6
+ import { useAssets } from './assets-store.js';
7
+ import { useAssetsCollectionViewSource } from './assets-collection-view-source.js';
8
+ import { CollectionViewProvider } from '../collection-view/collection-view-context.js';
9
+ import { RailStoreStatus } from '../rail-store-status.js';
10
+ import { LoadedCollectionTable } from '../cms/collection-table.js';
11
+ import { AssetGridView } from './asset-grid.js';
12
+ import { AssetDetail } from './asset-detail.js';
13
+ import { readAssetsViewMode, storeAssetsViewMode, } from './assets-view-preference.js';
14
14
  /**
15
15
  * The studio Assets module (EC-319). The reuse proof (alongside users/redirects)
16
16
  * that the portable list surface serves a rail with an entirely different