@elytracms/studio 0.0.53 → 0.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/components/app-views/default-modules.d.ts +16 -2
  2. package/dist/components/app-views/default-modules.js +22 -5
  3. package/dist/components/app-views/default-modules.js.map +1 -1
  4. package/dist/components/sign-in-view.d.ts +41 -0
  5. package/dist/components/sign-in-view.js +75 -0
  6. package/dist/components/sign-in-view.js.map +1 -0
  7. package/dist/lib/ai/convex-session-headers.d.ts +68 -0
  8. package/dist/lib/ai/convex-session-headers.js +79 -0
  9. package/dist/lib/ai/convex-session-headers.js.map +1 -0
  10. package/dist/lib/ai/convex-session.d.ts +82 -0
  11. package/dist/lib/ai/convex-session.js +92 -0
  12. package/dist/lib/ai/convex-session.js.map +1 -0
  13. package/dist/lib/ai/index.d.ts +12 -1
  14. package/dist/lib/ai/index.js +12 -1
  15. package/dist/lib/ai/index.js.map +1 -1
  16. package/dist/lib/ai/node-handler.d.ts +29 -0
  17. package/dist/lib/ai/node-handler.js +48 -0
  18. package/dist/lib/ai/node-handler.js.map +1 -0
  19. package/dist/lib/ai/vite.d.ts +32 -0
  20. package/dist/lib/ai/vite.js +97 -0
  21. package/dist/lib/ai/vite.js.map +1 -0
  22. package/dist/lib/persistence/backend-mode.d.ts +53 -1
  23. package/dist/lib/persistence/backend-mode.js +141 -10
  24. package/dist/lib/persistence/backend-mode.js.map +1 -1
  25. package/dist/lib/persistence/index.d.ts +2 -2
  26. package/dist/lib/persistence/index.js +1 -1
  27. package/dist/lib/persistence/index.js.map +1 -1
  28. package/dist/lib/webhooks/config.js +6 -5
  29. package/dist/lib/webhooks/config.js.map +1 -1
  30. package/dist/lib/workspace/index.d.ts +24 -2
  31. package/dist/lib/workspace/index.js +24 -17
  32. package/dist/lib/workspace/index.js.map +1 -1
  33. package/dist/router/root.d.ts +50 -0
  34. package/dist/router/root.js +69 -0
  35. package/dist/router/root.js.map +1 -0
  36. package/package.json +17 -5
@@ -35,8 +35,60 @@ export type EnvRecord = Record<string, string | undefined>;
35
35
  * use this instead — keeping results independent of any developer `.env.local`.
36
36
  */
37
37
  export declare function setStudioEnvOverrideForTests(env: EnvRecord | null): void;
38
- /** Resolve the configured studio env (Vite client env wins over process.env). */
38
+ /**
39
+ * Install the CONSUMER-captured env as the studio's env source (EC-357). The root
40
+ * cause this fixes: the engine used to read `import.meta.env.VITE_*` inside
41
+ * `@elytracms` package code, which Vite dep-optimizes in a real consumer — on
42
+ * Vite ≥8 those values are simply absent at runtime in the optimized chunk (the
43
+ * backend URL / mode / dev-login silently vanish and every sign-in fails), and on
44
+ * Vite 7 they froze at optimize time (stale until `node_modules/.vite` is wiped).
45
+ *
46
+ * The consumer's own entry module is code Vite ALWAYS transforms live, so the env
47
+ * is captured there and handed down: `createStudio(config, studioConfig, { env:
48
+ * import.meta.env })`. When a provided env exists it REPLACES the engine-visible
49
+ * Vite layer entirely (never merged with it — a stale optimizer snapshot must not
50
+ * shadow a removed key); `process.env` stays underneath as the SSR/tooling base.
51
+ *
52
+ * `DEV` is captured alongside (it gates dev auto-login): in a production consumer
53
+ * build Vite statically inlines `import.meta.env` with `DEV: false`, so the
54
+ * dev-login gate keeps failing closed exactly as before. Non-string values other
55
+ * than `DEV` are dropped — the studio env surface is a string record.
56
+ *
57
+ * Passing `null`/`undefined` is a no-op (a consumer entry that predates the option
58
+ * keeps today's engine-visible fallback behavior, plus a loud runtime warning when
59
+ * that fallback is provably broken — see {@link maybeWarnEngineEnvUnreachable}).
60
+ */
61
+ export declare function provideStudioEnv(env: Record<string, unknown> | null | undefined): void;
62
+ /** Test seam: clear the mount-provided env. Never used by app code. */
63
+ export declare function resetProvidedStudioEnvForTests(): void;
64
+ /**
65
+ * Resolve the configured studio env. Precedence: the test override, else the
66
+ * mount-provided env (over `process.env`), else the engine-visible Vite env
67
+ * (over `process.env`).
68
+ */
39
69
  export declare function studioEnv(): EnvRecord;
70
+ /** The env keys whose loss/staleness breaks boot or sign-in. */
71
+ export declare const STUDIO_ENV_KEYS: readonly string[];
72
+ export type StudioEnvAnomaly =
73
+ /** No mount-provided env AND the engine module has no `import.meta.env` at all. */
74
+ {
75
+ kind: 'engine-env-unreachable';
76
+ }
77
+ /** The engine-visible env disagrees with the mount-provided env (stale optimizer cache). */
78
+ | {
79
+ kind: 'engine-env-stale';
80
+ keys: string[];
81
+ };
82
+ /**
83
+ * Pure anomaly detection over the two env views (unit-testable core of the boot
84
+ * warnings). `engineVisible === null` means the engine module has no
85
+ * `import.meta.env` — expected under dep optimization WITH a provided env (silent),
86
+ * fatal without one (the values can never arrive). A stale mismatch fires only for
87
+ * keys the ENGINE sees with a different value than the mount provided (the Vite 7
88
+ * freeze); keys the engine simply lacks (Vite 8) are not anomalies — the provided
89
+ * env is authoritative and in effect.
90
+ */
91
+ export declare function detectStudioEnvAnomaly(provided: EnvRecord | null, engineVisible: EnvRecord | null, inBrowser: boolean): StudioEnvAnomaly | null;
40
92
  /**
41
93
  * Pure mode decision — testable with a plain env record. The app calls it with
42
94
  * {@link studioEnv}; `CONVEX_URL` (Node-side alias) is honored for SSR/tools.
@@ -15,10 +15,22 @@
15
15
  export const MODE_ENV_VAR = 'VITE_ELYTRA_MODE';
16
16
  export const PLAYGROUND_MODE = 'playground';
17
17
  export const CONVEX_URL_ENV_VAR = 'VITE_CONVEX_URL';
18
- function viteEnv() {
18
+ /**
19
+ * The ENGINE-visible `import.meta.env`, or `null` when the module has none at all.
20
+ * `null` is a meaningful signal (EC-357): every module Vite transforms live gets an
21
+ * `import.meta.env`, so its complete absence in a browser means this engine module
22
+ * was served from Vite's dep-optimizer cache — where Vite ≥8 no longer inlines
23
+ * custom `VITE_*` values (and Vite 7 froze them at optimize time). Engine code must
24
+ * therefore never trust this view when a mount-provided env exists — see
25
+ * {@link provideStudioEnv}.
26
+ */
27
+ function engineVisibleEnv() {
19
28
  if (typeof import.meta === 'undefined')
20
- return {};
21
- return import.meta.env ?? {};
29
+ return null;
30
+ return import.meta.env ?? null;
31
+ }
32
+ function viteEnv() {
33
+ return engineVisibleEnv() ?? {};
22
34
  }
23
35
  function nodeEnv() {
24
36
  return typeof process !== 'undefined' ? (process.env ?? {}) : {};
@@ -37,9 +49,119 @@ let envOverride = null;
37
49
  export function setStudioEnvOverrideForTests(env) {
38
50
  envOverride = env;
39
51
  }
40
- /** Resolve the configured studio env (Vite client env wins over process.env). */
52
+ let providedEnv = null;
53
+ /**
54
+ * Install the CONSUMER-captured env as the studio's env source (EC-357). The root
55
+ * cause this fixes: the engine used to read `import.meta.env.VITE_*` inside
56
+ * `@elytracms` package code, which Vite dep-optimizes in a real consumer — on
57
+ * Vite ≥8 those values are simply absent at runtime in the optimized chunk (the
58
+ * backend URL / mode / dev-login silently vanish and every sign-in fails), and on
59
+ * Vite 7 they froze at optimize time (stale until `node_modules/.vite` is wiped).
60
+ *
61
+ * The consumer's own entry module is code Vite ALWAYS transforms live, so the env
62
+ * is captured there and handed down: `createStudio(config, studioConfig, { env:
63
+ * import.meta.env })`. When a provided env exists it REPLACES the engine-visible
64
+ * Vite layer entirely (never merged with it — a stale optimizer snapshot must not
65
+ * shadow a removed key); `process.env` stays underneath as the SSR/tooling base.
66
+ *
67
+ * `DEV` is captured alongside (it gates dev auto-login): in a production consumer
68
+ * build Vite statically inlines `import.meta.env` with `DEV: false`, so the
69
+ * dev-login gate keeps failing closed exactly as before. Non-string values other
70
+ * than `DEV` are dropped — the studio env surface is a string record.
71
+ *
72
+ * Passing `null`/`undefined` is a no-op (a consumer entry that predates the option
73
+ * keeps today's engine-visible fallback behavior, plus a loud runtime warning when
74
+ * that fallback is provably broken — see {@link maybeWarnEngineEnvUnreachable}).
75
+ */
76
+ export function provideStudioEnv(env) {
77
+ if (env === null || env === undefined)
78
+ return;
79
+ const record = {};
80
+ for (const [key, value] of Object.entries(env)) {
81
+ if (typeof value === 'string')
82
+ record[key] = value;
83
+ }
84
+ providedEnv = {
85
+ record,
86
+ dev: env.DEV === true ? true : env.DEV === false ? false : undefined,
87
+ };
88
+ maybeWarnEngineEnvStale();
89
+ }
90
+ /** Test seam: clear the mount-provided env. Never used by app code. */
91
+ export function resetProvidedStudioEnvForTests() {
92
+ providedEnv = null;
93
+ warnedStale = false;
94
+ warnedUnreachable = false;
95
+ }
96
+ /**
97
+ * Resolve the configured studio env. Precedence: the test override, else the
98
+ * mount-provided env (over `process.env`), else the engine-visible Vite env
99
+ * (over `process.env`).
100
+ */
41
101
  export function studioEnv() {
42
- return envOverride ?? { ...nodeEnv(), ...viteEnv() };
102
+ if (envOverride)
103
+ return envOverride;
104
+ if (providedEnv)
105
+ return { ...nodeEnv(), ...providedEnv.record };
106
+ maybeWarnEngineEnvUnreachable();
107
+ return { ...nodeEnv(), ...viteEnv() };
108
+ }
109
+ // --- Boot-time env self-check (EC-357, Oli: "easier to catch") ---------------
110
+ /** The env keys whose loss/staleness breaks boot or sign-in. */
111
+ export const STUDIO_ENV_KEYS = [
112
+ 'VITE_ELYTRA_MODE',
113
+ 'VITE_CONVEX_URL',
114
+ 'VITE_ELYTRA_DEV_LOGIN',
115
+ ];
116
+ /**
117
+ * Pure anomaly detection over the two env views (unit-testable core of the boot
118
+ * warnings). `engineVisible === null` means the engine module has no
119
+ * `import.meta.env` — expected under dep optimization WITH a provided env (silent),
120
+ * fatal without one (the values can never arrive). A stale mismatch fires only for
121
+ * keys the ENGINE sees with a different value than the mount provided (the Vite 7
122
+ * freeze); keys the engine simply lacks (Vite 8) are not anomalies — the provided
123
+ * env is authoritative and in effect.
124
+ */
125
+ export function detectStudioEnvAnomaly(provided, engineVisible, inBrowser) {
126
+ if (!inBrowser)
127
+ return null;
128
+ if (!provided)
129
+ return engineVisible === null ? { kind: 'engine-env-unreachable' } : null;
130
+ if (engineVisible === null)
131
+ return null;
132
+ const stale = STUDIO_ENV_KEYS.filter((key) => {
133
+ const engineValue = readEnv(engineVisible, key);
134
+ return engineValue !== undefined && engineValue !== readEnv(provided, key);
135
+ });
136
+ return stale.length > 0 ? { kind: 'engine-env-stale', keys: stale } : null;
137
+ }
138
+ function inBrowser() {
139
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
140
+ }
141
+ let warnedUnreachable = false;
142
+ function maybeWarnEngineEnvUnreachable() {
143
+ if (warnedUnreachable)
144
+ return;
145
+ const anomaly = detectStudioEnvAnomaly(null, engineVisibleEnv(), inBrowser());
146
+ if (anomaly?.kind !== 'engine-env-unreachable')
147
+ return;
148
+ warnedUnreachable = true;
149
+ console.warn('[elytra] The engine cannot see import.meta.env — this @elytracms module was dep-optimized by Vite, ' +
150
+ 'so VITE_CONVEX_URL / VITE_ELYTRA_MODE / VITE_ELYTRA_DEV_LOGIN cannot reach it at runtime. ' +
151
+ 'Fix: pass your studio entry’s env to the mount — createStudio(config, studioConfig, { env: import.meta.env }) — ' +
152
+ "or add optimizeDeps.exclude: ['@elytracms/studio', '@elytracms/core'] to the studio vite.config.ts.");
153
+ }
154
+ let warnedStale = false;
155
+ function maybeWarnEngineEnvStale() {
156
+ if (warnedStale || !providedEnv)
157
+ return;
158
+ const anomaly = detectStudioEnvAnomaly(providedEnv.record, engineVisibleEnv(), inBrowser());
159
+ if (anomaly?.kind !== 'engine-env-stale')
160
+ return;
161
+ warnedStale = true;
162
+ console.warn(`[elytra] import.meta.env inside the engine disagrees with the studio mount's env for: ${anomaly.keys.join(', ')}. ` +
163
+ "The engine copy is a stale Vite dep-optimizer snapshot; the mount-provided values are in effect. " +
164
+ 'Delete node_modules/.vite to drop the stale copy.');
43
165
  }
44
166
  /**
45
167
  * Pure mode decision — testable with a plain env record. The app calls it with
@@ -62,7 +184,11 @@ export function resolveBackendConfig(env = studioEnv()) {
62
184
  return {
63
185
  kind: 'misconfigured',
64
186
  reason: 'missing-convex-url',
65
- detail: `No persistence backend is configured. Set ${CONVEX_URL_ENV_VAR} to your Convex deployment URL (see docs/backend-setup.md), or opt into the ephemeral local playground with ${MODE_ENV_VAR}=${PLAYGROUND_MODE}.`,
187
+ // EC-357: name BOTH causes — the var may be genuinely unset, or set but never
188
+ // reaching the client at runtime (Vite dep-optimized engine code; see
189
+ // `provideStudioEnv`). The paired fix text (deployment-ready `FIX.backendUrl`)
190
+ // carries the full remedy for the second cause.
191
+ 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}.`,
66
192
  };
67
193
  }
68
194
  /** True when the studio runs in the explicit, labeled playground mode. */
@@ -75,12 +201,17 @@ export function isPlaygroundMode() {
75
201
  /** `email:password` for unattended dev sign-in (gitignored `.env.local`). */
76
202
  export const DEV_LOGIN_ENV_VAR = 'VITE_ELYTRA_DEV_LOGIN';
77
203
  /**
78
- * Is this a Vite *dev* build? `import.meta.env.DEV` is statically `true` under
79
- * `vite dev` and statically `false` in any production build — so gating dev
80
- * auto-login on it means the whole feature **dead-code-eliminates** out of a
81
- * production bundle, even if `VITE_ELYTRA_DEV_LOGIN` were somehow present.
204
+ * Is this a Vite *dev* build? The mount-provided env's captured `DEV` flag wins
205
+ * (EC-357 — inside dep-optimized engine code the engine-visible flag is absent on
206
+ * Vite ≥8 and frozen on Vite 7); the consumer's `import.meta.env.DEV` is statically
207
+ * `true` under `vite dev` and statically `false` in any production build, so the
208
+ * dev auto-login gate still fails closed in production. Without a provided env the
209
+ * engine-visible static flag applies as before (where it also dead-code-eliminates
210
+ * the feature out of a directly-bundled production build).
82
211
  */
83
212
  function isViteDevBuild() {
213
+ if (providedEnv?.dev !== undefined)
214
+ return providedEnv.dev;
84
215
  if (typeof import.meta === 'undefined')
85
216
  return false;
86
217
  return import.meta.env?.DEV === true;
@@ -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\nfunction viteEnv(): EnvRecord {\n if (typeof import.meta === 'undefined') return {}\n return (import.meta as { env?: EnvRecord }).env ?? {}\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/** Resolve the configured studio env (Vite client env wins over process.env). */\nexport function studioEnv(): EnvRecord {\n return envOverride ?? { ...nodeEnv(), ...viteEnv() }\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 detail: `No persistence backend is configured. Set ${CONVEX_URL_ENV_VAR} 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? `import.meta.env.DEV` is statically `true` under\n * `vite dev` and statically `false` in any production build — so gating dev\n * auto-login on it means the whole feature **dead-code-eliminates** out of a\n * production bundle, even if `VITE_ELYTRA_DEV_LOGIN` were somehow present.\n */\nfunction isViteDevBuild(): boolean {\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 * 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"]}
@@ -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, isDevAutoLoginActive, isPlaygroundMode, resolveBackendConfig, resolveDevAutoLogin, } from './backend-mode.js';
30
- export type { DevAutoLoginCredentials, EnvRecord, StudioBackendConfig, StudioBackendMode, } from './backend-mode.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';
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';
@@ -60,7 +60,7 @@ export function persistenceMode() {
60
60
  return config.kind;
61
61
  }
62
62
  export { ConvexPersistenceAdapter } from './convex-adapter.js';
63
- export { CONVEX_URL_ENV_VAR, DEV_LOGIN_ENV_VAR, MODE_ENV_VAR, PLAYGROUND_MODE, isDevAutoLoginActive, isPlaygroundMode, resolveBackendConfig, resolveDevAutoLogin, } from './backend-mode.js';
63
+ 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';
64
64
  // EC-258: the deployment-readiness facts mapper (the boot DeploymentSetupGate + a
65
65
  // future `elytra doctor` CLI compose the same `checkDeploymentReady` contract).
66
66
  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,OAAO,8BAA8B,CAAC,0BAA0B,CAAC,eAAe,CAAC,CAAC,CAAA;IACpF,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,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,gBAAgB,CAAA;AAOvB,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 return createSeededPersistenceAdapter(resolveDeploymentProjectId(SEED_PROJECT_ID))\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 isDevAutoLoginActive,\n isPlaygroundMode,\n resolveBackendConfig,\n resolveDevAutoLogin,\n} from './backend-mode'\nexport type {\n DevAutoLoginCredentials,\n EnvRecord,\n StudioBackendConfig,\n StudioBackendMode,\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,OAAO,8BAA8B,CAAC,0BAA0B,CAAC,eAAe,CAAC,CAAC,CAAA;IACpF,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 return createSeededPersistenceAdapter(resolveDeploymentProjectId(SEED_PROJECT_ID))\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,4 +1,5 @@
1
1
  import { attachRevalidationWebhooks } from './dispatcher.js';
2
+ import { studioEnv } from '../persistence/backend-mode.js';
2
3
  import { resolveDeploymentLocales } from '../workspace/index.js';
3
4
  /**
4
5
  * Environment wiring for the EC-146 revalidation webhooks. The dispatcher is
@@ -20,12 +21,12 @@ import { resolveDeploymentLocales } from '../workspace/index.js';
20
21
  * The host must allow the studio origin via
21
22
  * `createRevalidateRoute({ allowOrigin })` for the browser preflight to pass.
22
23
  */
24
+ // EC-357: env reads go through the ONE studio env surface (`studioEnv()` — the
25
+ // mount-provided env first, then the engine-visible Vite env over `process.env`).
26
+ // A direct `import.meta.env` read here would be dep-optimized away in a real
27
+ // consumer (pinned by `env-read-discipline.test.ts`).
23
28
  function readEnv(name) {
24
- const viteValue = typeof import.meta !== 'undefined'
25
- ? import.meta.env?.[name]
26
- : undefined;
27
- const nodeValue = typeof process !== 'undefined' ? process.env?.[name] : undefined;
28
- return viteValue ?? nodeValue;
29
+ return studioEnv()[name];
29
30
  }
30
31
  /**
31
32
  * Attach the dispatcher from environment configuration. Returns `null` when
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/lib/webhooks/config.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AAEzD,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAA;AAEvD;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,SAAS,OAAO,CAAC,IAAY;IAC3B,MAAM,SAAS,GACb,OAAO,MAAM,CAAC,IAAI,KAAK,WAAW;QAChC,CAAC,CAAE,MAAM,CAAC,IAAqD,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC;QAC3E,CAAC,CAAC,SAAS,CAAA;IACf,MAAM,SAAS,GAAG,OAAO,OAAO,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAClF,OAAO,SAAS,IAAI,SAAS,CAAA;AAC/B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oCAAoC,CAClD,MAAyB;IAEzB,MAAM,GAAG,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAA;IAC/C,MAAM,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAA;IAChD,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAEhC,OAAO,0BAA0B,CAAC,MAAM,EAAE;QACxC,SAAS,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;QAC5B,OAAO,EAAE,mBAAmB,EAAE;QAC9B,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;YAC1B,8DAA8D;YAC9D,OAAO,CAAC,IAAI,CAAC,kDAAkD,OAAO,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC,CAAA;QACvF,CAAC;KACF,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,aAAa,GAAG,wBAAwB,EAAE,CAAA;IAChD,MAAM,UAAU,GAAG,CAAC,OAAO,CAAC,yBAAyB,CAAC,IAAI,EAAE,CAAC;SAC1D,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;SAC9B,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IACxC,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,aAAa,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,CAAA;IAC7D,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;AAC1C,CAAC","sourcesContent":["import type { OperationEventBus } from '@elytracms/core/operations'\nimport { attachRevalidationWebhooks } from './dispatcher'\nimport type { WebhookDispatcherAttachment } from './dispatcher'\nimport { resolveDeploymentLocales } from '../workspace'\n\n/**\n * Environment wiring for the EC-146 revalidation webhooks. The dispatcher is\n * attached only when a host endpoint is configured, so dev sessions and tests\n * (no env) run with zero webhook overhead.\n *\n * Configuration (Vite client env; SSR falls back to `process.env`):\n *\n * - `VITE_REVALIDATE_ENDPOINT` — the host's revalidate route URL\n * (e.g. `https://example-site.vercel.app/api/revalidate`)\n * - `VITE_REVALIDATE_SECRET` — shared HMAC secret (must equal the host's\n * `REVALIDATE_SECRET`). Browser-exposed by design in the v1 dispatch path —\n * see the CORS/secret notes in `dispatcher.ts`.\n * - `VITE_REVALIDATE_LOCALES` — optional comma-separated locale list that can ADD\n * locales to the revalidation tag set. It can never NARROW it: the project's\n * `elytra.config.ts` locales are always covered (EC-192), so a non-default\n * locale like `/de` can never be the one left stale after a publish.\n *\n * The host must allow the studio origin via\n * `createRevalidateRoute({ allowOrigin })` for the browser preflight to pass.\n */\n\nfunction readEnv(name: string): string | undefined {\n const viteValue =\n typeof import.meta !== 'undefined'\n ? (import.meta as { env?: Record<string, string | undefined> }).env?.[name]\n : undefined\n const nodeValue = typeof process !== 'undefined' ? process.env?.[name] : undefined\n return viteValue ?? nodeValue\n}\n\n/**\n * Attach the dispatcher from environment configuration. Returns `null` when\n * no endpoint/secret is configured (the common local-dev case).\n */\nexport function attachConfiguredRevalidationWebhooks(\n events: OperationEventBus,\n): WebhookDispatcherAttachment | null {\n const url = readEnv('VITE_REVALIDATE_ENDPOINT')\n const secret = readEnv('VITE_REVALIDATE_SECRET')\n if (!url || !secret) return null\n\n return attachRevalidationWebhooks(events, {\n endpoints: [{ url, secret }],\n locales: revalidationLocales(),\n onError: (error, context) => {\n // Visible, never fatal: a host outage must not break editing.\n console.warn(`[elytra] revalidation webhook delivery failed (${context.url})`, error)\n },\n })\n}\n\n/**\n * The locales the dispatcher emits per-collection tags for. The project's\n * configured locales (the same set the host tags by) are always covered so no\n * locale can be silently dropped (EC-192 — the `/de` freshness fix); the\n * `VITE_REVALIDATE_LOCALES` env may only ADD to that set. Falls back to `en`\n * only when neither config nor env supplies a locale (env-only wiring).\n */\nexport function revalidationLocales(): string[] {\n const configLocales = resolveDeploymentLocales()\n const envLocales = (readEnv('VITE_REVALIDATE_LOCALES') ?? '')\n .split(',')\n .map((locale) => locale.trim())\n .filter((locale) => locale.length > 0)\n const union = [...new Set([...configLocales, ...envLocales])]\n return union.length > 0 ? union : ['en']\n}\n"]}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/lib/webhooks/config.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AAEzD,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAA;AACvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAA;AAEvD;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,+EAA+E;AAC/E,kFAAkF;AAClF,6EAA6E;AAC7E,sDAAsD;AACtD,SAAS,OAAO,CAAC,IAAY;IAC3B,OAAO,SAAS,EAAE,CAAC,IAAI,CAAC,CAAA;AAC1B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oCAAoC,CAClD,MAAyB;IAEzB,MAAM,GAAG,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAA;IAC/C,MAAM,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAA;IAChD,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAEhC,OAAO,0BAA0B,CAAC,MAAM,EAAE;QACxC,SAAS,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;QAC5B,OAAO,EAAE,mBAAmB,EAAE;QAC9B,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;YAC1B,8DAA8D;YAC9D,OAAO,CAAC,IAAI,CAAC,kDAAkD,OAAO,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC,CAAA;QACvF,CAAC;KACF,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,aAAa,GAAG,wBAAwB,EAAE,CAAA;IAChD,MAAM,UAAU,GAAG,CAAC,OAAO,CAAC,yBAAyB,CAAC,IAAI,EAAE,CAAC;SAC1D,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;SAC9B,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IACxC,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,aAAa,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,CAAA;IAC7D,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;AAC1C,CAAC","sourcesContent":["import type { OperationEventBus } from '@elytracms/core/operations'\nimport { attachRevalidationWebhooks } from './dispatcher'\nimport type { WebhookDispatcherAttachment } from './dispatcher'\nimport { studioEnv } from '../persistence/backend-mode'\nimport { resolveDeploymentLocales } from '../workspace'\n\n/**\n * Environment wiring for the EC-146 revalidation webhooks. The dispatcher is\n * attached only when a host endpoint is configured, so dev sessions and tests\n * (no env) run with zero webhook overhead.\n *\n * Configuration (Vite client env; SSR falls back to `process.env`):\n *\n * - `VITE_REVALIDATE_ENDPOINT` — the host's revalidate route URL\n * (e.g. `https://example-site.vercel.app/api/revalidate`)\n * - `VITE_REVALIDATE_SECRET` — shared HMAC secret (must equal the host's\n * `REVALIDATE_SECRET`). Browser-exposed by design in the v1 dispatch path —\n * see the CORS/secret notes in `dispatcher.ts`.\n * - `VITE_REVALIDATE_LOCALES` — optional comma-separated locale list that can ADD\n * locales to the revalidation tag set. It can never NARROW it: the project's\n * `elytra.config.ts` locales are always covered (EC-192), so a non-default\n * locale like `/de` can never be the one left stale after a publish.\n *\n * The host must allow the studio origin via\n * `createRevalidateRoute({ allowOrigin })` for the browser preflight to pass.\n */\n\n// EC-357: env reads go through the ONE studio env surface (`studioEnv()` — the\n// mount-provided env first, then the engine-visible Vite env over `process.env`).\n// A direct `import.meta.env` read here would be dep-optimized away in a real\n// consumer (pinned by `env-read-discipline.test.ts`).\nfunction readEnv(name: string): string | undefined {\n return studioEnv()[name]\n}\n\n/**\n * Attach the dispatcher from environment configuration. Returns `null` when\n * no endpoint/secret is configured (the common local-dev case).\n */\nexport function attachConfiguredRevalidationWebhooks(\n events: OperationEventBus,\n): WebhookDispatcherAttachment | null {\n const url = readEnv('VITE_REVALIDATE_ENDPOINT')\n const secret = readEnv('VITE_REVALIDATE_SECRET')\n if (!url || !secret) return null\n\n return attachRevalidationWebhooks(events, {\n endpoints: [{ url, secret }],\n locales: revalidationLocales(),\n onError: (error, context) => {\n // Visible, never fatal: a host outage must not break editing.\n console.warn(`[elytra] revalidation webhook delivery failed (${context.url})`, error)\n },\n })\n}\n\n/**\n * The locales the dispatcher emits per-collection tags for. The project's\n * configured locales (the same set the host tags by) are always covered so no\n * locale can be silently dropped (EC-192 — the `/de` freshness fix); the\n * `VITE_REVALIDATE_LOCALES` env may only ADD to that set. Falls back to `en`\n * only when neither config nor env supplies a locale (env-only wiring).\n */\nexport function revalidationLocales(): string[] {\n const configLocales = resolveDeploymentLocales()\n const envLocales = (readEnv('VITE_REVALIDATE_LOCALES') ?? '')\n .split(',')\n .map((locale) => locale.trim())\n .filter((locale) => locale.length > 0)\n const union = [...new Set([...configLocales, ...envLocales])]\n return union.length > 0 ? union : ['en']\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { BreakpointConfig, ElytraConfigInput } from '@elytracms/core/studio-core';
2
- import type { CollectionDef, RouteRecord } from '@elytracms/core/cms-core';
2
+ import type { CollectionDef, LocaleConfig, RouteRecord } from '@elytracms/core/cms-core';
3
3
  import type { SidebarNode } from '../../components/app-views/module.js';
4
4
  import type { CustomFieldRenderer, StudioConfig } from '../../components/app-views/studio-config.js';
5
5
  import type { WorkspaceState } from './workspace-source.js';
@@ -22,8 +22,19 @@ export type { SwitcherEntry, SwitcherSourceInput } from './switcher-source.js';
22
22
  * Keeping them separate is the customization boundary principle in the mount
23
23
  * signature. Seeds the workspace state + the studio-config holder every later
24
24
  * query (`getActiveProjectSlug`, `resolveDeployment*`) reads.
25
+ *
26
+ * EC-357 — `options.env`: the consumer's `import.meta.env`, captured HERE because
27
+ * the entry module is code Vite always transforms live. The engine's own
28
+ * `import.meta.env` reads are unreliable inside dep-optimized package code (Vite ≥8
29
+ * omits custom `VITE_*` values there; Vite 7 froze them at optimize time), which
30
+ * silently broke backend URL / mode / dev-login resolution in real consumers.
31
+ * Always pass it: `createStudio(config, studioConfig, { env: import.meta.env })`.
25
32
  */
26
- export declare function createStudio(config: ElytraConfigInput, studio?: StudioConfig): void;
33
+ export interface StudioMountOptions {
34
+ /** The consumer entry's `import.meta.env` (see `provideStudioEnv` for the contract). */
35
+ env?: Record<string, unknown>;
36
+ }
37
+ export declare function createStudio(config: ElytraConfigInput, studio?: StudioConfig, options?: StudioMountOptions): void;
27
38
  /** Test seam: set/clear the studio customization config. Never used by app code. */
28
39
  export declare function setStudioConfigForTests(next: StudioConfig | null): void;
29
40
  /** Set the active project slug; returns whether the resolved deployment changed. */
@@ -132,6 +143,17 @@ export declare function resolveDeploymentRoutes(): RouteRecord[];
132
143
  * (e.g. `/de`, `/fr`), never just the default. `[]` when wired by env alone.
133
144
  */
134
145
  export declare function resolveDeploymentLocales(): string[];
146
+ /**
147
+ * The active config project's locales as a `LocaleConfig` (EC-357 finding #3).
148
+ * Locales are structure and structure is code: when `elytra.config.ts` declares
149
+ * them, they drive the studio's ambient locale switcher in EVERY mode — the CMS
150
+ * surface must not fall back to the sample `en/de` fixture (playground, or a
151
+ * connected first run with nothing persisted) when the config says otherwise.
152
+ * `undefined` when no config project is active or it declares no locales (env-only
153
+ * wiring keeps the existing kit/sample fallbacks). A missing/invalid explicit
154
+ * default falls back to the first declared locale.
155
+ */
156
+ export declare function resolveDeploymentLocaleConfig(): LocaleConfig | undefined;
135
157
  /**
136
158
  * The active project's components reference (EC-194, AD-11): where the consuming
137
159
  * repo's OWN real components live (`{ package }` or `{ dir }`). When set, the
@@ -1,26 +1,12 @@
1
1
  import { frontendUrlForEnvironment } from '@elytracms/core/studio-core';
2
+ import { provideStudioEnv } from '../persistence/backend-mode.js';
2
3
  import { resolveWorkspaceState } from './workspace-source.js';
3
4
  export { resolveWorkspaceState, WORKSPACE_ENV_VAR } from './workspace-source.js';
4
5
  export { persistedProjects, selectSwitcherProjects, unprovisionedProjects } from './switcher-source.js';
5
6
  let cached = null;
6
7
  let studioConfig = null;
7
- /**
8
- * Mount the studio against a project config (EC-223 mount contract) and an
9
- * optional studio-only customization config (EC-236). The consumer's entry point
10
- * does `import config from './elytra.config'` + `import studio from
11
- * './studio.config'` and calls this — an explicit init, NOT an `import.meta.glob`
12
- * autoload that reaches into the consumer's filesystem (Oli, 2026-06-19: "there's
13
- * always an entry point that can trigger a function call").
14
- *
15
- * Two configs, deliberately: `config` (`elytra.config.ts`) is the project
16
- * definition, shared with the `frontend/` delivery bundle and projected to data;
17
- * `studio` (`studio.config.tsx` via `defineStudio`) is live builder UI code — the
18
- * section rail, custom field renderers, branding — that must NEVER reach delivery.
19
- * Keeping them separate is the customization boundary principle in the mount
20
- * signature. Seeds the workspace state + the studio-config holder every later
21
- * query (`getActiveProjectSlug`, `resolveDeployment*`) reads.
22
- */
23
- export function createStudio(config, studio) {
8
+ export function createStudio(config, studio, options) {
9
+ provideStudioEnv(options?.env);
24
10
  cached = resolveWorkspaceState({
25
11
  fileModule: { default: config },
26
12
  fileName: 'elytra.config.ts',
@@ -233,6 +219,27 @@ export function resolveDeploymentRoutes() {
233
219
  export function resolveDeploymentLocales() {
234
220
  return activeConfigProject()?.locales ?? [];
235
221
  }
222
+ /**
223
+ * The active config project's locales as a `LocaleConfig` (EC-357 finding #3).
224
+ * Locales are structure and structure is code: when `elytra.config.ts` declares
225
+ * them, they drive the studio's ambient locale switcher in EVERY mode — the CMS
226
+ * surface must not fall back to the sample `en/de` fixture (playground, or a
227
+ * connected first run with nothing persisted) when the config says otherwise.
228
+ * `undefined` when no config project is active or it declares no locales (env-only
229
+ * wiring keeps the existing kit/sample fallbacks). A missing/invalid explicit
230
+ * default falls back to the first declared locale.
231
+ */
232
+ export function resolveDeploymentLocaleConfig() {
233
+ const project = activeConfigProject();
234
+ if (!project || project.locales.length === 0)
235
+ return undefined;
236
+ const fallback = project.locales[0];
237
+ const explicit = project.defaultLocale;
238
+ return {
239
+ default: explicit && project.locales.includes(explicit) ? explicit : fallback,
240
+ locales: [...project.locales],
241
+ };
242
+ }
236
243
  /**
237
244
  * The active project's components reference (EC-194, AD-11): where the consuming
238
245
  * repo's OWN real components live (`{ package }` or `{ dir }`). When set, the