@elytracms/studio 0.0.62 → 0.0.64
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/dist/components/account/credential-panel.js +6 -1
- package/dist/components/account/credential-panel.js.map +1 -1
- package/dist/components/auth-context.d.ts +14 -0
- package/dist/components/auth-context.js +32 -2
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +7 -0
- package/dist/components/builder/blocks-model.js +5 -0
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.js +2 -2
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.d.ts +54 -0
- package/dist/components/builder/canvas-chrome-css.js +164 -0
- package/dist/components/builder/canvas-chrome-css.js.map +1 -0
- package/dist/components/builder/canvas-chrome.d.ts +122 -18
- package/dist/components/builder/canvas-chrome.js +148 -82
- package/dist/components/builder/canvas-chrome.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +13 -0
- package/dist/components/builder/canvas-render-context.js +7 -5
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas-stylesheet.d.ts +1 -1
- package/dist/components/builder/canvas-stylesheet.js +16 -9
- package/dist/components/builder/canvas-stylesheet.js.map +1 -1
- package/dist/components/builder/canvas-theme-notice.d.ts +6 -0
- package/dist/components/builder/canvas-theme-notice.js +46 -0
- package/dist/components/builder/canvas-theme-notice.js.map +1 -0
- package/dist/components/builder/canvas.js +17 -10
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/fixture-thumbnail.d.ts +15 -0
- package/dist/components/builder/fixture-thumbnail.js +14 -0
- package/dist/components/builder/fixture-thumbnail.js.map +1 -1
- package/dist/components/builder/rich-text-toolbar.d.ts +23 -0
- package/dist/components/builder/rich-text-toolbar.js +24 -17
- package/dist/components/builder/rich-text-toolbar.js.map +1 -1
- package/dist/components/builder/spacing-handles.d.ts +31 -0
- package/dist/components/builder/spacing-handles.js +49 -37
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/cms-index.js +6 -3
- package/dist/components/cms/cms-index.js.map +1 -1
- package/dist/components/components/component-preview.d.ts +2 -1
- package/dist/components/components/component-preview.js +24 -25
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/deployment-setup-gate.js +40 -8
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/operation-error.d.ts +7 -5
- package/dist/components/operation-error.js +19 -36
- package/dist/components/operation-error.js.map +1 -1
- package/dist/components/project-list.js +17 -1
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-switcher.d.ts +10 -0
- package/dist/components/project-switcher.js +13 -3
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/session-expired-overlay.d.ts +27 -0
- package/dist/components/session-expired-overlay.js +51 -0
- package/dist/components/session-expired-overlay.js.map +1 -0
- package/dist/components/sign-in-view.d.ts +13 -1
- package/dist/components/sign-in-view.js +27 -13
- package/dist/components/sign-in-view.js.map +1 -1
- package/dist/components/studio-project-screen.js +2 -2
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +40 -3
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/customization.d.ts +4 -1
- package/dist/customization.js +3 -0
- package/dist/customization.js.map +1 -1
- package/dist/lib/auth-errors.d.ts +58 -28
- package/dist/lib/auth-errors.js +93 -15
- package/dist/lib/auth-errors.js.map +1 -1
- package/dist/lib/i18n/index.d.ts +1 -0
- package/dist/lib/i18n/index.js.map +1 -1
- package/dist/lib/i18n/messages.de.d.ts +24 -0
- package/dist/lib/i18n/messages.de.js +1023 -0
- package/dist/lib/i18n/messages.de.js.map +1 -0
- package/dist/lib/i18n/messages.en.d.ts +884 -0
- package/dist/lib/i18n/messages.en.js +76 -3
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +7 -0
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/backend-mode.d.ts +7 -0
- package/dist/lib/persistence/backend-mode.js +9 -0
- package/dist/lib/persistence/backend-mode.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.d.ts +8 -1
- package/dist/lib/persistence/deployment-readiness.js +16 -1
- package/dist/lib/persistence/deployment-readiness.js.map +1 -1
- package/dist/lib/persistence/index.d.ts +1 -1
- package/dist/lib/persistence/index.js +1 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/session-expiry.d.ts +67 -0
- package/dist/lib/session-expiry.js +72 -0
- package/dist/lib/session-expiry.js.map +1 -0
- package/dist/lib/tab-title.d.ts +53 -0
- package/dist/lib/tab-title.js +77 -0
- package/dist/lib/tab-title.js.map +1 -0
- package/dist/lib/workspace/index.d.ts +1 -1
- package/dist/lib/workspace/index.js +1 -1
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.d.ts +27 -0
- package/dist/lib/workspace/switcher-source.js +30 -0
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/dist/router/index.d.ts +6 -0
- package/dist/router/index.js +6 -0
- package/dist/router/index.js.map +1 -1
- package/dist/router/launcher-visit.d.ts +13 -0
- package/dist/router/launcher-visit.js +34 -0
- package/dist/router/launcher-visit.js.map +1 -0
- package/dist/router/root.js +12 -2
- package/dist/router/root.js.map +1 -1
- package/dist/ui/dialog.d.ts +15 -0
- package/dist/ui/dialog.js +17 -0
- package/dist/ui/dialog.js.map +1 -1
- package/package.json +6 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { activeDeploymentKey, selectPersistenceAdapter } from '../persistence/index.js';
|
|
2
|
+
import { attachSessionExpiryReporting } from '../session-expiry.js';
|
|
2
3
|
import { attachConfiguredRevalidationWebhooks } from '../webhooks/index.js';
|
|
3
4
|
import { attachReferenceIndex, createChangesets, createOperations, createRoleAuthorization, } from './operations-package.js';
|
|
4
5
|
/**
|
|
@@ -30,6 +31,12 @@ export function createStudioOperations(adapter, options = {}) {
|
|
|
30
31
|
// setState, graph publish/unpublish) fires a signed revalidation webhook.
|
|
31
32
|
// No env (dev/tests) → no-op.
|
|
32
33
|
attachConfiguredRevalidationWebhooks(operations.events);
|
|
34
|
+
// EC-403: session-expiry detection rides the SAME lifecycle bus — every failed
|
|
35
|
+
// command/query/changeset operation is checked against the session-expired
|
|
36
|
+
// predicate here, so expiry raises the ONE app-level blocking re-auth state no
|
|
37
|
+
// matter which surface issued the operation (per-surface banners can't cover
|
|
38
|
+
// surfaces that render no banner).
|
|
39
|
+
attachSessionExpiryReporting(operations.events);
|
|
33
40
|
return { operations, changesets, adapter, references };
|
|
34
41
|
}
|
|
35
42
|
// One adapter + one operations bundle per backend (EC-179): keyed by the active
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"studio-operations.js","sourceRoot":"","sources":["../../../src/lib/operations/studio-operations.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport { activeDeploymentKey, selectPersistenceAdapter } from '../persistence'\nimport { attachConfiguredRevalidationWebhooks } from '../webhooks'\nimport {\n attachReferenceIndex,\n createChangesets,\n createOperations,\n createRoleAuthorization,\n} from './operations-package'\nimport type {\n ChangesetOperations,\n OperationsClient,\n Principal,\n ReferenceIndexAttachment,\n} from './operations-package'\n\n/**\n * The studio's single operations bootstrap (EC-141, vision AD-7).\n *\n * One `OperationsClient` + one `ChangesetOperations` instance, built over the\n * selected persistence adapter, shared by every store and surface in the app.\n * The UI never talks to a repository for writes — every mutation is a typed\n * operation command (validate → authorize → mutate → event), so the studio\n * dogfoods the same seam plugins, AI changesets, and the CLI use.\n *\n * The EC-155 reference index is attached to the shared event bus here, so any\n * document/graph/publishing command issued anywhere in the studio keeps the\n * index maintained.\n */\nexport interface StudioOperations {\n readonly operations: OperationsClient\n readonly changesets: ChangesetOperations\n /**\n * The raw adapter, exposed for *transport and read paths only*: blob byte\n * upload/download (`adapter.blobs`) and existing read stores. Studio code\n * must not call repository write methods on it — writes go through\n * `operations`.\n */\n readonly adapter: PersistenceAdapter\n /** EC-155 maintained-on-write index attachment (flushable in tests). */\n readonly references: ReferenceIndexAttachment\n}\n\nexport interface CreateStudioOperationsOptions {\n /**\n * The signed-in principal (EC-150). When present, the EC-150 role hook\n * (`createRoleAuthorization` over `adapter.members`) becomes the\n * authorization chokepoint for every command and query — viewers get\n * structured denials on mutations, editors on administration. Without an\n * actor the client runs pass-through (seeds, fixtures, tests that exercise\n * domain logic rather than permissions); the route guard keeps\n * unauthenticated users away from every operation surface.\n */\n actor?: Principal\n}\n\n/**\n * A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):\n * nothing is subscribed, so `detach` and `flush` are trivially satisfied (the index\n * is kept current in the adapter's write path, not by a queued subscriber).\n */\nfunction noopReferenceIndexAttachment(): ReferenceIndexAttachment {\n return { detach: () => {}, flush: async () => {} }\n}\n\n/** Build a studio operations bundle over an adapter (exported for tests). */\nexport function createStudioOperations(\n adapter: PersistenceAdapter,\n options: CreateStudioOperationsOptions = {},\n): StudioOperations {\n const authorized = options.actor\n ? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }\n : {}\n const operations = createOperations({ adapter, ...authorized })\n // EC-277: when the adapter maintains the index in its own write path (AD-6 \"at the\n // source\" — the Convex provider does this server-side, so a raw `convex run` is\n // caught too), the client-side subscriber would only double the work. Attach it\n // ONLY for providers that don't self-maintain. Where it does run, extraction is a\n // schema-free, manifest-free walk over self-describing content, so it needs no\n // registry (compositions self-typed their relation/asset props on write).\n const references = adapter.maintainsReferenceIndex\n ? noopReferenceIndexAttachment()\n : attachReferenceIndex(operations.events, adapter)\n const changesets = createChangesets({ adapter, events: operations.events, ...authorized })\n // EC-146 instant publish: when a host revalidate endpoint is configured via\n // env, every successful publish-shaped command (document upsert/delete/\n // setState, graph publish/unpublish) fires a signed revalidation webhook.\n // No env (dev/tests) → no-op.\n attachConfiguredRevalidationWebhooks(operations.events)\n return { operations, changesets, adapter, references }\n}\n\n// One adapter + one operations bundle per backend (EC-179): keyed by the active\n// deployment so switching to a project on another Convex deployment selects (or\n// lazily builds) that backend's client, rather than reusing the first one.\nconst adaptersByKey = new Map<string, Promise<PersistenceAdapter>>()\nconst instancesByKey = new Map<string, Promise<StudioOperations>>()\nlet currentActor: Principal | null = null\n\n/**\n * The active backend key, tolerant of an unconfigured env: the real app never\n * reaches `getStudioOperations` while misconfigured (the `DeploymentSetupGate` renders a\n * boot error first), but tests inject a bundle directly via the seam below — so\n * a missing backend resolves to one stable key rather than throwing.\n */\nfunction deploymentKey(): string {\n try {\n return activeDeploymentKey()\n } catch {\n return '__unconfigured__'\n }\n}\n\nfunction getAdapter(key: string): Promise<PersistenceAdapter> {\n let adapter = adaptersByKey.get(key)\n if (!adapter) {\n adapter = selectPersistenceAdapter()\n adaptersByKey.set(key, adapter)\n }\n return adapter\n}\n\n/**\n * The shared studio operations client for the active backend (lazily\n * bootstrapped, cached per deployment). In-memory mode this owns the one seeded\n * adapter the whole session mutates; against Convex it owns one client per\n * deployment — the active project (EC-179) selects which.\n */\nexport function getStudioOperations(): Promise<StudioOperations> {\n const key = deploymentKey()\n let instance = instancesByKey.get(key)\n if (!instance) {\n const actor = currentActor\n instance = getAdapter(key).then((adapter) =>\n createStudioOperations(adapter, actor ? { actor } : {}),\n )\n instancesByKey.set(key, instance)\n }\n return instance\n}\n\n/**\n * Bind (or clear) the session principal (EC-150). Called by the auth context\n * on sign-in/sign-out. Rebuilds the operations clients over the *same* adapters\n * — the persisted state (and, in-memory, the whole session's edits) survives;\n * only the authorization identity changes. All cached backends are rebuilt so a\n * later switch picks up the new actor too.\n */\nexport function setStudioSession(actor: Principal | null): void {\n if (currentActor?.id === actor?.id) return // same user (or both null) — keep the clients\n currentActor = actor\n instancesByKey.clear()\n}\n\n/** Test seam: replace (or clear) the singleton for the active backend. */\nexport function setStudioOperationsForTests(next: StudioOperations | null): void {\n if (next) {\n instancesByKey.set(deploymentKey(), Promise.resolve(next))\n return\n }\n instancesByKey.clear()\n adaptersByKey.clear()\n currentActor = null\n}\n"]}
|
|
1
|
+
{"version":3,"file":"studio-operations.js","sourceRoot":"","sources":["../../../src/lib/operations/studio-operations.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport { activeDeploymentKey, selectPersistenceAdapter } from '../persistence'\nimport { attachSessionExpiryReporting } from '../session-expiry'\nimport { attachConfiguredRevalidationWebhooks } from '../webhooks'\nimport {\n attachReferenceIndex,\n createChangesets,\n createOperations,\n createRoleAuthorization,\n} from './operations-package'\nimport type {\n ChangesetOperations,\n OperationsClient,\n Principal,\n ReferenceIndexAttachment,\n} from './operations-package'\n\n/**\n * The studio's single operations bootstrap (EC-141, vision AD-7).\n *\n * One `OperationsClient` + one `ChangesetOperations` instance, built over the\n * selected persistence adapter, shared by every store and surface in the app.\n * The UI never talks to a repository for writes — every mutation is a typed\n * operation command (validate → authorize → mutate → event), so the studio\n * dogfoods the same seam plugins, AI changesets, and the CLI use.\n *\n * The EC-155 reference index is attached to the shared event bus here, so any\n * document/graph/publishing command issued anywhere in the studio keeps the\n * index maintained.\n */\nexport interface StudioOperations {\n readonly operations: OperationsClient\n readonly changesets: ChangesetOperations\n /**\n * The raw adapter, exposed for *transport and read paths only*: blob byte\n * upload/download (`adapter.blobs`) and existing read stores. Studio code\n * must not call repository write methods on it — writes go through\n * `operations`.\n */\n readonly adapter: PersistenceAdapter\n /** EC-155 maintained-on-write index attachment (flushable in tests). */\n readonly references: ReferenceIndexAttachment\n}\n\nexport interface CreateStudioOperationsOptions {\n /**\n * The signed-in principal (EC-150). When present, the EC-150 role hook\n * (`createRoleAuthorization` over `adapter.members`) becomes the\n * authorization chokepoint for every command and query — viewers get\n * structured denials on mutations, editors on administration. Without an\n * actor the client runs pass-through (seeds, fixtures, tests that exercise\n * domain logic rather than permissions); the route guard keeps\n * unauthenticated users away from every operation surface.\n */\n actor?: Principal\n}\n\n/**\n * A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):\n * nothing is subscribed, so `detach` and `flush` are trivially satisfied (the index\n * is kept current in the adapter's write path, not by a queued subscriber).\n */\nfunction noopReferenceIndexAttachment(): ReferenceIndexAttachment {\n return { detach: () => {}, flush: async () => {} }\n}\n\n/** Build a studio operations bundle over an adapter (exported for tests). */\nexport function createStudioOperations(\n adapter: PersistenceAdapter,\n options: CreateStudioOperationsOptions = {},\n): StudioOperations {\n const authorized = options.actor\n ? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }\n : {}\n const operations = createOperations({ adapter, ...authorized })\n // EC-277: when the adapter maintains the index in its own write path (AD-6 \"at the\n // source\" — the Convex provider does this server-side, so a raw `convex run` is\n // caught too), the client-side subscriber would only double the work. Attach it\n // ONLY for providers that don't self-maintain. Where it does run, extraction is a\n // schema-free, manifest-free walk over self-describing content, so it needs no\n // registry (compositions self-typed their relation/asset props on write).\n const references = adapter.maintainsReferenceIndex\n ? noopReferenceIndexAttachment()\n : attachReferenceIndex(operations.events, adapter)\n const changesets = createChangesets({ adapter, events: operations.events, ...authorized })\n // EC-146 instant publish: when a host revalidate endpoint is configured via\n // env, every successful publish-shaped command (document upsert/delete/\n // setState, graph publish/unpublish) fires a signed revalidation webhook.\n // No env (dev/tests) → no-op.\n attachConfiguredRevalidationWebhooks(operations.events)\n // EC-403: session-expiry detection rides the SAME lifecycle bus — every failed\n // command/query/changeset operation is checked against the session-expired\n // predicate here, so expiry raises the ONE app-level blocking re-auth state no\n // matter which surface issued the operation (per-surface banners can't cover\n // surfaces that render no banner).\n attachSessionExpiryReporting(operations.events)\n return { operations, changesets, adapter, references }\n}\n\n// One adapter + one operations bundle per backend (EC-179): keyed by the active\n// deployment so switching to a project on another Convex deployment selects (or\n// lazily builds) that backend's client, rather than reusing the first one.\nconst adaptersByKey = new Map<string, Promise<PersistenceAdapter>>()\nconst instancesByKey = new Map<string, Promise<StudioOperations>>()\nlet currentActor: Principal | null = null\n\n/**\n * The active backend key, tolerant of an unconfigured env: the real app never\n * reaches `getStudioOperations` while misconfigured (the `DeploymentSetupGate` renders a\n * boot error first), but tests inject a bundle directly via the seam below — so\n * a missing backend resolves to one stable key rather than throwing.\n */\nfunction deploymentKey(): string {\n try {\n return activeDeploymentKey()\n } catch {\n return '__unconfigured__'\n }\n}\n\nfunction getAdapter(key: string): Promise<PersistenceAdapter> {\n let adapter = adaptersByKey.get(key)\n if (!adapter) {\n adapter = selectPersistenceAdapter()\n adaptersByKey.set(key, adapter)\n }\n return adapter\n}\n\n/**\n * The shared studio operations client for the active backend (lazily\n * bootstrapped, cached per deployment). In-memory mode this owns the one seeded\n * adapter the whole session mutates; against Convex it owns one client per\n * deployment — the active project (EC-179) selects which.\n */\nexport function getStudioOperations(): Promise<StudioOperations> {\n const key = deploymentKey()\n let instance = instancesByKey.get(key)\n if (!instance) {\n const actor = currentActor\n instance = getAdapter(key).then((adapter) =>\n createStudioOperations(adapter, actor ? { actor } : {}),\n )\n instancesByKey.set(key, instance)\n }\n return instance\n}\n\n/**\n * Bind (or clear) the session principal (EC-150). Called by the auth context\n * on sign-in/sign-out. Rebuilds the operations clients over the *same* adapters\n * — the persisted state (and, in-memory, the whole session's edits) survives;\n * only the authorization identity changes. All cached backends are rebuilt so a\n * later switch picks up the new actor too.\n */\nexport function setStudioSession(actor: Principal | null): void {\n if (currentActor?.id === actor?.id) return // same user (or both null) — keep the clients\n currentActor = actor\n instancesByKey.clear()\n}\n\n/** Test seam: replace (or clear) the singleton for the active backend. */\nexport function setStudioOperationsForTests(next: StudioOperations | null): void {\n if (next) {\n instancesByKey.set(deploymentKey(), Promise.resolve(next))\n return\n }\n instancesByKey.clear()\n adaptersByKey.clear()\n currentActor = null\n}\n"]}
|
|
@@ -103,6 +103,13 @@ export interface DevAutoLoginCredentials {
|
|
|
103
103
|
email: string;
|
|
104
104
|
password: string;
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* The studio's house dev-build check, public: dev-only surfaces (e.g. the
|
|
108
|
+
* EC-414 canvas-theme migration notice) gate on THIS — it honors the
|
|
109
|
+
* mount-provided env's captured `DEV` flag (the EC-357 dep-optimization
|
|
110
|
+
* nuance) before falling back to the engine-visible static flag.
|
|
111
|
+
*/
|
|
112
|
+
export declare function isDevRuntime(): boolean;
|
|
106
113
|
/**
|
|
107
114
|
* Dev-only unattended auth — **not a bypass**. When `VITE_ELYTRA_DEV_LOGIN=
|
|
108
115
|
* email:password` is set in a dev build, the studio auto-signs-in with those
|
|
@@ -216,6 +216,15 @@ function isViteDevBuild() {
|
|
|
216
216
|
return false;
|
|
217
217
|
return import.meta.env?.DEV === true;
|
|
218
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* The studio's house dev-build check, public: dev-only surfaces (e.g. the
|
|
221
|
+
* EC-414 canvas-theme migration notice) gate on THIS — it honors the
|
|
222
|
+
* mount-provided env's captured `DEV` flag (the EC-357 dep-optimization
|
|
223
|
+
* nuance) before falling back to the engine-visible static flag.
|
|
224
|
+
*/
|
|
225
|
+
export function isDevRuntime() {
|
|
226
|
+
return isViteDevBuild();
|
|
227
|
+
}
|
|
219
228
|
/**
|
|
220
229
|
* Dev-only unattended auth — **not a bypass**. When `VITE_ELYTRA_DEV_LOGIN=
|
|
221
230
|
* email:password` is set in a dev build, the studio auto-signs-in with those
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend-mode.js","sourceRoot":"","sources":["../../../src/lib/persistence/backend-mode.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Studio backend mode resolution (EC-153). Convex is the **default and primary**\n * persistence backend; the deterministic in-memory adapter is demoted to an\n * explicit, labeled **playground** mode and is never an automatic fallback:\n *\n * - `VITE_CONVEX_URL` set → Convex mode (the production/dev default).\n * - `VITE_ELYTRA_MODE=playground` → playground mode (seeded in-memory adapter,\n * local + ephemeral). The flag wins over a configured URL — explicit beats\n * implicit — so a checked-out `.env.local` can be flipped without deleting it.\n * - Neither → **misconfigured**: the studio renders a deterministic boot-error\n * screen (see `DeploymentSetupGate`) telling the developer exactly what to set.\n * Likewise, a Convex backend that is unreachable at runtime is a visible\n * error state — the studio never silently swaps in in-memory storage.\n */\n\nexport const MODE_ENV_VAR = 'VITE_ELYTRA_MODE'\nexport const PLAYGROUND_MODE = 'playground'\nexport const CONVEX_URL_ENV_VAR = 'VITE_CONVEX_URL'\n\nexport type StudioBackendMode = 'convex' | 'playground'\n\nexport type StudioBackendConfig =\n | { kind: 'convex'; url: string }\n | { kind: 'playground' }\n | { kind: 'misconfigured'; reason: 'missing-convex-url' | 'unknown-mode'; detail: string }\n\n/** The studio's env surface: Vite client env first, Node/SSR env second. */\nexport type EnvRecord = Record<string, string | undefined>\n\n/**\n * The ENGINE-visible `import.meta.env`, or `null` when the module has none at all.\n * `null` is a meaningful signal (EC-357): every module Vite transforms live gets an\n * `import.meta.env`, so its complete absence in a browser means this engine module\n * was served from Vite's dep-optimizer cache — where Vite ≥8 no longer inlines\n * custom `VITE_*` values (and Vite 7 froze them at optimize time). Engine code must\n * therefore never trust this view when a mount-provided env exists — see\n * {@link provideStudioEnv}.\n */\nfunction engineVisibleEnv(): EnvRecord | null {\n if (typeof import.meta === 'undefined') return null\n return (import.meta as { env?: EnvRecord }).env ?? null\n}\n\nfunction viteEnv(): EnvRecord {\n return engineVisibleEnv() ?? {}\n}\n\nfunction nodeEnv(): EnvRecord {\n return typeof process !== 'undefined' ? (process.env ?? {}) : {}\n}\n\nfunction readEnv(env: EnvRecord, key: string): string | undefined {\n const value = env[key]\n return value && value.trim() !== '' ? value.trim() : undefined\n}\n\nlet envOverride: EnvRecord | null = null\n\n/**\n * Test seam: pin the env the selectors see. `vi.stubEnv` cannot reach the\n * `import.meta.env` snapshot Vitest hands this module, so suites that exercise\n * `selectPersistenceAdapter`/`selectAuthAdapter` against specific configurations\n * use this instead — keeping results independent of any developer `.env.local`.\n */\nexport function setStudioEnvOverrideForTests(env: EnvRecord | null): void {\n envOverride = env\n}\n\n// ---------------------------------------------------------------------------\n// Consumer-provided env (EC-357 — the Vite dep-optimizer env fix)\n// ---------------------------------------------------------------------------\n\n/** Normalized mount-provided env: the string entries + the captured `DEV` flag. */\ninterface ProvidedStudioEnv {\n record: EnvRecord\n dev: boolean | undefined\n}\n\nlet providedEnv: ProvidedStudioEnv | null = null\n\n/**\n * Install the CONSUMER-captured env as the studio's env source (EC-357). The root\n * cause this fixes: the engine used to read `import.meta.env.VITE_*` inside\n * `@elytracms` package code, which Vite dep-optimizes in a real consumer — on\n * Vite ≥8 those values are simply absent at runtime in the optimized chunk (the\n * backend URL / mode / dev-login silently vanish and every sign-in fails), and on\n * Vite 7 they froze at optimize time (stale until `node_modules/.vite` is wiped).\n *\n * The consumer's own entry module is code Vite ALWAYS transforms live, so the env\n * is captured there and handed down: `createStudio(config, studioConfig, { env:\n * import.meta.env })`. When a provided env exists it REPLACES the engine-visible\n * Vite layer entirely (never merged with it — a stale optimizer snapshot must not\n * shadow a removed key); `process.env` stays underneath as the SSR/tooling base.\n *\n * `DEV` is captured alongside (it gates dev auto-login): in a production consumer\n * build Vite statically inlines `import.meta.env` with `DEV: false`, so the\n * dev-login gate keeps failing closed exactly as before. Non-string values other\n * than `DEV` are dropped — the studio env surface is a string record.\n *\n * Passing `null`/`undefined` is a no-op (a consumer entry that predates the option\n * keeps today's engine-visible fallback behavior, plus a loud runtime warning when\n * that fallback is provably broken — see {@link maybeWarnEngineEnvUnreachable}).\n */\nexport function provideStudioEnv(env: Record<string, unknown> | null | undefined): void {\n if (env === null || env === undefined) return\n const record: EnvRecord = {}\n for (const [key, value] of Object.entries(env)) {\n if (typeof value === 'string') record[key] = value\n }\n providedEnv = {\n record,\n dev: env.DEV === true ? true : env.DEV === false ? false : undefined,\n }\n maybeWarnEngineEnvStale()\n}\n\n/** Test seam: clear the mount-provided env. Never used by app code. */\nexport function resetProvidedStudioEnvForTests(): void {\n providedEnv = null\n warnedStale = false\n warnedUnreachable = false\n}\n\n/**\n * Resolve the configured studio env. Precedence: the test override, else the\n * mount-provided env (over `process.env`), else the engine-visible Vite env\n * (over `process.env`).\n */\nexport function studioEnv(): EnvRecord {\n if (envOverride) return envOverride\n if (providedEnv) return { ...nodeEnv(), ...providedEnv.record }\n maybeWarnEngineEnvUnreachable()\n return { ...nodeEnv(), ...viteEnv() }\n}\n\n// --- Boot-time env self-check (EC-357, Oli: \"easier to catch\") ---------------\n\n/** The env keys whose loss/staleness breaks boot or sign-in. */\nexport const STUDIO_ENV_KEYS: readonly string[] = [\n 'VITE_ELYTRA_MODE',\n 'VITE_CONVEX_URL',\n 'VITE_ELYTRA_DEV_LOGIN',\n]\n\nexport type StudioEnvAnomaly =\n /** No mount-provided env AND the engine module has no `import.meta.env` at all. */\n | { kind: 'engine-env-unreachable' }\n /** The engine-visible env disagrees with the mount-provided env (stale optimizer cache). */\n | { kind: 'engine-env-stale'; keys: string[] }\n\n/**\n * Pure anomaly detection over the two env views (unit-testable core of the boot\n * warnings). `engineVisible === null` means the engine module has no\n * `import.meta.env` — expected under dep optimization WITH a provided env (silent),\n * fatal without one (the values can never arrive). A stale mismatch fires only for\n * keys the ENGINE sees with a different value than the mount provided (the Vite 7\n * freeze); keys the engine simply lacks (Vite 8) are not anomalies — the provided\n * env is authoritative and in effect.\n */\nexport function detectStudioEnvAnomaly(\n provided: EnvRecord | null,\n engineVisible: EnvRecord | null,\n inBrowser: boolean,\n): StudioEnvAnomaly | null {\n if (!inBrowser) return null\n if (!provided) return engineVisible === null ? { kind: 'engine-env-unreachable' } : null\n if (engineVisible === null) return null\n const stale = STUDIO_ENV_KEYS.filter((key) => {\n const engineValue = readEnv(engineVisible, key)\n return engineValue !== undefined && engineValue !== readEnv(provided, key)\n })\n return stale.length > 0 ? { kind: 'engine-env-stale', keys: stale } : null\n}\n\nfunction inBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined'\n}\n\nlet warnedUnreachable = false\nfunction maybeWarnEngineEnvUnreachable(): void {\n if (warnedUnreachable) return\n const anomaly = detectStudioEnvAnomaly(null, engineVisibleEnv(), inBrowser())\n if (anomaly?.kind !== 'engine-env-unreachable') return\n warnedUnreachable = true\n console.warn(\n '[elytra] The engine cannot see import.meta.env — this @elytracms module was dep-optimized by Vite, ' +\n 'so VITE_CONVEX_URL / VITE_ELYTRA_MODE / VITE_ELYTRA_DEV_LOGIN cannot reach it at runtime. ' +\n 'Fix: pass your studio entry’s env to the mount — createStudio(config, studioConfig, { env: import.meta.env }) — ' +\n \"or add optimizeDeps.exclude: ['@elytracms/studio', '@elytracms/core'] to the studio vite.config.ts.\",\n )\n}\n\nlet warnedStale = false\nfunction maybeWarnEngineEnvStale(): void {\n if (warnedStale || !providedEnv) return\n const anomaly = detectStudioEnvAnomaly(providedEnv.record, engineVisibleEnv(), inBrowser())\n if (anomaly?.kind !== 'engine-env-stale') return\n warnedStale = true\n console.warn(\n `[elytra] import.meta.env inside the engine disagrees with the studio mount's env for: ${anomaly.keys.join(', ')}. ` +\n \"The engine copy is a stale Vite dep-optimizer snapshot; the mount-provided values are in effect. \" +\n 'Delete node_modules/.vite to drop the stale copy.',\n )\n}\n\n/**\n * Pure mode decision — testable with a plain env record. The app calls it with\n * {@link studioEnv}; `CONVEX_URL` (Node-side alias) is honored for SSR/tools.\n */\nexport function resolveBackendConfig(env: EnvRecord = studioEnv()): StudioBackendConfig {\n const mode = readEnv(env, MODE_ENV_VAR)\n if (mode === PLAYGROUND_MODE) return { kind: 'playground' }\n if (mode !== undefined) {\n return {\n kind: 'misconfigured',\n reason: 'unknown-mode',\n detail: `${MODE_ENV_VAR}=\"${mode}\" is not a known mode. The only supported value is \"${PLAYGROUND_MODE}\"; unset it to run against Convex.`,\n }\n }\n const url = readEnv(env, CONVEX_URL_ENV_VAR) ?? readEnv(env, 'CONVEX_URL')\n if (url) return { kind: 'convex', url }\n return {\n kind: 'misconfigured',\n reason: 'missing-convex-url',\n // EC-357: name BOTH causes — the var may be genuinely unset, or set but never\n // reaching the client at runtime (Vite dep-optimized engine code; see\n // `provideStudioEnv`). The paired fix text (deployment-ready `FIX.backendUrl`)\n // carries the full remedy for the second cause.\n detail: `No persistence backend is configured — ${CONVEX_URL_ENV_VAR} is unset or is not reaching the client at runtime. Set it to your Convex deployment URL (see docs/backend-setup.md), or opt into the ephemeral local playground with ${MODE_ENV_VAR}=${PLAYGROUND_MODE}.`,\n }\n}\n\n/** True when the studio runs in the explicit, labeled playground mode. */\nexport function isPlaygroundMode(): boolean {\n return resolveBackendConfig().kind === 'playground'\n}\n\n// ---------------------------------------------------------------------------\n// Dev auto-login (unattended testing — NOT an auth bypass)\n// ---------------------------------------------------------------------------\n\n/** `email:password` for unattended dev sign-in (gitignored `.env.local`). */\nexport const DEV_LOGIN_ENV_VAR = 'VITE_ELYTRA_DEV_LOGIN'\n\n/** Parsed dev auto-login credentials. */\nexport interface DevAutoLoginCredentials {\n email: string\n password: string\n}\n\n/**\n * Is this a Vite *dev* build? The mount-provided env's captured `DEV` flag wins\n * (EC-357 — inside dep-optimized engine code the engine-visible flag is absent on\n * Vite ≥8 and frozen on Vite 7); the consumer's `import.meta.env.DEV` is statically\n * `true` under `vite dev` and statically `false` in any production build, so the\n * dev auto-login gate still fails closed in production. Without a provided env the\n * engine-visible static flag applies as before (where it also dead-code-eliminates\n * the feature out of a directly-bundled production build).\n */\nfunction isViteDevBuild(): boolean {\n if (providedEnv?.dev !== undefined) return providedEnv.dev\n if (typeof import.meta === 'undefined') return false\n return (import.meta as { env?: { DEV?: boolean } }).env?.DEV === true\n}\n\n/**\n * Dev-only unattended auth — **not a bypass**. When `VITE_ELYTRA_DEV_LOGIN=\n * email:password` is set in a dev build, the studio auto-signs-in with those\n * credentials on load (see {@link DevAutoLoginAdapter}) so local/automated\n * testing skips the sign-in screen while still establishing a REAL backend\n * session: Convex's server-side `requireRole` guards still apply, so the dev\n * user must exist and be a member. Returns null in production builds (gated by\n * {@link isViteDevBuild}) and when unset/malformed, so it can never weaken a\n * deployed app. `isDevBuild` is a parameter purely so the decision is unit\n * testable (`import.meta.env.DEV` is not reachable via `vi.stubEnv`).\n *\n * `password` may itself contain `:` — only the first colon separates the fields.\n */\nexport function resolveDevAutoLogin(\n env: EnvRecord = studioEnv(),\n isDevBuild: boolean = isViteDevBuild(),\n): DevAutoLoginCredentials | null {\n if (!isDevBuild) return null\n const raw = readEnv(env, DEV_LOGIN_ENV_VAR)\n if (!raw) return null\n const separator = raw.indexOf(':')\n if (separator <= 0) return null\n const email = raw.slice(0, separator).trim()\n const password = raw.slice(separator + 1)\n if (!email || !password) return null\n return { email, password }\n}\n\n/** True when dev auto-login is configured (dev build only) — for the UI marker. */\nexport function isDevAutoLoginActive(): boolean {\n return resolveDevAutoLogin() !== null\n}\n"]}
|
|
1
|
+
{"version":3,"file":"backend-mode.js","sourceRoot":"","sources":["../../../src/lib/persistence/backend-mode.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Studio backend mode resolution (EC-153). Convex is the **default and primary**\n * persistence backend; the deterministic in-memory adapter is demoted to an\n * explicit, labeled **playground** mode and is never an automatic fallback:\n *\n * - `VITE_CONVEX_URL` set → Convex mode (the production/dev default).\n * - `VITE_ELYTRA_MODE=playground` → playground mode (seeded in-memory adapter,\n * local + ephemeral). The flag wins over a configured URL — explicit beats\n * implicit — so a checked-out `.env.local` can be flipped without deleting it.\n * - Neither → **misconfigured**: the studio renders a deterministic boot-error\n * screen (see `DeploymentSetupGate`) telling the developer exactly what to set.\n * Likewise, a Convex backend that is unreachable at runtime is a visible\n * error state — the studio never silently swaps in in-memory storage.\n */\n\nexport const MODE_ENV_VAR = 'VITE_ELYTRA_MODE'\nexport const PLAYGROUND_MODE = 'playground'\nexport const CONVEX_URL_ENV_VAR = 'VITE_CONVEX_URL'\n\nexport type StudioBackendMode = 'convex' | 'playground'\n\nexport type StudioBackendConfig =\n | { kind: 'convex'; url: string }\n | { kind: 'playground' }\n | { kind: 'misconfigured'; reason: 'missing-convex-url' | 'unknown-mode'; detail: string }\n\n/** The studio's env surface: Vite client env first, Node/SSR env second. */\nexport type EnvRecord = Record<string, string | undefined>\n\n/**\n * The ENGINE-visible `import.meta.env`, or `null` when the module has none at all.\n * `null` is a meaningful signal (EC-357): every module Vite transforms live gets an\n * `import.meta.env`, so its complete absence in a browser means this engine module\n * was served from Vite's dep-optimizer cache — where Vite ≥8 no longer inlines\n * custom `VITE_*` values (and Vite 7 froze them at optimize time). Engine code must\n * therefore never trust this view when a mount-provided env exists — see\n * {@link provideStudioEnv}.\n */\nfunction engineVisibleEnv(): EnvRecord | null {\n if (typeof import.meta === 'undefined') return null\n return (import.meta as { env?: EnvRecord }).env ?? null\n}\n\nfunction viteEnv(): EnvRecord {\n return engineVisibleEnv() ?? {}\n}\n\nfunction nodeEnv(): EnvRecord {\n return typeof process !== 'undefined' ? (process.env ?? {}) : {}\n}\n\nfunction readEnv(env: EnvRecord, key: string): string | undefined {\n const value = env[key]\n return value && value.trim() !== '' ? value.trim() : undefined\n}\n\nlet envOverride: EnvRecord | null = null\n\n/**\n * Test seam: pin the env the selectors see. `vi.stubEnv` cannot reach the\n * `import.meta.env` snapshot Vitest hands this module, so suites that exercise\n * `selectPersistenceAdapter`/`selectAuthAdapter` against specific configurations\n * use this instead — keeping results independent of any developer `.env.local`.\n */\nexport function setStudioEnvOverrideForTests(env: EnvRecord | null): void {\n envOverride = env\n}\n\n// ---------------------------------------------------------------------------\n// Consumer-provided env (EC-357 — the Vite dep-optimizer env fix)\n// ---------------------------------------------------------------------------\n\n/** Normalized mount-provided env: the string entries + the captured `DEV` flag. */\ninterface ProvidedStudioEnv {\n record: EnvRecord\n dev: boolean | undefined\n}\n\nlet providedEnv: ProvidedStudioEnv | null = null\n\n/**\n * Install the CONSUMER-captured env as the studio's env source (EC-357). The root\n * cause this fixes: the engine used to read `import.meta.env.VITE_*` inside\n * `@elytracms` package code, which Vite dep-optimizes in a real consumer — on\n * Vite ≥8 those values are simply absent at runtime in the optimized chunk (the\n * backend URL / mode / dev-login silently vanish and every sign-in fails), and on\n * Vite 7 they froze at optimize time (stale until `node_modules/.vite` is wiped).\n *\n * The consumer's own entry module is code Vite ALWAYS transforms live, so the env\n * is captured there and handed down: `createStudio(config, studioConfig, { env:\n * import.meta.env })`. When a provided env exists it REPLACES the engine-visible\n * Vite layer entirely (never merged with it — a stale optimizer snapshot must not\n * shadow a removed key); `process.env` stays underneath as the SSR/tooling base.\n *\n * `DEV` is captured alongside (it gates dev auto-login): in a production consumer\n * build Vite statically inlines `import.meta.env` with `DEV: false`, so the\n * dev-login gate keeps failing closed exactly as before. Non-string values other\n * than `DEV` are dropped — the studio env surface is a string record.\n *\n * Passing `null`/`undefined` is a no-op (a consumer entry that predates the option\n * keeps today's engine-visible fallback behavior, plus a loud runtime warning when\n * that fallback is provably broken — see {@link maybeWarnEngineEnvUnreachable}).\n */\nexport function provideStudioEnv(env: Record<string, unknown> | null | undefined): void {\n if (env === null || env === undefined) return\n const record: EnvRecord = {}\n for (const [key, value] of Object.entries(env)) {\n if (typeof value === 'string') record[key] = value\n }\n providedEnv = {\n record,\n dev: env.DEV === true ? true : env.DEV === false ? false : undefined,\n }\n maybeWarnEngineEnvStale()\n}\n\n/** Test seam: clear the mount-provided env. Never used by app code. */\nexport function resetProvidedStudioEnvForTests(): void {\n providedEnv = null\n warnedStale = false\n warnedUnreachable = false\n}\n\n/**\n * Resolve the configured studio env. Precedence: the test override, else the\n * mount-provided env (over `process.env`), else the engine-visible Vite env\n * (over `process.env`).\n */\nexport function studioEnv(): EnvRecord {\n if (envOverride) return envOverride\n if (providedEnv) return { ...nodeEnv(), ...providedEnv.record }\n maybeWarnEngineEnvUnreachable()\n return { ...nodeEnv(), ...viteEnv() }\n}\n\n// --- Boot-time env self-check (EC-357, Oli: \"easier to catch\") ---------------\n\n/** The env keys whose loss/staleness breaks boot or sign-in. */\nexport const STUDIO_ENV_KEYS: readonly string[] = [\n 'VITE_ELYTRA_MODE',\n 'VITE_CONVEX_URL',\n 'VITE_ELYTRA_DEV_LOGIN',\n]\n\nexport type StudioEnvAnomaly =\n /** No mount-provided env AND the engine module has no `import.meta.env` at all. */\n | { kind: 'engine-env-unreachable' }\n /** The engine-visible env disagrees with the mount-provided env (stale optimizer cache). */\n | { kind: 'engine-env-stale'; keys: string[] }\n\n/**\n * Pure anomaly detection over the two env views (unit-testable core of the boot\n * warnings). `engineVisible === null` means the engine module has no\n * `import.meta.env` — expected under dep optimization WITH a provided env (silent),\n * fatal without one (the values can never arrive). A stale mismatch fires only for\n * keys the ENGINE sees with a different value than the mount provided (the Vite 7\n * freeze); keys the engine simply lacks (Vite 8) are not anomalies — the provided\n * env is authoritative and in effect.\n */\nexport function detectStudioEnvAnomaly(\n provided: EnvRecord | null,\n engineVisible: EnvRecord | null,\n inBrowser: boolean,\n): StudioEnvAnomaly | null {\n if (!inBrowser) return null\n if (!provided) return engineVisible === null ? { kind: 'engine-env-unreachable' } : null\n if (engineVisible === null) return null\n const stale = STUDIO_ENV_KEYS.filter((key) => {\n const engineValue = readEnv(engineVisible, key)\n return engineValue !== undefined && engineValue !== readEnv(provided, key)\n })\n return stale.length > 0 ? { kind: 'engine-env-stale', keys: stale } : null\n}\n\nfunction inBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined'\n}\n\nlet warnedUnreachable = false\nfunction maybeWarnEngineEnvUnreachable(): void {\n if (warnedUnreachable) return\n const anomaly = detectStudioEnvAnomaly(null, engineVisibleEnv(), inBrowser())\n if (anomaly?.kind !== 'engine-env-unreachable') return\n warnedUnreachable = true\n console.warn(\n '[elytra] The engine cannot see import.meta.env — this @elytracms module was dep-optimized by Vite, ' +\n 'so VITE_CONVEX_URL / VITE_ELYTRA_MODE / VITE_ELYTRA_DEV_LOGIN cannot reach it at runtime. ' +\n 'Fix: pass your studio entry’s env to the mount — createStudio(config, studioConfig, { env: import.meta.env }) — ' +\n \"or add optimizeDeps.exclude: ['@elytracms/studio', '@elytracms/core'] to the studio vite.config.ts.\",\n )\n}\n\nlet warnedStale = false\nfunction maybeWarnEngineEnvStale(): void {\n if (warnedStale || !providedEnv) return\n const anomaly = detectStudioEnvAnomaly(providedEnv.record, engineVisibleEnv(), inBrowser())\n if (anomaly?.kind !== 'engine-env-stale') return\n warnedStale = true\n console.warn(\n `[elytra] import.meta.env inside the engine disagrees with the studio mount's env for: ${anomaly.keys.join(', ')}. ` +\n \"The engine copy is a stale Vite dep-optimizer snapshot; the mount-provided values are in effect. \" +\n 'Delete node_modules/.vite to drop the stale copy.',\n )\n}\n\n/**\n * Pure mode decision — testable with a plain env record. The app calls it with\n * {@link studioEnv}; `CONVEX_URL` (Node-side alias) is honored for SSR/tools.\n */\nexport function resolveBackendConfig(env: EnvRecord = studioEnv()): StudioBackendConfig {\n const mode = readEnv(env, MODE_ENV_VAR)\n if (mode === PLAYGROUND_MODE) return { kind: 'playground' }\n if (mode !== undefined) {\n return {\n kind: 'misconfigured',\n reason: 'unknown-mode',\n detail: `${MODE_ENV_VAR}=\"${mode}\" is not a known mode. The only supported value is \"${PLAYGROUND_MODE}\"; unset it to run against Convex.`,\n }\n }\n const url = readEnv(env, CONVEX_URL_ENV_VAR) ?? readEnv(env, 'CONVEX_URL')\n if (url) return { kind: 'convex', url }\n return {\n kind: 'misconfigured',\n reason: 'missing-convex-url',\n // EC-357: name BOTH causes — the var may be genuinely unset, or set but never\n // reaching the client at runtime (Vite dep-optimized engine code; see\n // `provideStudioEnv`). The paired fix text (deployment-ready `FIX.backendUrl`)\n // carries the full remedy for the second cause.\n detail: `No persistence backend is configured — ${CONVEX_URL_ENV_VAR} is unset or is not reaching the client at runtime. Set it to your Convex deployment URL (see docs/backend-setup.md), or opt into the ephemeral local playground with ${MODE_ENV_VAR}=${PLAYGROUND_MODE}.`,\n }\n}\n\n/** True when the studio runs in the explicit, labeled playground mode. */\nexport function isPlaygroundMode(): boolean {\n return resolveBackendConfig().kind === 'playground'\n}\n\n// ---------------------------------------------------------------------------\n// Dev auto-login (unattended testing — NOT an auth bypass)\n// ---------------------------------------------------------------------------\n\n/** `email:password` for unattended dev sign-in (gitignored `.env.local`). */\nexport const DEV_LOGIN_ENV_VAR = 'VITE_ELYTRA_DEV_LOGIN'\n\n/** Parsed dev auto-login credentials. */\nexport interface DevAutoLoginCredentials {\n email: string\n password: string\n}\n\n/**\n * Is this a Vite *dev* build? The mount-provided env's captured `DEV` flag wins\n * (EC-357 — inside dep-optimized engine code the engine-visible flag is absent on\n * Vite ≥8 and frozen on Vite 7); the consumer's `import.meta.env.DEV` is statically\n * `true` under `vite dev` and statically `false` in any production build, so the\n * dev auto-login gate still fails closed in production. Without a provided env the\n * engine-visible static flag applies as before (where it also dead-code-eliminates\n * the feature out of a directly-bundled production build).\n */\nfunction isViteDevBuild(): boolean {\n if (providedEnv?.dev !== undefined) return providedEnv.dev\n if (typeof import.meta === 'undefined') return false\n return (import.meta as { env?: { DEV?: boolean } }).env?.DEV === true\n}\n\n/**\n * The studio's house dev-build check, public: dev-only surfaces (e.g. the\n * EC-414 canvas-theme migration notice) gate on THIS — it honors the\n * mount-provided env's captured `DEV` flag (the EC-357 dep-optimization\n * nuance) before falling back to the engine-visible static flag.\n */\nexport function isDevRuntime(): boolean {\n return isViteDevBuild()\n}\n\n/**\n * Dev-only unattended auth — **not a bypass**. When `VITE_ELYTRA_DEV_LOGIN=\n * email:password` is set in a dev build, the studio auto-signs-in with those\n * credentials on load (see {@link DevAutoLoginAdapter}) so local/automated\n * testing skips the sign-in screen while still establishing a REAL backend\n * session: Convex's server-side `requireRole` guards still apply, so the dev\n * user must exist and be a member. Returns null in production builds (gated by\n * {@link isViteDevBuild}) and when unset/malformed, so it can never weaken a\n * deployed app. `isDevBuild` is a parameter purely so the decision is unit\n * testable (`import.meta.env.DEV` is not reachable via `vi.stubEnv`).\n *\n * `password` may itself contain `:` — only the first colon separates the fields.\n */\nexport function resolveDevAutoLogin(\n env: EnvRecord = studioEnv(),\n isDevBuild: boolean = isViteDevBuild(),\n): DevAutoLoginCredentials | null {\n if (!isDevBuild) return null\n const raw = readEnv(env, DEV_LOGIN_ENV_VAR)\n if (!raw) return null\n const separator = raw.indexOf(':')\n if (separator <= 0) return null\n const email = raw.slice(0, separator).trim()\n const password = raw.slice(separator + 1)\n if (!email || !password) return null\n return { email, password }\n}\n\n/** True when dev auto-login is configured (dev build only) — for the UI marker. */\nexport function isDevAutoLoginActive(): boolean {\n return resolveDevAutoLogin() !== null\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DeploymentReadinessFacts, DeploymentReadinessReport } from '@elytracms/core/export-sync/deployment-ready';
|
|
1
|
+
import type { DeclaredEnvVar, DeploymentReadinessFacts, DeploymentReadinessReport } from '@elytracms/core/export-sync/deployment-ready';
|
|
2
2
|
import type { CollectionDef } from '@elytracms/core/cms-core';
|
|
3
3
|
import type { StudioBackendConfig } from './backend-mode.js';
|
|
4
4
|
import type { WorkspaceState } from '../workspace/index.js';
|
|
@@ -29,6 +29,13 @@ export interface StudioReadinessInputs {
|
|
|
29
29
|
available: boolean;
|
|
30
30
|
detail?: string;
|
|
31
31
|
};
|
|
32
|
+
/**
|
|
33
|
+
* Whether `defineStudio({ ai })` is configured and enabled (EC-399) — drives the
|
|
34
|
+
* AI provider-key expectation in the deployment-env checks.
|
|
35
|
+
*/
|
|
36
|
+
aiRouteConfigured?: boolean;
|
|
37
|
+
/** Consumer-declared deployment-env expectations (EC-399) — merged into the engine contract. */
|
|
38
|
+
declaredEnv?: readonly DeclaredEnvVar[];
|
|
32
39
|
}
|
|
33
40
|
/** Map the studio's resolved state onto the core readiness facts (pure). */
|
|
34
41
|
export declare function studioReadinessFacts(inputs: StudioReadinessInputs): DeploymentReadinessFacts;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { checkDeploymentReady } from '@elytracms/core/export-sync/deployment-ready';
|
|
1
|
+
import { checkDeploymentReady, expectedDeploymentEnv } from '@elytracms/core/export-sync/deployment-ready';
|
|
2
2
|
/** Map the studio's resolved state onto the core readiness facts (pure). */
|
|
3
3
|
export function studioReadinessFacts(inputs) {
|
|
4
4
|
const { backend, workspace, collections, reachable, effectiveUrl } = inputs;
|
|
@@ -12,11 +12,26 @@ export function studioReadinessFacts(inputs) {
|
|
|
12
12
|
: backend.kind === 'playground'
|
|
13
13
|
? { kind: 'playground' }
|
|
14
14
|
: { kind: 'unconfigured', detail: backend.detail };
|
|
15
|
+
// Expected-env facts (EC-399): the studio surfaces the SAME contract the CLI
|
|
16
|
+
// checks, but the browser can read neither the Convex deployment env nor the
|
|
17
|
+
// studio host's server env (the AI key is deliberately never VITE_-prefixed) —
|
|
18
|
+
// so no presence source is gathered and every env check surfaces as an honest
|
|
19
|
+
// `unknown` pointing at `elytra doctor`, exactly the auth-signing-keys precedent.
|
|
20
|
+
const deploymentEnv = backendFact.kind === 'convex'
|
|
21
|
+
? {
|
|
22
|
+
expected: expectedDeploymentEnv({
|
|
23
|
+
aiRouteConfigured: inputs.aiRouteConfigured ?? false,
|
|
24
|
+
...(inputs.declaredEnv ? { declared: inputs.declaredEnv } : {}),
|
|
25
|
+
}),
|
|
26
|
+
unavailableDetail: 'Not readable from the browser — run `elytra doctor` to check it (presence only).',
|
|
27
|
+
}
|
|
28
|
+
: undefined;
|
|
15
29
|
return {
|
|
16
30
|
backend: backendFact,
|
|
17
31
|
...(reachable ? { reachable } : {}),
|
|
18
32
|
collections,
|
|
19
33
|
config,
|
|
34
|
+
...(deploymentEnv ? { deploymentEnv } : {}),
|
|
20
35
|
};
|
|
21
36
|
}
|
|
22
37
|
/** Compose the studio's deployment-readiness report (pure). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deployment-readiness.js","sourceRoot":"","sources":["../../../src/lib/persistence/deployment-readiness.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,8CAA8C,CAAA;
|
|
1
|
+
{"version":3,"file":"deployment-readiness.js","sourceRoot":"","sources":["../../../src/lib/persistence/deployment-readiness.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,8CAA8C,CAAA;AA2C1G,4EAA4E;AAC5E,MAAM,UAAU,oBAAoB,CAAC,MAA6B;IAChE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,MAAM,CAAA;IAE3E,MAAM,MAAM,GACV,SAAS,CAAC,IAAI,KAAK,SAAS;QAC1B,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE;QACjF,CAAC,CAAC,SAAS,CAAC,IAAI,KAAK,QAAQ;YAC3B,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE;YAC3C,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IAEvB,MAAM,WAAW,GACf,OAAO,CAAC,IAAI,KAAK,QAAQ;QACvB,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,YAAY,IAAI,OAAO,CAAC,GAAG,EAAE;QACtD,CAAC,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY;YAC7B,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE;YACxB,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAA;IAExD,6EAA6E;IAC7E,6EAA6E;IAC7E,+EAA+E;IAC/E,8EAA8E;IAC9E,kFAAkF;IAClF,MAAM,aAAa,GACjB,WAAW,CAAC,IAAI,KAAK,QAAQ;QAC3B,CAAC,CAAC;YACE,QAAQ,EAAE,qBAAqB,CAAC;gBAC9B,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,IAAI,KAAK;gBACpD,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAChE,CAAC;YACF,iBAAiB,EAAE,kFAAkF;SACtG;QACH,CAAC,CAAC,SAAS,CAAA;IAEf,OAAO;QACL,OAAO,EAAE,WAAW;QACpB,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnC,WAAW;QACX,MAAM;QACN,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5C,CAAA;AACH,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,yBAAyB,CAAC,MAA6B;IACrE,OAAO,oBAAoB,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAA;AAC3D,CAAC","sourcesContent":["import { checkDeploymentReady, expectedDeploymentEnv } from '@elytracms/core/export-sync/deployment-ready'\nimport type {\n DeclaredEnvVar,\n DeploymentReadinessFacts,\n DeploymentReadinessReport,\n} from '@elytracms/core/export-sync/deployment-ready'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { StudioBackendConfig } from './backend-mode'\nimport type { WorkspaceState } from '../workspace'\n\n/**\n * Studio-side adapter for the shared `checkDeploymentReady` contract (EC-258).\n * Maps the studio's resolved state — backend mode (`resolveBackendConfig`),\n * workspace config (`getWorkspaceState`), config-owned collections\n * (`resolveDeploymentCollections`), and an optional reachability probe outcome —\n * onto the core-owned {@link DeploymentReadinessFacts}, then composes the report.\n *\n * A PURE mapper: the async I/O (the `adapter.status()` reachability probe) lives in\n * the boot `DeploymentSetupGate`, which passes the outcome in. `authKeys` is\n * deliberately left unprobed here — the deployment's JWKS endpoint is CORS-blocked\n * from the browser, so the auth-signing-keys check resolves to `unknown` at boot\n * (EC-257's honest sign-in error is the browser's actionable surface; the\n * `elytra doctor` CLI probes it definitively from node). `session` is left unset —\n * this gate runs before auth, so the per-user bucket stays `unknown` (never a false\n * fail) until a signed-in surface supplies it.\n */\nexport interface StudioReadinessInputs {\n backend: StudioBackendConfig\n /** The active project's effective deployment URL (`resolveDeploymentUrl`); defaults to `backend.url`. */\n effectiveUrl?: string\n workspace: WorkspaceState\n collections: readonly CollectionDef[]\n /** Reachability probe outcome (`adapter.status()`); omitted when not probed (playground / unconfigured). */\n reachable?: { available: boolean; detail?: string }\n /**\n * Whether `defineStudio({ ai })` is configured and enabled (EC-399) — drives the\n * AI provider-key expectation in the deployment-env checks.\n */\n aiRouteConfigured?: boolean\n /** Consumer-declared deployment-env expectations (EC-399) — merged into the engine contract. */\n declaredEnv?: readonly DeclaredEnvVar[]\n}\n\n/** Map the studio's resolved state onto the core readiness facts (pure). */\nexport function studioReadinessFacts(inputs: StudioReadinessInputs): DeploymentReadinessFacts {\n const { backend, workspace, collections, reachable, effectiveUrl } = inputs\n\n const config: DeploymentReadinessFacts['config'] =\n workspace.kind === 'invalid'\n ? { valid: false, source: workspace.source, issueCount: workspace.issues.length }\n : workspace.kind === 'config'\n ? { valid: true, source: workspace.source }\n : { valid: true }\n\n const backendFact: DeploymentReadinessFacts['backend'] =\n backend.kind === 'convex'\n ? { kind: 'convex', url: effectiveUrl ?? backend.url }\n : backend.kind === 'playground'\n ? { kind: 'playground' }\n : { kind: 'unconfigured', detail: backend.detail }\n\n // Expected-env facts (EC-399): the studio surfaces the SAME contract the CLI\n // checks, but the browser can read neither the Convex deployment env nor the\n // studio host's server env (the AI key is deliberately never VITE_-prefixed) —\n // so no presence source is gathered and every env check surfaces as an honest\n // `unknown` pointing at `elytra doctor`, exactly the auth-signing-keys precedent.\n const deploymentEnv: DeploymentReadinessFacts['deploymentEnv'] =\n backendFact.kind === 'convex'\n ? {\n expected: expectedDeploymentEnv({\n aiRouteConfigured: inputs.aiRouteConfigured ?? false,\n ...(inputs.declaredEnv ? { declared: inputs.declaredEnv } : {}),\n }),\n unavailableDetail: 'Not readable from the browser — run `elytra doctor` to check it (presence only).',\n }\n : undefined\n\n return {\n backend: backendFact,\n ...(reachable ? { reachable } : {}),\n collections,\n config,\n ...(deploymentEnv ? { deploymentEnv } : {}),\n }\n}\n\n/** Compose the studio's deployment-readiness report (pure). */\nexport function studioDeploymentReadiness(inputs: StudioReadinessInputs): DeploymentReadinessReport {\n return checkDeploymentReady(studioReadinessFacts(inputs))\n}\n"]}
|
|
@@ -26,7 +26,7 @@ export declare function activeDeploymentKey(): string;
|
|
|
26
26
|
*/
|
|
27
27
|
export declare function persistenceMode(): StudioBackendMode;
|
|
28
28
|
export { ConvexPersistenceAdapter } from './convex-adapter.js';
|
|
29
|
-
export { CONVEX_URL_ENV_VAR, DEV_LOGIN_ENV_VAR, MODE_ENV_VAR, PLAYGROUND_MODE, STUDIO_ENV_KEYS, detectStudioEnvAnomaly, isDevAutoLoginActive, isPlaygroundMode, provideStudioEnv, resetProvidedStudioEnvForTests, resolveBackendConfig, resolveDevAutoLogin, studioEnv, } from './backend-mode.js';
|
|
29
|
+
export { CONVEX_URL_ENV_VAR, DEV_LOGIN_ENV_VAR, MODE_ENV_VAR, PLAYGROUND_MODE, STUDIO_ENV_KEYS, detectStudioEnvAnomaly, isDevAutoLoginActive, isDevRuntime, isPlaygroundMode, provideStudioEnv, resetProvidedStudioEnvForTests, resolveBackendConfig, resolveDevAutoLogin, studioEnv, } from './backend-mode.js';
|
|
30
30
|
export type { DevAutoLoginCredentials, EnvRecord, StudioBackendConfig, StudioBackendMode, StudioEnvAnomaly, } from './backend-mode.js';
|
|
31
31
|
export { studioDeploymentReadiness, studioReadinessFacts } from './deployment-readiness.js';
|
|
32
32
|
export type { StudioReadinessInputs } from './deployment-readiness.js';
|
|
@@ -72,7 +72,7 @@ export function persistenceMode() {
|
|
|
72
72
|
return config.kind;
|
|
73
73
|
}
|
|
74
74
|
export { ConvexPersistenceAdapter } from './convex-adapter.js';
|
|
75
|
-
export { CONVEX_URL_ENV_VAR, DEV_LOGIN_ENV_VAR, MODE_ENV_VAR, PLAYGROUND_MODE, STUDIO_ENV_KEYS, detectStudioEnvAnomaly, isDevAutoLoginActive, isPlaygroundMode, provideStudioEnv, resetProvidedStudioEnvForTests, resolveBackendConfig, resolveDevAutoLogin, studioEnv, } from './backend-mode.js';
|
|
75
|
+
export { CONVEX_URL_ENV_VAR, DEV_LOGIN_ENV_VAR, MODE_ENV_VAR, PLAYGROUND_MODE, STUDIO_ENV_KEYS, detectStudioEnvAnomaly, isDevAutoLoginActive, isDevRuntime, isPlaygroundMode, provideStudioEnv, resetProvidedStudioEnvForTests, resolveBackendConfig, resolveDevAutoLogin, studioEnv, } from './backend-mode.js';
|
|
76
76
|
// EC-258: the deployment-readiness facts mapper (the boot DeploymentSetupGate + a
|
|
77
77
|
// future `elytra doctor` CLI compose the same `checkDeploymentReady` contract).
|
|
78
78
|
export { studioDeploymentReadiness, studioReadinessFacts } from './deployment-readiness.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/persistence/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,8BAA8B,EAAE,MAAM,6BAA6B,CAAA;AAE7F,OAAO,EACL,4BAA4B,EAC5B,0BAA0B,EAC1B,oBAAoB,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAGrD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB;IAC5C,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,wBAAwB,CACjC,eAAe,CAAC,oBAAoB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EACjD,0BAA0B,CAAC,kBAAkB,CAAC;QAC9C,sEAAsE;QACtE,0EAA0E;QAC1E,sCAAsC;QACtC,4BAA4B,EAAE,CAC/B,CAAA;IACH,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QACjC,6EAA6E;QAC7E,0DAA0D;QAC1D,0EAA0E;QAC1E,+EAA+E;QAC/E,+EAA+E;QAC/E,8EAA8E;QAC9E,MAAM,SAAS,GAAG,0BAA0B,CAAC,eAAe,CAAC,CAAA;QAC7D,MAAM,OAAO,GAAG,MAAM,8BAA8B,CAAC,SAAS,CAAC,CAAA;QAC/D,6EAA6E;QAC7E,gFAAgF;QAChF,8EAA8E;QAC9E,+EAA+E;QAC/E,2EAA2E;QAC3E,wEAAwE;QACxE,6EAA6E;QAC7E,MAAM,WAAW,GAAG,4BAA4B,EAAE,CAAA;QAClD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,EAAE,WAAW,CAAC,CAAA;QAChF,OAAO,OAAO,CAAA;IAChB,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;AAChC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,oBAAoB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACrE,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,YAAY,CAAA;IACrD,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;AAChC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,IAAI,MAAM,CAAC,IAAI,KAAK,eAAe;QAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACnE,OAAO,MAAM,CAAC,IAAI,CAAA;AACpB,CAAC;AAED,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAChB,8BAA8B,EAC9B,oBAAoB,EACpB,mBAAmB,EACnB,SAAS,GACV,MAAM,gBAAgB,CAAA;AAQvB,kFAAkF;AAClF,gFAAgF;AAChF,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA","sourcesContent":["import { SEED_PROJECT_ID, createSeededPersistenceAdapter } from '@elytracms/core/persistence'\nimport type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport {\n resolveDeploymentCollections,\n resolveDeploymentProjectId,\n resolveDeploymentUrl,\n} from '../workspace'\nimport { getConvexClient } from './convex-client'\nimport { ConvexPersistenceAdapter } from './convex-adapter'\nimport { resolveBackendConfig } from './backend-mode'\nimport type { StudioBackendMode } from './backend-mode'\n\n/**\n * Select the persistence backend (EC-153, vision AD-1). **Convex is the\n * default**: a configured `VITE_CONVEX_URL` is required unless the explicit\n * playground flag (`VITE_ELYTRA_MODE=playground`) opts into the deterministic,\n * seeded in-memory adapter. There is **no automatic in-memory fallback** —\n * missing configuration is a hard, descriptive error (the `DeploymentSetupGate`\n * renders it as a boot-error screen before any adapter is requested).\n *\n * EC-150: the Convex path uses the *shared* client so the auth adapter's\n * session JWT (set via `client.setAuth`) authenticates every persistence call.\n */\nexport async function selectPersistenceAdapter(): Promise<PersistenceAdapter> {\n const config = resolveBackendConfig()\n if (config.kind === 'convex') {\n return new ConvexPersistenceAdapter(\n getConvexClient(resolveDeploymentUrl(config.url)),\n resolveDeploymentProjectId('aurora-marketing'),\n // CMS collections are config-owned (EC-183, AD-11): inject the active\n // project's collections so the adapter serves the schema from config (the\n // backend has no `cmsSchemas` table).\n resolveDeploymentCollections(),\n )\n }\n if (config.kind === 'playground') {\n // EC-207: routes/redirects are config-sourced, never seeded to the backend —\n // the playground adapter no longer seeds route documents.\n // EC-310: key the seed under the ACTIVE project id (the config slug, e.g.\n // `aurora`) — not the engine's default `prj_aurora` — so content + memberships\n // sit under the same id the studio operates on. Otherwise authorization denies\n // every read (\"not a member of project 'aurora'\") and content resolves empty.\n const projectId = resolveDeploymentProjectId(SEED_PROJECT_ID)\n const adapter = await createSeededPersistenceAdapter(projectId)\n // EC-361 (AD-11 parity with the Convex adapter above): when a config project\n // declares collections, the playground serves the CONFIG-OWNED schema — not the\n // engine's seeded sample schema — so every schema reader (the content-session\n // hydration, the persister baseline, query validation's `filterable` set) sees\n // the same declarations connected mode serves. The resolved config already\n // carries the EC-257 default asset collection. Without a config (engine\n // fixtures/tests) the seeded sample schema stands, byte-identical to before.\n const collections = resolveDeploymentCollections()\n if (collections.length > 0) await adapter.cms.saveSchema(projectId, collections)\n return adapter\n }\n throw new Error(config.detail)\n}\n\n/**\n * A stable key identifying the active backend (EC-179): the resolved deployment\n * URL for the active project under Convex, or `playground`. The operations\n * bootstrap caches one bundle per key, so switching to a project on a different\n * deployment transparently selects (or builds) that backend's operations client.\n */\nexport function activeDeploymentKey(): string {\n const config = resolveBackendConfig()\n if (config.kind === 'convex') return resolveDeploymentUrl(config.url)\n if (config.kind === 'playground') return 'playground'\n throw new Error(config.detail)\n}\n\n/**\n * The active backend mode. Throws the same descriptive error as\n * {@link selectPersistenceAdapter} when the studio is misconfigured — UI that\n * only needs \"is this the playground?\" should use `isPlaygroundMode()`.\n */\nexport function persistenceMode(): StudioBackendMode {\n const config = resolveBackendConfig()\n if (config.kind === 'misconfigured') throw new Error(config.detail)\n return config.kind\n}\n\nexport { ConvexPersistenceAdapter } from './convex-adapter'\nexport {\n CONVEX_URL_ENV_VAR,\n DEV_LOGIN_ENV_VAR,\n MODE_ENV_VAR,\n PLAYGROUND_MODE,\n STUDIO_ENV_KEYS,\n detectStudioEnvAnomaly,\n isDevAutoLoginActive,\n isPlaygroundMode,\n provideStudioEnv,\n resetProvidedStudioEnvForTests,\n resolveBackendConfig,\n resolveDevAutoLogin,\n studioEnv,\n} from './backend-mode'\nexport type {\n DevAutoLoginCredentials,\n EnvRecord,\n StudioBackendConfig,\n StudioBackendMode,\n StudioEnvAnomaly,\n} from './backend-mode'\n// EC-258: the deployment-readiness facts mapper (the boot DeploymentSetupGate + a\n// future `elytra doctor` CLI compose the same `checkDeploymentReady` contract).\nexport { studioDeploymentReadiness, studioReadinessFacts } from './deployment-readiness'\nexport type { StudioReadinessInputs } from './deployment-readiness'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/persistence/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,8BAA8B,EAAE,MAAM,6BAA6B,CAAA;AAE7F,OAAO,EACL,4BAA4B,EAC5B,0BAA0B,EAC1B,oBAAoB,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAGrD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB;IAC5C,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,wBAAwB,CACjC,eAAe,CAAC,oBAAoB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EACjD,0BAA0B,CAAC,kBAAkB,CAAC;QAC9C,sEAAsE;QACtE,0EAA0E;QAC1E,sCAAsC;QACtC,4BAA4B,EAAE,CAC/B,CAAA;IACH,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QACjC,6EAA6E;QAC7E,0DAA0D;QAC1D,0EAA0E;QAC1E,+EAA+E;QAC/E,+EAA+E;QAC/E,8EAA8E;QAC9E,MAAM,SAAS,GAAG,0BAA0B,CAAC,eAAe,CAAC,CAAA;QAC7D,MAAM,OAAO,GAAG,MAAM,8BAA8B,CAAC,SAAS,CAAC,CAAA;QAC/D,6EAA6E;QAC7E,gFAAgF;QAChF,8EAA8E;QAC9E,+EAA+E;QAC/E,2EAA2E;QAC3E,wEAAwE;QACxE,6EAA6E;QAC7E,MAAM,WAAW,GAAG,4BAA4B,EAAE,CAAA;QAClD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,EAAE,WAAW,CAAC,CAAA;QAChF,OAAO,OAAO,CAAA;IAChB,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;AAChC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,oBAAoB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACrE,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,YAAY,CAAA;IACrD,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;AAChC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,IAAI,MAAM,CAAC,IAAI,KAAK,eAAe;QAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACnE,OAAO,MAAM,CAAC,IAAI,CAAA;AACpB,CAAC;AAED,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,8BAA8B,EAC9B,oBAAoB,EACpB,mBAAmB,EACnB,SAAS,GACV,MAAM,gBAAgB,CAAA;AAQvB,kFAAkF;AAClF,gFAAgF;AAChF,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA","sourcesContent":["import { SEED_PROJECT_ID, createSeededPersistenceAdapter } from '@elytracms/core/persistence'\nimport type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport {\n resolveDeploymentCollections,\n resolveDeploymentProjectId,\n resolveDeploymentUrl,\n} from '../workspace'\nimport { getConvexClient } from './convex-client'\nimport { ConvexPersistenceAdapter } from './convex-adapter'\nimport { resolveBackendConfig } from './backend-mode'\nimport type { StudioBackendMode } from './backend-mode'\n\n/**\n * Select the persistence backend (EC-153, vision AD-1). **Convex is the\n * default**: a configured `VITE_CONVEX_URL` is required unless the explicit\n * playground flag (`VITE_ELYTRA_MODE=playground`) opts into the deterministic,\n * seeded in-memory adapter. There is **no automatic in-memory fallback** —\n * missing configuration is a hard, descriptive error (the `DeploymentSetupGate`\n * renders it as a boot-error screen before any adapter is requested).\n *\n * EC-150: the Convex path uses the *shared* client so the auth adapter's\n * session JWT (set via `client.setAuth`) authenticates every persistence call.\n */\nexport async function selectPersistenceAdapter(): Promise<PersistenceAdapter> {\n const config = resolveBackendConfig()\n if (config.kind === 'convex') {\n return new ConvexPersistenceAdapter(\n getConvexClient(resolveDeploymentUrl(config.url)),\n resolveDeploymentProjectId('aurora-marketing'),\n // CMS collections are config-owned (EC-183, AD-11): inject the active\n // project's collections so the adapter serves the schema from config (the\n // backend has no `cmsSchemas` table).\n resolveDeploymentCollections(),\n )\n }\n if (config.kind === 'playground') {\n // EC-207: routes/redirects are config-sourced, never seeded to the backend —\n // the playground adapter no longer seeds route documents.\n // EC-310: key the seed under the ACTIVE project id (the config slug, e.g.\n // `aurora`) — not the engine's default `prj_aurora` — so content + memberships\n // sit under the same id the studio operates on. Otherwise authorization denies\n // every read (\"not a member of project 'aurora'\") and content resolves empty.\n const projectId = resolveDeploymentProjectId(SEED_PROJECT_ID)\n const adapter = await createSeededPersistenceAdapter(projectId)\n // EC-361 (AD-11 parity with the Convex adapter above): when a config project\n // declares collections, the playground serves the CONFIG-OWNED schema — not the\n // engine's seeded sample schema — so every schema reader (the content-session\n // hydration, the persister baseline, query validation's `filterable` set) sees\n // the same declarations connected mode serves. The resolved config already\n // carries the EC-257 default asset collection. Without a config (engine\n // fixtures/tests) the seeded sample schema stands, byte-identical to before.\n const collections = resolveDeploymentCollections()\n if (collections.length > 0) await adapter.cms.saveSchema(projectId, collections)\n return adapter\n }\n throw new Error(config.detail)\n}\n\n/**\n * A stable key identifying the active backend (EC-179): the resolved deployment\n * URL for the active project under Convex, or `playground`. The operations\n * bootstrap caches one bundle per key, so switching to a project on a different\n * deployment transparently selects (or builds) that backend's operations client.\n */\nexport function activeDeploymentKey(): string {\n const config = resolveBackendConfig()\n if (config.kind === 'convex') return resolveDeploymentUrl(config.url)\n if (config.kind === 'playground') return 'playground'\n throw new Error(config.detail)\n}\n\n/**\n * The active backend mode. Throws the same descriptive error as\n * {@link selectPersistenceAdapter} when the studio is misconfigured — UI that\n * only needs \"is this the playground?\" should use `isPlaygroundMode()`.\n */\nexport function persistenceMode(): StudioBackendMode {\n const config = resolveBackendConfig()\n if (config.kind === 'misconfigured') throw new Error(config.detail)\n return config.kind\n}\n\nexport { ConvexPersistenceAdapter } from './convex-adapter'\nexport {\n CONVEX_URL_ENV_VAR,\n DEV_LOGIN_ENV_VAR,\n MODE_ENV_VAR,\n PLAYGROUND_MODE,\n STUDIO_ENV_KEYS,\n detectStudioEnvAnomaly,\n isDevAutoLoginActive,\n isDevRuntime,\n isPlaygroundMode,\n provideStudioEnv,\n resetProvidedStudioEnvForTests,\n resolveBackendConfig,\n resolveDevAutoLogin,\n studioEnv,\n} from './backend-mode'\nexport type {\n DevAutoLoginCredentials,\n EnvRecord,\n StudioBackendConfig,\n StudioBackendMode,\n StudioEnvAnomaly,\n} from './backend-mode'\n// EC-258: the deployment-readiness facts mapper (the boot DeploymentSetupGate + a\n// future `elytra doctor` CLI compose the same `checkDeploymentReady` contract).\nexport { studioDeploymentReadiness, studioReadinessFacts } from './deployment-readiness'\nexport type { StudioReadinessInputs } from './deployment-readiness'\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AuthStatus } from './auth/index.js';
|
|
2
|
+
import type { OperationError, OperationEvent, OperationEventBus } from './operations/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* App-level session-expiry chokepoint (EC-403).
|
|
5
|
+
*
|
|
6
|
+
* Detection is reactive: an expired Convex session surfaces as a failed
|
|
7
|
+
* operation whose error matches {@link isSessionExpiredError}. Every studio
|
|
8
|
+
* command/query already flows through the operations pipeline, and every
|
|
9
|
+
* failure is emitted on the shared {@link OperationEventBus} — so ONE
|
|
10
|
+
* subscriber on that bus ({@link attachSessionExpiryReporting}, attached in
|
|
11
|
+
* `createStudioOperations`) sees every expiry, no matter which surface issued
|
|
12
|
+
* the operation. Surfaces that hold a structured `OperationError` directly
|
|
13
|
+
* (the shared error banner) report through the same funnel
|
|
14
|
+
* ({@link reportSessionExpiredError}).
|
|
15
|
+
*
|
|
16
|
+
* The consumer is the auth store (`components/auth-context.tsx`): it raises
|
|
17
|
+
* one app-wide `sessionExpired` state which mounts the blocking re-auth
|
|
18
|
+
* overlay — in place, no navigation, the React tree and all unsaved editor
|
|
19
|
+
* state stay alive. This module is deliberately framework-free (a plain
|
|
20
|
+
* listener registry + pure predicates) so the state machine is unit-testable.
|
|
21
|
+
*
|
|
22
|
+
* Proactive re-validation (window focus/interval) deliberately does NOT ship:
|
|
23
|
+
* the auth adapters' `currentSession()` treats *any* failure — including a
|
|
24
|
+
* transient network drop — as "signed out" and destroys the stored token, so
|
|
25
|
+
* polling it would sign editors out on a network blip. Distinguishing
|
|
26
|
+
* "token invalid" from "backend unreachable" needs an adapter change under
|
|
27
|
+
* the no-touch `lib/auth/**` (reported as a finding on EC-403).
|
|
28
|
+
*/
|
|
29
|
+
type SessionExpiryListener = () => void;
|
|
30
|
+
/**
|
|
31
|
+
* Subscribe to session-expiry reports (the auth store does, once). Returns the
|
|
32
|
+
* unsubscribe function.
|
|
33
|
+
*/
|
|
34
|
+
export declare function onSessionExpiry(listener: SessionExpiryListener): () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Report a structured operation failure as a *possible* session expiry: runs
|
|
37
|
+
* the {@link isSessionExpiredError} predicate, dedupes per error instance, and
|
|
38
|
+
* notifies listeners. Returns whether listeners were notified. Non-expiry
|
|
39
|
+
* errors are ignored — callers can funnel every failure through unconditionally.
|
|
40
|
+
*/
|
|
41
|
+
export declare function reportSessionExpiredError(error: OperationError): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Is this operation lifecycle event a session-expiry failure? Only `failed`
|
|
44
|
+
* events can carry the backend's unauthenticated transport blob (`rejected` is
|
|
45
|
+
* input validation, `denied` is the role hook — both structured, never expiry).
|
|
46
|
+
*/
|
|
47
|
+
export declare function isSessionExpiryEvent(event: OperationEvent): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The one reactive detection seam (EC-403): subscribe to an operations event
|
|
50
|
+
* bus and report every session-expiry failure. Attached per operations bundle
|
|
51
|
+
* in `createStudioOperations`, so commands, queries, and changeset operations
|
|
52
|
+
* are all covered. Returns the unsubscribe function.
|
|
53
|
+
*/
|
|
54
|
+
export declare function attachSessionExpiryReporting(events: OperationEventBus): () => void;
|
|
55
|
+
/**
|
|
56
|
+
* Pure raise-guard for the auth store's `sessionExpired` state: an expiry
|
|
57
|
+
* report blocks the studio only when a live session exists (a failure surfacing
|
|
58
|
+
* while signed out or still restoring must not overlay the sign-in screen),
|
|
59
|
+
* and raising is idempotent while already blocked.
|
|
60
|
+
*/
|
|
61
|
+
export declare function shouldBlockOnSessionExpiry(input: {
|
|
62
|
+
status: AuthStatus;
|
|
63
|
+
alreadyBlocked: boolean;
|
|
64
|
+
}): boolean;
|
|
65
|
+
/** Test seam: forget reported errors and listeners between test cases. */
|
|
66
|
+
export declare function resetSessionExpiryForTests(): void;
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { isSessionExpiredError } from './auth-errors.js';
|
|
2
|
+
const listeners = new Set();
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe to session-expiry reports (the auth store does, once). Returns the
|
|
5
|
+
* unsubscribe function.
|
|
6
|
+
*/
|
|
7
|
+
export function onSessionExpiry(listener) {
|
|
8
|
+
listeners.add(listener);
|
|
9
|
+
return () => {
|
|
10
|
+
listeners.delete(listener);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Each expired-session error object is reported at most ONCE. The bus produces
|
|
15
|
+
* a fresh error object per failure, so genuine new failures always report; but
|
|
16
|
+
* a surface can keep rendering a *stale* error long after re-auth (a
|
|
17
|
+
* `persistError` only clears on the next successful save), and remounting that
|
|
18
|
+
* surface must not re-raise the overlay against a session that is valid again.
|
|
19
|
+
*/
|
|
20
|
+
let reportedErrors = new WeakSet();
|
|
21
|
+
/**
|
|
22
|
+
* Report a structured operation failure as a *possible* session expiry: runs
|
|
23
|
+
* the {@link isSessionExpiredError} predicate, dedupes per error instance, and
|
|
24
|
+
* notifies listeners. Returns whether listeners were notified. Non-expiry
|
|
25
|
+
* errors are ignored — callers can funnel every failure through unconditionally.
|
|
26
|
+
*/
|
|
27
|
+
export function reportSessionExpiredError(error) {
|
|
28
|
+
if (!isSessionExpiredError(error))
|
|
29
|
+
return false;
|
|
30
|
+
if (reportedErrors.has(error))
|
|
31
|
+
return false;
|
|
32
|
+
reportedErrors.add(error);
|
|
33
|
+
for (const listener of [...listeners])
|
|
34
|
+
listener();
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Is this operation lifecycle event a session-expiry failure? Only `failed`
|
|
39
|
+
* events can carry the backend's unauthenticated transport blob (`rejected` is
|
|
40
|
+
* input validation, `denied` is the role hook — both structured, never expiry).
|
|
41
|
+
*/
|
|
42
|
+
export function isSessionExpiryEvent(event) {
|
|
43
|
+
return event.status === 'failed' && event.error !== undefined && isSessionExpiredError(event.error);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The one reactive detection seam (EC-403): subscribe to an operations event
|
|
47
|
+
* bus and report every session-expiry failure. Attached per operations bundle
|
|
48
|
+
* in `createStudioOperations`, so commands, queries, and changeset operations
|
|
49
|
+
* are all covered. Returns the unsubscribe function.
|
|
50
|
+
*/
|
|
51
|
+
export function attachSessionExpiryReporting(events) {
|
|
52
|
+
return events.subscribe((event) => {
|
|
53
|
+
if (event.status === 'failed' && event.error !== undefined) {
|
|
54
|
+
reportSessionExpiredError(event.error);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Pure raise-guard for the auth store's `sessionExpired` state: an expiry
|
|
60
|
+
* report blocks the studio only when a live session exists (a failure surfacing
|
|
61
|
+
* while signed out or still restoring must not overlay the sign-in screen),
|
|
62
|
+
* and raising is idempotent while already blocked.
|
|
63
|
+
*/
|
|
64
|
+
export function shouldBlockOnSessionExpiry(input) {
|
|
65
|
+
return input.status === 'signed-in' && !input.alreadyBlocked;
|
|
66
|
+
}
|
|
67
|
+
/** Test seam: forget reported errors and listeners between test cases. */
|
|
68
|
+
export function resetSessionExpiryForTests() {
|
|
69
|
+
reportedErrors = new WeakSet();
|
|
70
|
+
listeners.clear();
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=session-expiry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-expiry.js","sourceRoot":"","sources":["../../src/lib/session-expiry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAiCrD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAyB,CAAA;AAElD;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,QAA+B;IAC7D,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC5B,CAAC,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,IAAI,cAAc,GAAG,IAAI,OAAO,EAAkB,CAAA;AAElD;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAqB;IAC7D,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAC/C,IAAI,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAC3C,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IACzB,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,SAAS,CAAC;QAAE,QAAQ,EAAE,CAAA;IACjD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAqB;IACxD,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,qBAAqB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;AACrG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAAyB;IACpE,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;QAChC,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC3D,yBAAyB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QACxC,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CAAC,KAG1C;IACC,OAAO,KAAK,CAAC,MAAM,KAAK,WAAW,IAAI,CAAC,KAAK,CAAC,cAAc,CAAA;AAC9D,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,0BAA0B;IACxC,cAAc,GAAG,IAAI,OAAO,EAAkB,CAAA;IAC9C,SAAS,CAAC,KAAK,EAAE,CAAA;AACnB,CAAC","sourcesContent":["import { isSessionExpiredError } from './auth-errors'\nimport type { AuthStatus } from './auth'\nimport type { OperationError, OperationEvent, OperationEventBus } from './operations'\n\n/**\n * App-level session-expiry chokepoint (EC-403).\n *\n * Detection is reactive: an expired Convex session surfaces as a failed\n * operation whose error matches {@link isSessionExpiredError}. Every studio\n * command/query already flows through the operations pipeline, and every\n * failure is emitted on the shared {@link OperationEventBus} — so ONE\n * subscriber on that bus ({@link attachSessionExpiryReporting}, attached in\n * `createStudioOperations`) sees every expiry, no matter which surface issued\n * the operation. Surfaces that hold a structured `OperationError` directly\n * (the shared error banner) report through the same funnel\n * ({@link reportSessionExpiredError}).\n *\n * The consumer is the auth store (`components/auth-context.tsx`): it raises\n * one app-wide `sessionExpired` state which mounts the blocking re-auth\n * overlay — in place, no navigation, the React tree and all unsaved editor\n * state stay alive. This module is deliberately framework-free (a plain\n * listener registry + pure predicates) so the state machine is unit-testable.\n *\n * Proactive re-validation (window focus/interval) deliberately does NOT ship:\n * the auth adapters' `currentSession()` treats *any* failure — including a\n * transient network drop — as \"signed out\" and destroys the stored token, so\n * polling it would sign editors out on a network blip. Distinguishing\n * \"token invalid\" from \"backend unreachable\" needs an adapter change under\n * the no-touch `lib/auth/**` (reported as a finding on EC-403).\n */\n\ntype SessionExpiryListener = () => void\n\nconst listeners = new Set<SessionExpiryListener>()\n\n/**\n * Subscribe to session-expiry reports (the auth store does, once). Returns the\n * unsubscribe function.\n */\nexport function onSessionExpiry(listener: SessionExpiryListener): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Each expired-session error object is reported at most ONCE. The bus produces\n * a fresh error object per failure, so genuine new failures always report; but\n * a surface can keep rendering a *stale* error long after re-auth (a\n * `persistError` only clears on the next successful save), and remounting that\n * surface must not re-raise the overlay against a session that is valid again.\n */\nlet reportedErrors = new WeakSet<OperationError>()\n\n/**\n * Report a structured operation failure as a *possible* session expiry: runs\n * the {@link isSessionExpiredError} predicate, dedupes per error instance, and\n * notifies listeners. Returns whether listeners were notified. Non-expiry\n * errors are ignored — callers can funnel every failure through unconditionally.\n */\nexport function reportSessionExpiredError(error: OperationError): boolean {\n if (!isSessionExpiredError(error)) return false\n if (reportedErrors.has(error)) return false\n reportedErrors.add(error)\n for (const listener of [...listeners]) listener()\n return true\n}\n\n/**\n * Is this operation lifecycle event a session-expiry failure? Only `failed`\n * events can carry the backend's unauthenticated transport blob (`rejected` is\n * input validation, `denied` is the role hook — both structured, never expiry).\n */\nexport function isSessionExpiryEvent(event: OperationEvent): boolean {\n return event.status === 'failed' && event.error !== undefined && isSessionExpiredError(event.error)\n}\n\n/**\n * The one reactive detection seam (EC-403): subscribe to an operations event\n * bus and report every session-expiry failure. Attached per operations bundle\n * in `createStudioOperations`, so commands, queries, and changeset operations\n * are all covered. Returns the unsubscribe function.\n */\nexport function attachSessionExpiryReporting(events: OperationEventBus): () => void {\n return events.subscribe((event) => {\n if (event.status === 'failed' && event.error !== undefined) {\n reportSessionExpiredError(event.error)\n }\n })\n}\n\n/**\n * Pure raise-guard for the auth store's `sessionExpired` state: an expiry\n * report blocks the studio only when a live session exists (a failure surfacing\n * while signed out or still restoring must not overlay the sign-in screen),\n * and raising is idempotent while already blocked.\n */\nexport function shouldBlockOnSessionExpiry(input: {\n status: AuthStatus\n alreadyBlocked: boolean\n}): boolean {\n return input.status === 'signed-in' && !input.alreadyBlocked\n}\n\n/** Test seam: forget reported errors and listeners between test cases. */\nexport function resetSessionExpiryForTests(): void {\n reportedErrors = new WeakSet<OperationError>()\n listeners.clear()\n}\n"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-view browser tab titles (EC-402). The studio mounts as ONE catch-all
|
|
3
|
+
* splat route, so per-view titles cannot come from per-route `head()` — the
|
|
4
|
+
* shell composes `document.title` from the data it already assembles for the
|
|
5
|
+
* header breadcrumb: `deepest crumb · view label · project name · base title`.
|
|
6
|
+
*
|
|
7
|
+
* This module is the pure core: the composition rule, the parts selector, and
|
|
8
|
+
* the base-title holder. The `document.title` assignment itself is a thin
|
|
9
|
+
* effect in the shell (`StudioTabTitle` in `components/studio-shell.tsx`) —
|
|
10
|
+
* effect-only, so SSR never touches the DOM.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Join title parts with ` · `, dropping empty/whitespace-only segments and
|
|
14
|
+
* adjacent duplicates (e.g. a "Settings" view whose deepest crumb is also
|
|
15
|
+
* "Settings", or a project literally named like the base title). Segments are
|
|
16
|
+
* trimmed; there are never leading, trailing, or doubled separators.
|
|
17
|
+
*/
|
|
18
|
+
export declare function composeTabTitle(parts: ReadonlyArray<string | null | undefined>): string;
|
|
19
|
+
/** The shell state the tab title is derived from — all sources the header already has. */
|
|
20
|
+
export interface TabTitleState {
|
|
21
|
+
/**
|
|
22
|
+
* The contributed breadcrumb trail below the active section, shallowest →
|
|
23
|
+
* deepest (the shell's `useBreadcrumbTrail()` value). Only the DEEPEST
|
|
24
|
+
* segment (collection → document/component title) enters the tab title.
|
|
25
|
+
*/
|
|
26
|
+
breadcrumb: ReadonlyArray<{
|
|
27
|
+
label: string;
|
|
28
|
+
}>;
|
|
29
|
+
/** The active view's resolved (localized) label; absent on unknown views. */
|
|
30
|
+
viewLabel: string | null | undefined;
|
|
31
|
+
/** The active project's display name. */
|
|
32
|
+
projectName: string | null | undefined;
|
|
33
|
+
/** The consumer's base title (see {@link studioBaseTitle}) — branding always lands. */
|
|
34
|
+
baseTitle: string | null | undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The ordered tab-title parts for a shell state:
|
|
38
|
+
* `deepest crumb · view label · project name · base title`.
|
|
39
|
+
* Feed the result to {@link composeTabTitle} (which drops the empty slots).
|
|
40
|
+
*/
|
|
41
|
+
export declare function tabTitleParts(state: TabTitleState): Array<string | null | undefined>;
|
|
42
|
+
/** Register the configured base title (called by `createStudioRootRoute`). */
|
|
43
|
+
export declare function setStudioBaseTitle(title: string): void;
|
|
44
|
+
/**
|
|
45
|
+
* The base title the launcher/sign-in screens keep and composed titles end
|
|
46
|
+
* with. Prefers the registered `createStudioRootRoute` title; a hand-rolled
|
|
47
|
+
* root that never registered one falls back to capturing `document.title`
|
|
48
|
+
* once (before the shell effect first writes it). SSR-safe: never touches
|
|
49
|
+
* `document` unless it exists.
|
|
50
|
+
*/
|
|
51
|
+
export declare function studioBaseTitle(): string;
|
|
52
|
+
/** Test-only: reset both the registered and the captured base title. */
|
|
53
|
+
export declare function resetStudioBaseTitleForTests(): void;
|