@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.
- package/convex/auth.ts +135 -62
- package/convex/invites.ts +77 -0
- package/dist/components/account/account-settings.js +4 -4
- package/dist/components/account/credential-panel.js +1 -1
- package/dist/components/account/self-account-view-source.js +3 -3
- package/dist/components/ai/ai-chat-panel-lazy.js +1 -1
- package/dist/components/ai/ai-chat-panel.js +4 -4
- package/dist/components/ai/ai-cms-panel.js +1 -1
- package/dist/components/ai/proposal-card.js +4 -4
- package/dist/components/ai/translate-document-action.js +1 -1
- package/dist/components/ai/translate-document-dialog.d.ts +1 -1
- package/dist/components/app-views/default-modules.d.ts +3 -3
- package/dist/components/app-views/default-modules.js +20 -20
- package/dist/components/app-views/sidebar.d.ts +1 -1
- package/dist/components/app-views/sidebar.js +2 -2
- package/dist/components/app-views/studio-config.d.ts +1 -1
- package/dist/components/assets/asset-detail.js +9 -9
- package/dist/components/assets/asset-grid.js +7 -7
- package/dist/components/assets/asset-thumb.d.ts +1 -1
- package/dist/components/assets/asset-thumb.js +2 -2
- package/dist/components/assets/assets-collection-view-source.js +3 -3
- package/dist/components/assets/assets-module.js +8 -8
- package/dist/components/assets/assets-store.d.ts +1 -1
- package/dist/components/assets/assets-store.js +1 -1
- package/dist/components/builder/blocks-model.js +1 -1
- package/dist/components/builder/blocks-panel.d.ts +1 -1
- package/dist/components/builder/blocks-panel.js +6 -6
- package/dist/components/builder/canvas.js +11 -11
- package/dist/components/builder/clipboard-actions.js +2 -2
- package/dist/components/builder/composition-field-editor-lazy.d.ts +1 -1
- package/dist/components/builder/composition-field-editor-lazy.js +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +1 -1
- package/dist/components/builder/composition-field-editor.js +9 -9
- package/dist/components/builder/insert-context.d.ts +2 -2
- package/dist/components/builder/insert-context.js +2 -2
- package/dist/components/builder/insert-menu.d.ts +2 -2
- package/dist/components/builder/insert-menu.js +1 -1
- package/dist/components/builder/layout-inspector.js +1 -1
- package/dist/components/builder/page-tree.js +5 -5
- package/dist/components/builder/property-inspector.js +3 -3
- package/dist/components/builder/reuse-context.d.ts +1 -1
- package/dist/components/builder/reuse-context.js +3 -3
- package/dist/components/builder/spacing-handles.js +2 -2
- package/dist/components/cms/asset-picker.js +4 -4
- package/dist/components/cms/bulk-actions.js +1 -1
- package/dist/components/cms/bulk-bar.d.ts +1 -1
- package/dist/components/cms/bulk-bar.js +3 -3
- package/dist/components/cms/cell-edit.js +1 -1
- package/dist/components/cms/cell.js +2 -2
- package/dist/components/cms/cms-index.js +1 -1
- package/dist/components/cms/cms-store.d.ts +2 -2
- package/dist/components/cms/cms-store.js +6 -6
- package/dist/components/cms/collection-table.js +17 -17
- package/dist/components/cms/detail-editor.js +31 -31
- package/dist/components/cms/document-diff-view.js +4 -4
- package/dist/components/cms/editable-cell.d.ts +1 -1
- package/dist/components/cms/editable-cell.js +6 -6
- package/dist/components/cms/field-control.js +15 -15
- package/dist/components/cms/field-renderer.d.ts +2 -2
- package/dist/components/cms/filter-builder.js +3 -3
- package/dist/components/cms/import-export.js +1 -1
- package/dist/components/cms/issues-pane.js +1 -1
- package/dist/components/cms/prop-fields.js +3 -3
- package/dist/components/cms/reference-confirm.js +1 -1
- package/dist/components/cms/relation-picker.js +1 -1
- package/dist/components/cms/rich-text/block-commands.d.ts +1 -1
- package/dist/components/cms/rich-text/component-embed.js +2 -2
- package/dist/components/cms/rich-text/editor.js +8 -8
- package/dist/components/cms/rich-text/embed-inspector.d.ts +1 -1
- package/dist/components/cms/rich-text/embed-inspector.js +2 -2
- package/dist/components/cms/rich-text/embed-picker.js +1 -1
- package/dist/components/cms/rich-text/embed-props.js +1 -1
- package/dist/components/cms/rich-text/extensions.d.ts +1 -1
- package/dist/components/cms/rich-text/extensions.js +4 -4
- package/dist/components/cms/rich-text/index.d.ts +1 -1
- package/dist/components/cms/rich-text/index.js +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +1 -1
- package/dist/components/cms/rich-text/lazy.d.ts +1 -1
- package/dist/components/cms/rich-text/lazy.js +1 -1
- package/dist/components/cms/rich-text/slash-command.js +1 -1
- package/dist/components/cms/rich-text/toolbar.d.ts +1 -1
- package/dist/components/cms/rich-text/toolbar.js +4 -4
- package/dist/components/cms/use-cell-edit.d.ts +1 -1
- package/dist/components/cms/use-cell-edit.js +2 -2
- package/dist/components/cms/use-compare-selection.d.ts +1 -1
- package/dist/components/cms/use-discard-controller.d.ts +1 -1
- package/dist/components/cms/use-discard-controller.js +2 -2
- package/dist/components/cms/use-document-versions.d.ts +1 -1
- package/dist/components/cms/use-document-versions.js +1 -1
- package/dist/components/cms/use-embed-inspector-host.d.ts +1 -1
- package/dist/components/cms/use-published-perspective.d.ts +2 -2
- package/dist/components/cms/use-published-perspective.js +2 -2
- package/dist/components/cms/use-restore-version.js +1 -1
- package/dist/components/cms/version-compare-lazy.js +1 -1
- package/dist/components/cms/version-compare.js +6 -6
- package/dist/components/cms/versions-model.d.ts +1 -1
- package/dist/components/cms/versions-pane.js +5 -5
- package/dist/components/cms/view-bar.js +2 -2
- package/dist/components/collab/collab-context.js +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +2 -2
- package/dist/components/components/component-preview.js +3 -3
- package/dist/components/components/components-index.js +2 -2
- package/dist/components/components/registry-browser.js +2 -2
- package/dist/components/components/transfer-dialog.js +5 -5
- package/dist/components/components/transfer.js +4 -4
- package/dist/components/current-user-menu.js +2 -2
- package/dist/components/delivery-health-banner.js +2 -2
- package/dist/components/operation-error.js +1 -1
- package/dist/components/project-list.js +5 -5
- package/dist/components/project-session.js +1 -1
- package/dist/components/project-switcher.js +3 -3
- package/dist/components/rail-store-status.js +1 -1
- package/dist/components/redirects/redirects-collection-view-source.js +3 -3
- package/dist/components/redirects/redirects-module.js +6 -6
- package/dist/components/section-nav.js +3 -3
- package/dist/components/section-surface.js +1 -1
- package/dist/components/studio-project-screen.js +7 -7
- package/dist/components/studio-shell.js +9 -9
- package/dist/components/use-hydration-phase.js +1 -1
- package/dist/components/users/users-collection-view-source.js +4 -4
- package/dist/components/users/users-module.js +9 -9
- package/dist/components/workspace-projects.js +1 -1
- package/dist/customization.d.ts +9 -9
- package/dist/customization.js +5 -5
- package/dist/lib/ai/anthropic-provider.d.ts +1 -1
- package/dist/lib/ai/apply-content.d.ts +1 -1
- package/dist/lib/ai/apply-content.js +3 -3
- package/dist/lib/ai/chat-state.d.ts +2 -2
- package/dist/lib/ai/config.d.ts +1 -1
- package/dist/lib/ai/create-flow.d.ts +2 -2
- package/dist/lib/ai/create-flow.js +3 -3
- package/dist/lib/ai/fake-provider.d.ts +1 -1
- package/dist/lib/ai/index.d.ts +15 -15
- package/dist/lib/ai/index.js +15 -15
- package/dist/lib/ai/knowledge.d.ts +1 -1
- package/dist/lib/ai/provenance.d.ts +1 -1
- package/dist/lib/ai/route-provider.d.ts +1 -1
- package/dist/lib/ai/route.d.ts +1 -1
- package/dist/lib/ai/shape.d.ts +1 -1
- package/dist/lib/ai/translate-document.d.ts +1 -1
- package/dist/lib/ai/translate-document.js +3 -3
- package/dist/lib/ai-panel.js +1 -1
- package/dist/lib/assets/index.d.ts +4 -4
- package/dist/lib/assets/index.js +4 -4
- package/dist/lib/assets/upload.d.ts +2 -2
- package/dist/lib/assets/upload.js +2 -2
- package/dist/lib/auth/convex-auth-adapter.d.ts +53 -2
- package/dist/lib/auth/convex-auth-adapter.js +149 -68
- package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
- package/dist/lib/auth/dev-auto-login-adapter.d.ts +1 -1
- package/dist/lib/auth/index.d.ts +9 -9
- package/dist/lib/auth/index.js +10 -10
- package/dist/lib/auth/local-auth-adapter.d.ts +1 -1
- package/dist/lib/auth/route-guard.d.ts +1 -1
- package/dist/lib/auth-errors.d.ts +1 -1
- package/dist/lib/clipboard/index.d.ts +3 -3
- package/dist/lib/clipboard/index.js +3 -3
- package/dist/lib/cms/published-documents.d.ts +1 -1
- package/dist/lib/cms-views/index.d.ts +2 -2
- package/dist/lib/cms-views/index.js +2 -2
- package/dist/lib/collection-view/collection-view-source.d.ts +4 -4
- package/dist/lib/data/collection-content.js +1 -1
- package/dist/lib/data/data-store.js +6 -6
- package/dist/lib/data/index.d.ts +4 -4
- package/dist/lib/data/index.js +4 -4
- package/dist/lib/data/sample-capture.js +4 -4
- package/dist/lib/data/sources-store.d.ts +1 -1
- package/dist/lib/diff-view-preference.js +1 -1
- package/dist/lib/dnd/gap-drag.d.ts +1 -1
- package/dist/lib/i18n/i18n.d.ts +1 -1
- package/dist/lib/i18n/i18n.js +1 -1
- package/dist/lib/i18n/index.d.ts +4 -4
- package/dist/lib/i18n/index.js +3 -3
- package/dist/lib/i18n/messages.en.d.ts +1 -1
- package/dist/lib/i18n/messages.en.js +1 -1
- package/dist/lib/locale/binding-fallback.d.ts +1 -1
- package/dist/lib/locale/binding-fallback.js +2 -2
- package/dist/lib/locale/index.d.ts +6 -6
- package/dist/lib/locale/index.js +6 -6
- package/dist/lib/locale/locale-switcher.js +3 -3
- package/dist/lib/locale/use-studio-locale.js +1 -1
- package/dist/lib/operations/asset-repository.d.ts +1 -1
- package/dist/lib/operations/cms-persistence.d.ts +1 -1
- package/dist/lib/operations/cms-persistence.js +1 -1
- package/dist/lib/operations/index.d.ts +6 -6
- package/dist/lib/operations/index.js +6 -6
- package/dist/lib/operations/session-hydration.d.ts +1 -1
- package/dist/lib/operations/session-hydration.js +1 -1
- package/dist/lib/operations/studio-operations.d.ts +1 -1
- package/dist/lib/operations/studio-operations.js +3 -3
- package/dist/lib/persistence/convex-adapter.js +2 -2
- package/dist/lib/persistence/convex-blob-storage.d.ts +1 -1
- package/dist/lib/persistence/convex-blob-storage.js +1 -1
- package/dist/lib/persistence/deployment-readiness.d.ts +2 -2
- package/dist/lib/persistence/index.d.ts +6 -6
- package/dist/lib/persistence/index.js +7 -7
- package/dist/lib/persona/labels.js +1 -1
- package/dist/lib/query/index.d.ts +2 -2
- package/dist/lib/query/index.js +1 -1
- package/dist/lib/rail.js +1 -1
- package/dist/lib/references/impact.d.ts +1 -1
- package/dist/lib/references/impact.js +1 -1
- package/dist/lib/references/index.d.ts +3 -3
- package/dist/lib/references/index.js +3 -3
- package/dist/lib/references/references-store.d.ts +2 -2
- package/dist/lib/references/references-store.js +3 -3
- package/dist/lib/registry/index.js +2 -2
- package/dist/lib/theme.js +1 -1
- package/dist/lib/webhooks/config.d.ts +1 -1
- package/dist/lib/webhooks/config.js +2 -2
- package/dist/lib/webhooks/dispatcher.js +2 -2
- package/dist/lib/webhooks/index.d.ts +6 -6
- package/dist/lib/webhooks/index.js +4 -4
- package/dist/lib/workspace/index.d.ts +7 -7
- package/dist/lib/workspace/index.js +3 -3
- package/dist/lib/workspace/switcher-source.d.ts +1 -1
- package/dist/router/index.d.ts +1 -1
- package/dist/router/index.js +1 -1
- package/dist/ui/avatar.js +1 -1
- package/dist/ui/badge.js +1 -1
- package/dist/ui/button.js +1 -1
- package/dist/ui/card.js +1 -1
- package/dist/ui/collapsible-section.js +2 -2
- package/dist/ui/dialog.js +1 -1
- package/dist/ui/dropdown-menu.js +1 -1
- package/dist/ui/field.js +1 -1
- package/dist/ui/index.d.ts +19 -19
- package/dist/ui/index.js +19 -19
- package/dist/ui/input.js +1 -1
- package/dist/ui/inspector.js +1 -1
- package/dist/ui/layout.js +1 -1
- package/dist/ui/panel.js +2 -2
- package/dist/ui/popover.js +1 -1
- package/dist/ui/row-item.js +2 -2
- package/dist/ui/segmented-control.js +1 -1
- package/dist/ui/separator.js +1 -1
- package/dist/ui/toolbar.js +2 -2
- package/dist/ui/typography.js +1 -1
- package/package.json +3 -3
- package/dist/components/assets/asset-library.d.ts +0 -14
- package/dist/components/assets/asset-library.js +0 -202
- package/dist/components/assets/asset-library.js.map +0 -1
- package/dist/components/backend-gate.d.ts +0 -19
- package/dist/components/backend-gate.js +0 -64
- package/dist/components/backend-gate.js.map +0 -1
- package/dist/components/builder/composition-scope.d.ts +0 -16
- package/dist/components/builder/composition-scope.js +0 -7
- package/dist/components/builder/composition-scope.js.map +0 -1
- package/dist/components/cms/alias-selector.d.ts +0 -20
- package/dist/components/cms/alias-selector.js +0 -34
- package/dist/components/cms/alias-selector.js.map +0 -1
- package/dist/components/cms/collections-sidebar.d.ts +0 -18
- package/dist/components/cms/collections-sidebar.js +0 -36
- package/dist/components/cms/collections-sidebar.js.map +0 -1
- package/dist/components/cms/new-page-dialog.d.ts +0 -22
- package/dist/components/cms/new-page-dialog.js +0 -42
- package/dist/components/cms/new-page-dialog.js.map +0 -1
- package/dist/components/cms/rich-text/embed-control.d.ts +0 -17
- package/dist/components/cms/rich-text/embed-control.js +0 -62
- package/dist/components/cms/rich-text/embed-control.js.map +0 -1
- package/dist/components/cms/rich-text/image-control.d.ts +0 -16
- package/dist/components/cms/rich-text/image-control.js +0 -36
- package/dist/components/cms/rich-text/image-control.js.map +0 -1
- package/dist/components/components/fixture-runner.d.ts +0 -10
- package/dist/components/components/fixture-runner.js +0 -27
- package/dist/components/components/fixture-runner.js.map +0 -1
- package/dist/components/references/builder-deep-link.d.ts +0 -12
- package/dist/components/references/builder-deep-link.js +0 -53
- package/dist/components/references/builder-deep-link.js.map +0 -1
- package/dist/components/routes/redirect-chain.d.ts +0 -9
- package/dist/components/routes/redirect-chain.js +0 -16
- package/dist/components/routes/redirect-chain.js.map +0 -1
- package/dist/components/routes/redirect-manager.d.ts +0 -13
- package/dist/components/routes/redirect-manager.js +0 -24
- package/dist/components/routes/redirect-manager.js.map +0 -1
- package/dist/components/routes/route-list.d.ts +0 -35
- package/dist/components/routes/route-list.js +0 -69
- package/dist/components/routes/route-list.js.map +0 -1
- package/dist/components/routes/routes-shell.d.ts +0 -14
- package/dist/components/routes/routes-shell.js +0 -68
- package/dist/components/routes/routes-shell.js.map +0 -1
- package/dist/components/sections.d.ts +0 -37
- package/dist/components/sections.js +0 -62
- package/dist/components/sections.js.map +0 -1
- package/dist/components/users/user-search.d.ts +0 -8
- package/dist/components/users/user-search.js +0 -13
- package/dist/components/users/user-search.js.map +0 -1
- package/dist/components/workspace-gate.d.ts +0 -12
- package/dist/components/workspace-gate.js +0 -28
- package/dist/components/workspace-gate.js.map +0 -1
- package/dist/lib/cms-views/views-store.d.ts +0 -70
- package/dist/lib/cms-views/views-store.js +0 -129
- package/dist/lib/cms-views/views-store.js.map +0 -1
- package/dist/lib/operations/graph-commands.d.ts +0 -136
- package/dist/lib/operations/graph-commands.js +0 -85
- package/dist/lib/operations/graph-commands.js.map +0 -1
- package/dist/lib/operations/graph-persistence.d.ts +0 -27
- package/dist/lib/operations/graph-persistence.js +0 -35
- package/dist/lib/operations/graph-persistence.js.map +0 -1
- package/dist/lib/routing/index.d.ts +0 -10
- package/dist/lib/routing/index.js +0 -11
- package/dist/lib/routing/index.js.map +0 -1
- package/dist/lib/routing/routing-analysis.d.ts +0 -26
- package/dist/lib/routing/routing-analysis.js +0 -37
- package/dist/lib/routing/routing-analysis.js.map +0 -1
- package/dist/lib/routing/routing-store.d.ts +0 -50
- package/dist/lib/routing/routing-store.js +0 -72
- package/dist/lib/routing/routing-store.js.map +0 -1
- package/dist/lib/working-graph.d.ts +0 -82
- package/dist/lib/working-graph.js +0 -113
- 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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|