@elytracms/studio 0.0.47 → 0.0.49

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 (82) hide show
  1. package/convex/auth.ts +61 -12
  2. package/convex/authority.ts +40 -2
  3. package/convex/functions.ts +9 -0
  4. package/convex/invites.ts +75 -0
  5. package/convex/members.ts +44 -18
  6. package/convex/schema.ts +37 -0
  7. package/convex/users.ts +89 -2
  8. package/dist/components/account/account-settings.d.ts +13 -0
  9. package/dist/components/account/account-settings.js +54 -0
  10. package/dist/components/account/account-settings.js.map +1 -0
  11. package/dist/components/account/credential-panel.d.ts +14 -0
  12. package/dist/components/account/credential-panel.js +52 -0
  13. package/dist/components/account/credential-panel.js.map +1 -0
  14. package/dist/components/account/self-account-schema.d.ts +19 -0
  15. package/dist/components/account/self-account-schema.js +52 -0
  16. package/dist/components/account/self-account-schema.js.map +1 -0
  17. package/dist/components/account/self-account-view-source.d.ts +35 -0
  18. package/dist/components/account/self-account-view-source.js +110 -0
  19. package/dist/components/account/self-account-view-source.js.map +1 -0
  20. package/dist/components/auth-context.d.ts +5 -1
  21. package/dist/components/auth-context.js +9 -0
  22. package/dist/components/auth-context.js.map +1 -1
  23. package/dist/components/cms/cms-store.d.ts +7 -2
  24. package/dist/components/cms/cms-store.js +35 -4
  25. package/dist/components/cms/cms-store.js.map +1 -1
  26. package/dist/components/cms/view-bar.js +4 -2
  27. package/dist/components/cms/view-bar.js.map +1 -1
  28. package/dist/components/current-user-menu.js +11 -2
  29. package/dist/components/current-user-menu.js.map +1 -1
  30. package/dist/components/deployment-setup-gate.d.ts +31 -0
  31. package/dist/components/deployment-setup-gate.js +92 -0
  32. package/dist/components/deployment-setup-gate.js.map +1 -0
  33. package/dist/components/project-session.d.ts +1 -1
  34. package/dist/components/project-session.js.map +1 -1
  35. package/dist/components/use-hydration-phase.js +1 -1
  36. package/dist/components/use-hydration-phase.js.map +1 -1
  37. package/dist/components/users/invite-user-bar.d.ts +3 -0
  38. package/dist/components/users/invite-user-bar.js +91 -0
  39. package/dist/components/users/invite-user-bar.js.map +1 -0
  40. package/dist/components/users/users-module.js +13 -2
  41. package/dist/components/users/users-module.js.map +1 -1
  42. package/dist/lib/auth/auth-adapter.d.ts +50 -2
  43. package/dist/lib/auth/auth-adapter.js.map +1 -1
  44. package/dist/lib/auth/convex-auth-adapter.d.ts +28 -1
  45. package/dist/lib/auth/convex-auth-adapter.js +113 -1
  46. package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
  47. package/dist/lib/auth/dev-auto-login-adapter.d.ts +3 -1
  48. package/dist/lib/auth/dev-auto-login-adapter.js +6 -0
  49. package/dist/lib/auth/dev-auto-login-adapter.js.map +1 -1
  50. package/dist/lib/auth/index.d.ts +2 -2
  51. package/dist/lib/auth/index.js +1 -1
  52. package/dist/lib/auth/index.js.map +1 -1
  53. package/dist/lib/auth/local-auth-adapter.d.ts +3 -1
  54. package/dist/lib/auth/local-auth-adapter.js +16 -0
  55. package/dist/lib/auth/local-auth-adapter.js.map +1 -1
  56. package/dist/lib/cms-views/view-model.d.ts +33 -4
  57. package/dist/lib/cms-views/view-model.js +66 -10
  58. package/dist/lib/cms-views/view-model.js.map +1 -1
  59. package/dist/lib/collection-view/collection-view-source.d.ts +3 -2
  60. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  61. package/dist/lib/i18n/messages.en.js +45 -0
  62. package/dist/lib/i18n/messages.en.js.map +1 -1
  63. package/dist/lib/operations/studio-operations.js +1 -1
  64. package/dist/lib/operations/studio-operations.js.map +1 -1
  65. package/dist/lib/persistence/backend-mode.d.ts +1 -1
  66. package/dist/lib/persistence/backend-mode.js +1 -1
  67. package/dist/lib/persistence/backend-mode.js.map +1 -1
  68. package/dist/lib/persistence/convex-adapter.d.ts +2 -1
  69. package/dist/lib/persistence/convex-adapter.js +69 -1
  70. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  71. package/dist/lib/persistence/deployment-readiness.d.ts +36 -0
  72. package/dist/lib/persistence/deployment-readiness.js +26 -0
  73. package/dist/lib/persistence/deployment-readiness.js.map +1 -0
  74. package/dist/lib/persistence/index.d.ts +3 -1
  75. package/dist/lib/persistence/index.js +4 -1
  76. package/dist/lib/persistence/index.js.map +1 -1
  77. package/dist/lib/workspace/switcher-source.js +1 -1
  78. package/dist/lib/workspace/switcher-source.js.map +1 -1
  79. package/dist/lib/workspace/workspace-source.d.ts +1 -1
  80. package/dist/lib/workspace/workspace-source.js +1 -1
  81. package/dist/lib/workspace/workspace-source.js.map +1 -1
  82. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { actionRef, queryRef } from '../persistence/convex-client';
1
+ import { actionRef, mutationRef, queryRef } from '../persistence/convex-client';
2
2
  /**
3
3
  * Convex Auth implementation of the project-owned {@link AuthAdapter}
4
4
  * (EC-150). Decision record (also in `convex/auth.ts`): **Convex Auth** over
@@ -22,6 +22,19 @@ import { actionRef, queryRef } from '../persistence/convex-client';
22
22
  * adapter's shared interface.
23
23
  */
24
24
  const TOKEN_STORAGE_PREFIX = 'elytra.convex-auth.jwt';
25
+ /**
26
+ * Classify a thrown sign-in error as a DEPLOYMENT-setup failure rather than a
27
+ * credential problem (EC-257). Matches the Convex Auth signing-path throws:
28
+ * `requireEnv('JWT_PRIVATE_KEY'|'JWKS'|'CONVEX_SITE_URL')` (→ "Missing environment
29
+ * variable …") and `importPKCS8` on a malformed key. Deliberately narrow — a genuine
30
+ * wrong-password error carries none of these tokens — and checked BEFORE the credential
31
+ * regex so it always wins. Note: a PRODUCTION deployment redacts non-`ConvexError`
32
+ * messages to a generic "Server Error", which this can't match; there the honest
33
+ * fallback is the neutral "unavailable" branch (never "password too short").
34
+ */
35
+ export function isDeploymentMisconfigured(message) {
36
+ return /JWT_PRIVATE_KEY|JWKS|CONVEX_SITE_URL|Missing environment variable|PKCS8|private key/i.test(message);
37
+ }
25
38
  /**
26
39
  * Per-deployment token key (EC-179, vision AD-8). One project per backend, so a
27
40
  * session is scoped to its deployment: signing into Aurora's backend must not
@@ -108,6 +121,31 @@ export class ConvexAuthAdapter {
108
121
  catch (error) {
109
122
  // Convex Auth surfaces credential/registration problems as thrown errors.
110
123
  const message = error instanceof Error ? error.message : String(error);
124
+ // EC-257: a DEPLOYMENT-misconfiguration — not a credential problem — must be
125
+ // classified FIRST so it never falls through to the lying "password too short".
126
+ // Convex Auth signs the session JWT with the deployment's `JWT_PRIVATE_KEY`
127
+ // (+ `JWKS`/`CONVEX_SITE_URL`); when any is unset (`npx @convex-dev/auth`
128
+ // never run) the `requireEnv`/`importPKCS8` call throws *after* the password
129
+ // is validated, so EVERY sign-in fails regardless of the password. This is
130
+ // the exact footgun that cost hours on the Konditorei proof (2026-06-22).
131
+ if (isDeploymentMisconfigured(message)) {
132
+ return {
133
+ ok: false,
134
+ reason: 'deployment-misconfigured',
135
+ message: 'This deployment can’t sign you in: its auth signing key is not set. Run `npx @convex-dev/auth` against this deployment, then retry. (The deployment-setup check reports the same.)',
136
+ };
137
+ }
138
+ // EC-324: an uninvited sign-up is rejected server-side (`beforeSessionCreation`
139
+ // throws a `ConvexError('not-invited: …')`, whose data survives to the client
140
+ // even in production). Classified BEFORE the credential regex so it never
141
+ // degrades to "password too short".
142
+ if (/not-invited|invite-only|invite only/i.test(message)) {
143
+ return {
144
+ ok: false,
145
+ reason: 'not-invited',
146
+ message: 'This deployment is invite-only. Ask an admin to invite your email, then create your account.',
147
+ };
148
+ }
111
149
  if (flow === 'signUp' && /already|exists|InvalidAccountId/i.test(message)) {
112
150
  return {
113
151
  ok: false,
@@ -152,6 +190,80 @@ export class ConvexAuthAdapter {
152
190
  }
153
191
  this.clearToken();
154
192
  }
193
+ /**
194
+ * Self-service identity edit (EC-240 H-tail item 2). Patches the caller's OWN
195
+ * auth `users` row (name / avatar) via the self-scoped `users:updateProfile`
196
+ * mutation, then re-reads `users:me` so the returned session carries the new
197
+ * name. Auth-owned fields never flow through the members-rail `users.save`.
198
+ */
199
+ async updateProfile(input) {
200
+ const token = this.store.getItem(this.tokenKey);
201
+ if (!token) {
202
+ return { ok: false, reason: 'not-authenticated', message: 'You are not signed in.' };
203
+ }
204
+ this.client.setAuth(token);
205
+ try {
206
+ await this.client.mutation(mutationRef('users:updateProfile'), {
207
+ ...(input.name !== undefined ? { name: input.name } : {}),
208
+ ...(input.avatar !== undefined ? { avatar: input.avatar } : {}),
209
+ });
210
+ const me = (await this.client.query(queryRef('users:me'), {}));
211
+ if (!me) {
212
+ return { ok: false, reason: 'unavailable', message: 'Profile update is currently unavailable.' };
213
+ }
214
+ return { ok: true, session: { user: this.toUser(me) } };
215
+ }
216
+ catch {
217
+ return { ok: false, reason: 'unavailable', message: 'Profile update is currently unavailable.' };
218
+ }
219
+ }
220
+ /**
221
+ * Self-service password change (EC-240 H-tail item 3). Delegates entirely to
222
+ * the provider via the self-scoped `users:changePassword` ACTION (which calls
223
+ * Convex Auth's `retrieveAccount` to verify the current password, then
224
+ * `modifyAccountCredentials` to set the new one). **Seam only:** the raw
225
+ * password values are forwarded to the provider and NEVER logged or inspected
226
+ * here.
227
+ */
228
+ async changePassword(input) {
229
+ const token = this.store.getItem(this.tokenKey);
230
+ if (!token) {
231
+ return { ok: false, reason: 'not-authenticated', message: 'You are not signed in.' };
232
+ }
233
+ this.client.setAuth(token);
234
+ try {
235
+ await this.client.action(actionRef('users:changePassword'), {
236
+ currentPassword: input.currentPassword,
237
+ newPassword: input.newPassword,
238
+ });
239
+ return { ok: true };
240
+ }
241
+ catch (error) {
242
+ const message = error instanceof Error ? error.message : String(error);
243
+ if (isDeploymentMisconfigured(message)) {
244
+ return {
245
+ ok: false,
246
+ reason: 'unavailable',
247
+ message: 'This deployment can’t change your password: its auth signing key is not set. Run `npx @convex-dev/auth`.',
248
+ };
249
+ }
250
+ if (/InvalidSecret|InvalidAccountId|current password|incorrect/i.test(message)) {
251
+ return {
252
+ ok: false,
253
+ reason: 'invalid-credentials',
254
+ message: 'Your current password is incorrect.',
255
+ };
256
+ }
257
+ if (/at least|too short|8 characters|password requirement|weak/i.test(message)) {
258
+ return {
259
+ ok: false,
260
+ reason: 'weak-password',
261
+ message: 'Choose a stronger password (at least 8 characters).',
262
+ };
263
+ }
264
+ return { ok: false, reason: 'unavailable', message: 'Password change is currently unavailable.' };
265
+ }
266
+ }
155
267
  toUser(me) {
156
268
  return {
157
269
  id: me.id,
@@ -1 +1 @@
1
- {"version":3,"file":"convex-auth-adapter.js","sourceRoot":"","sources":["../../../src/lib/auth/convex-auth-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ConvexHttpClient } from 'convex/browser'\nimport { actionRef, queryRef } from '../persistence/convex-client'\nimport type { AuthAdapter, AuthSession, SignInCredentials, SignInResult } from './auth-adapter'\nimport type { SessionStore } from './local-auth-adapter'\n\n/**\n * Convex Auth implementation of the project-owned {@link AuthAdapter}\n * (EC-150). Decision record (also in `convex/auth.ts`): **Convex Auth** over\n * Clerk — self-contained on the Convex stack, sessions in our own tables, no\n * extra vendor; reachable only through this adapter so a swap stays local.\n *\n * Flow (server side lives in `convex/auth.ts` + `convex/users.ts`):\n * 1. `signIn` calls the `auth:signIn` action with the Password provider\n * (`flow: 'signIn'`); on success Convex returns a JWT + refresh token.\n * 2. The JWT is set on the *shared* Convex client (`client.setAuth`), so every\n * subsequent persistence query/mutation runs authenticated and the\n * server-side `requireRole` guards see the caller.\n * 3. `currentSession` restores the JWT from storage and asks `users:me`.\n * 4. `signOut` calls `auth:signOut` and clears the JWT.\n *\n * v1 scope note: the stored JWT is short-lived and this adapter does not yet\n * run the refresh-token loop (the official `@convex-dev/auth/react` client\n * does); an expired session simply resolves to \"signed out\" and the user signs\n * in again. Live-deployment hardening is EC-156. No live deployment exists in\n * this repo — this file is typecheck-verified and exercised against the local\n * adapter's shared interface.\n */\n\nconst TOKEN_STORAGE_PREFIX = 'elytra.convex-auth.jwt'\n\n/**\n * Per-deployment token key (EC-179, vision AD-8). One project per backend, so a\n * session is scoped to its deployment: signing into Aurora's backend must not\n * carry a JWT to Northwind's. Keying the stored token by the deployment URL\n * makes each backend's session independent — switching to a project on a\n * not-yet-authenticated backend reads no token and the guard asks for sign-in.\n * (A future shared external IdP — WorkOS/Clerk — would issue one identity valid\n * on every deployment, collapsing this back to a single session.)\n */\nexport function tokenStorageKey(deploymentUrl?: string): string {\n return deploymentUrl ? `${TOKEN_STORAGE_PREFIX}::${deploymentUrl}` : TOKEN_STORAGE_PREFIX\n}\n\n/**\n * Best-effort \"is there a session for this deployment?\" — a synchronous token\n * presence check used to label per-backend projects in the switcher/launcher\n * (EC-179). It does not validate the token (an expired one still redirects to\n * sign-in on entry); it just answers \"has the user signed into this backend?\".\n */\nexport function hasStoredConvexSession(\n deploymentUrl: string,\n store: SessionStore = defaultStore(),\n): boolean {\n return store.getItem(tokenStorageKey(deploymentUrl)) !== null\n}\n\ninterface SignInActionResult {\n tokens?: { token: string; refreshToken: string } | null\n}\n\ninterface MeResult {\n id: string\n email: string\n name?: string\n}\n\nfunction defaultStore(): SessionStore {\n if (typeof window !== 'undefined' && window.sessionStorage) return window.sessionStorage\n const memory = new Map<string, string>()\n return {\n getItem: (key) => memory.get(key) ?? null,\n setItem: (key, value) => void memory.set(key, value),\n removeItem: (key) => void memory.delete(key),\n }\n}\n\nexport class ConvexAuthAdapter implements AuthAdapter {\n private readonly tokenKey: string\n\n constructor(\n private readonly client: ConvexHttpClient,\n private readonly store: SessionStore = defaultStore(),\n /** Deployment URL the token is scoped to (per-backend sessions, EC-179). */\n deploymentUrl?: string,\n ) {\n this.tokenKey = tokenStorageKey(deploymentUrl)\n }\n\n async currentSession(): Promise<AuthSession | null> {\n const token = this.store.getItem(this.tokenKey)\n if (!token) return null\n this.client.setAuth(token)\n try {\n const me = (await this.client.query(queryRef('users:me'), {})) as MeResult | null\n if (!me) {\n this.clearToken()\n return null\n }\n return { user: this.toUser(me) }\n } catch {\n // Expired/invalid JWT or unreachable backend → treat as signed out.\n this.clearToken()\n return null\n }\n }\n\n async signIn(credentials: SignInCredentials): Promise<SignInResult> {\n return this.authenticate('signIn', credentials)\n }\n\n async signUp(credentials: SignInCredentials): Promise<SignInResult> {\n return this.authenticate('signUp', credentials)\n }\n\n /**\n * Shared password flow (EC-150/EC-179). `signIn` and `signUp` differ only by\n * the Convex Auth `flow` param; both return a token + load `users:me`.\n */\n private async authenticate(\n flow: 'signIn' | 'signUp',\n credentials: SignInCredentials,\n ): Promise<SignInResult> {\n let result: SignInActionResult\n try {\n result = (await this.client.action(actionRef('auth:signIn'), {\n provider: 'password',\n params: {\n email: credentials.email.trim().toLowerCase(),\n password: credentials.password,\n flow,\n },\n })) as SignInActionResult\n } catch (error) {\n // Convex Auth surfaces credential/registration problems as thrown errors.\n const message = error instanceof Error ? error.message : String(error)\n if (flow === 'signUp' && /already|exists|InvalidAccountId/i.test(message)) {\n return {\n ok: false,\n reason: 'account-exists',\n message: 'An account with this email already exists — sign in instead.',\n }\n }\n if (/InvalidSecret|InvalidAccountId|credential|password/i.test(message)) {\n return {\n ok: false,\n reason: 'invalid-credentials',\n message:\n flow === 'signUp'\n ? 'That email or password can’t be used (password too short?).'\n : 'Email or password is incorrect.',\n }\n }\n const verb = flow === 'signUp' ? 'Sign-up' : 'Sign-in'\n return { ok: false, reason: 'unavailable', message: `${verb} is currently unavailable.` }\n }\n if (!result.tokens) {\n return {\n ok: false,\n reason: 'invalid-credentials',\n message: 'Email or password is incorrect.',\n }\n }\n this.store.setItem(this.tokenKey, result.tokens.token)\n this.client.setAuth(result.tokens.token)\n const me = (await this.client.query(queryRef('users:me'), {})) as MeResult | null\n if (!me) {\n this.clearToken()\n return { ok: false, reason: 'unavailable', message: 'Sign-in is currently unavailable.' }\n }\n return { ok: true, session: { user: this.toUser(me) } }\n }\n\n async signOut(): Promise<void> {\n try {\n await this.client.action(actionRef('auth:signOut'), {})\n } catch {\n // Already signed out / unreachable — clearing local state is what matters.\n }\n this.clearToken()\n }\n\n private toUser(me: MeResult): AuthSession['user'] {\n return {\n id: me.id,\n email: me.email,\n ...(me.name ? { name: me.name } : {}),\n // No global flags: project creation is config/CLI-owned (AD-11), so there\n // is no in-studio `create-projects` capability. Per-project roles come\n // from `projectMembers`.\n globalFlags: [],\n }\n }\n\n private clearToken(): void {\n this.store.removeItem(this.tokenKey)\n this.client.clearAuth()\n }\n}\n"]}
1
+ {"version":3,"file":"convex-auth-adapter.js","sourceRoot":"","sources":["../../../src/lib/auth/convex-auth-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ConvexHttpClient } from 'convex/browser'\nimport { actionRef, mutationRef, queryRef } from '../persistence/convex-client'\nimport type {\n AuthAdapter,\n AuthSession,\n ChangePasswordInput,\n ChangePasswordResult,\n SignInCredentials,\n SignInResult,\n UpdateProfileInput,\n UpdateProfileResult,\n} from './auth-adapter'\nimport type { SessionStore } from './local-auth-adapter'\n\n/**\n * Convex Auth implementation of the project-owned {@link AuthAdapter}\n * (EC-150). Decision record (also in `convex/auth.ts`): **Convex Auth** over\n * Clerk — self-contained on the Convex stack, sessions in our own tables, no\n * extra vendor; reachable only through this adapter so a swap stays local.\n *\n * Flow (server side lives in `convex/auth.ts` + `convex/users.ts`):\n * 1. `signIn` calls the `auth:signIn` action with the Password provider\n * (`flow: 'signIn'`); on success Convex returns a JWT + refresh token.\n * 2. The JWT is set on the *shared* Convex client (`client.setAuth`), so every\n * subsequent persistence query/mutation runs authenticated and the\n * server-side `requireRole` guards see the caller.\n * 3. `currentSession` restores the JWT from storage and asks `users:me`.\n * 4. `signOut` calls `auth:signOut` and clears the JWT.\n *\n * v1 scope note: the stored JWT is short-lived and this adapter does not yet\n * run the refresh-token loop (the official `@convex-dev/auth/react` client\n * does); an expired session simply resolves to \"signed out\" and the user signs\n * in again. Live-deployment hardening is EC-156. No live deployment exists in\n * this repo — this file is typecheck-verified and exercised against the local\n * adapter's shared interface.\n */\n\nconst TOKEN_STORAGE_PREFIX = 'elytra.convex-auth.jwt'\n\n/**\n * Classify a thrown sign-in error as a DEPLOYMENT-setup failure rather than a\n * credential problem (EC-257). Matches the Convex Auth signing-path throws:\n * `requireEnv('JWT_PRIVATE_KEY'|'JWKS'|'CONVEX_SITE_URL')` (→ \"Missing environment\n * variable …\") and `importPKCS8` on a malformed key. Deliberately narrow — a genuine\n * wrong-password error carries none of these tokens — and checked BEFORE the credential\n * regex so it always wins. Note: a PRODUCTION deployment redacts non-`ConvexError`\n * messages to a generic \"Server Error\", which this can't match; there the honest\n * fallback is the neutral \"unavailable\" branch (never \"password too short\").\n */\nexport function isDeploymentMisconfigured(message: string): boolean {\n return /JWT_PRIVATE_KEY|JWKS|CONVEX_SITE_URL|Missing environment variable|PKCS8|private key/i.test(\n message,\n )\n}\n\n/**\n * Per-deployment token key (EC-179, vision AD-8). One project per backend, so a\n * session is scoped to its deployment: signing into Aurora's backend must not\n * carry a JWT to Northwind's. Keying the stored token by the deployment URL\n * makes each backend's session independent — switching to a project on a\n * not-yet-authenticated backend reads no token and the guard asks for sign-in.\n * (A future shared external IdP — WorkOS/Clerk — would issue one identity valid\n * on every deployment, collapsing this back to a single session.)\n */\nexport function tokenStorageKey(deploymentUrl?: string): string {\n return deploymentUrl ? `${TOKEN_STORAGE_PREFIX}::${deploymentUrl}` : TOKEN_STORAGE_PREFIX\n}\n\n/**\n * Best-effort \"is there a session for this deployment?\" — a synchronous token\n * presence check used to label per-backend projects in the switcher/launcher\n * (EC-179). It does not validate the token (an expired one still redirects to\n * sign-in on entry); it just answers \"has the user signed into this backend?\".\n */\nexport function hasStoredConvexSession(\n deploymentUrl: string,\n store: SessionStore = defaultStore(),\n): boolean {\n return store.getItem(tokenStorageKey(deploymentUrl)) !== null\n}\n\ninterface SignInActionResult {\n tokens?: { token: string; refreshToken: string } | null\n}\n\ninterface MeResult {\n id: string\n email: string\n name?: string\n}\n\nfunction defaultStore(): SessionStore {\n if (typeof window !== 'undefined' && window.sessionStorage) return window.sessionStorage\n const memory = new Map<string, string>()\n return {\n getItem: (key) => memory.get(key) ?? null,\n setItem: (key, value) => void memory.set(key, value),\n removeItem: (key) => void memory.delete(key),\n }\n}\n\nexport class ConvexAuthAdapter implements AuthAdapter {\n private readonly tokenKey: string\n\n constructor(\n private readonly client: ConvexHttpClient,\n private readonly store: SessionStore = defaultStore(),\n /** Deployment URL the token is scoped to (per-backend sessions, EC-179). */\n deploymentUrl?: string,\n ) {\n this.tokenKey = tokenStorageKey(deploymentUrl)\n }\n\n async currentSession(): Promise<AuthSession | null> {\n const token = this.store.getItem(this.tokenKey)\n if (!token) return null\n this.client.setAuth(token)\n try {\n const me = (await this.client.query(queryRef('users:me'), {})) as MeResult | null\n if (!me) {\n this.clearToken()\n return null\n }\n return { user: this.toUser(me) }\n } catch {\n // Expired/invalid JWT or unreachable backend → treat as signed out.\n this.clearToken()\n return null\n }\n }\n\n async signIn(credentials: SignInCredentials): Promise<SignInResult> {\n return this.authenticate('signIn', credentials)\n }\n\n async signUp(credentials: SignInCredentials): Promise<SignInResult> {\n return this.authenticate('signUp', credentials)\n }\n\n /**\n * Shared password flow (EC-150/EC-179). `signIn` and `signUp` differ only by\n * the Convex Auth `flow` param; both return a token + load `users:me`.\n */\n private async authenticate(\n flow: 'signIn' | 'signUp',\n credentials: SignInCredentials,\n ): Promise<SignInResult> {\n let result: SignInActionResult\n try {\n result = (await this.client.action(actionRef('auth:signIn'), {\n provider: 'password',\n params: {\n email: credentials.email.trim().toLowerCase(),\n password: credentials.password,\n flow,\n },\n })) as SignInActionResult\n } catch (error) {\n // Convex Auth surfaces credential/registration problems as thrown errors.\n const message = error instanceof Error ? error.message : String(error)\n // EC-257: a DEPLOYMENT-misconfiguration — not a credential problem — must be\n // classified FIRST so it never falls through to the lying \"password too short\".\n // Convex Auth signs the session JWT with the deployment's `JWT_PRIVATE_KEY`\n // (+ `JWKS`/`CONVEX_SITE_URL`); when any is unset (`npx @convex-dev/auth`\n // never run) the `requireEnv`/`importPKCS8` call throws *after* the password\n // is validated, so EVERY sign-in fails regardless of the password. This is\n // the exact footgun that cost hours on the Konditorei proof (2026-06-22).\n if (isDeploymentMisconfigured(message)) {\n return {\n ok: false,\n reason: 'deployment-misconfigured',\n message:\n 'This deployment can’t sign you in: its auth signing key is not set. Run `npx @convex-dev/auth` against this deployment, then retry. (The deployment-setup check reports the same.)',\n }\n }\n // EC-324: an uninvited sign-up is rejected server-side (`beforeSessionCreation`\n // throws a `ConvexError('not-invited: …')`, whose data survives to the client\n // even in production). Classified BEFORE the credential regex so it never\n // degrades to \"password too short\".\n if (/not-invited|invite-only|invite only/i.test(message)) {\n return {\n ok: false,\n reason: 'not-invited',\n message:\n 'This deployment is invite-only. Ask an admin to invite your email, then create your account.',\n }\n }\n if (flow === 'signUp' && /already|exists|InvalidAccountId/i.test(message)) {\n return {\n ok: false,\n reason: 'account-exists',\n message: 'An account with this email already exists — sign in instead.',\n }\n }\n if (/InvalidSecret|InvalidAccountId|credential|password/i.test(message)) {\n return {\n ok: false,\n reason: 'invalid-credentials',\n message:\n flow === 'signUp'\n ? 'That email or password can’t be used (password too short?).'\n : 'Email or password is incorrect.',\n }\n }\n const verb = flow === 'signUp' ? 'Sign-up' : 'Sign-in'\n return { ok: false, reason: 'unavailable', message: `${verb} is currently unavailable.` }\n }\n if (!result.tokens) {\n return {\n ok: false,\n reason: 'invalid-credentials',\n message: 'Email or password is incorrect.',\n }\n }\n this.store.setItem(this.tokenKey, result.tokens.token)\n this.client.setAuth(result.tokens.token)\n const me = (await this.client.query(queryRef('users:me'), {})) as MeResult | null\n if (!me) {\n this.clearToken()\n return { ok: false, reason: 'unavailable', message: 'Sign-in is currently unavailable.' }\n }\n return { ok: true, session: { user: this.toUser(me) } }\n }\n\n async signOut(): Promise<void> {\n try {\n await this.client.action(actionRef('auth:signOut'), {})\n } catch {\n // Already signed out / unreachable — clearing local state is what matters.\n }\n this.clearToken()\n }\n\n /**\n * Self-service identity edit (EC-240 H-tail item 2). Patches the caller's OWN\n * auth `users` row (name / avatar) via the self-scoped `users:updateProfile`\n * mutation, then re-reads `users:me` so the returned session carries the new\n * name. Auth-owned fields never flow through the members-rail `users.save`.\n */\n async updateProfile(input: UpdateProfileInput): Promise<UpdateProfileResult> {\n const token = this.store.getItem(this.tokenKey)\n if (!token) {\n return { ok: false, reason: 'not-authenticated', message: 'You are not signed in.' }\n }\n this.client.setAuth(token)\n try {\n await this.client.mutation(mutationRef('users:updateProfile'), {\n ...(input.name !== undefined ? { name: input.name } : {}),\n ...(input.avatar !== undefined ? { avatar: input.avatar } : {}),\n })\n const me = (await this.client.query(queryRef('users:me'), {})) as MeResult | null\n if (!me) {\n return { ok: false, reason: 'unavailable', message: 'Profile update is currently unavailable.' }\n }\n return { ok: true, session: { user: this.toUser(me) } }\n } catch {\n return { ok: false, reason: 'unavailable', message: 'Profile update is currently unavailable.' }\n }\n }\n\n /**\n * Self-service password change (EC-240 H-tail item 3). Delegates entirely to\n * the provider via the self-scoped `users:changePassword` ACTION (which calls\n * Convex Auth's `retrieveAccount` to verify the current password, then\n * `modifyAccountCredentials` to set the new one). **Seam only:** the raw\n * password values are forwarded to the provider and NEVER logged or inspected\n * here.\n */\n async changePassword(input: ChangePasswordInput): Promise<ChangePasswordResult> {\n const token = this.store.getItem(this.tokenKey)\n if (!token) {\n return { ok: false, reason: 'not-authenticated', message: 'You are not signed in.' }\n }\n this.client.setAuth(token)\n try {\n await this.client.action(actionRef('users:changePassword'), {\n currentPassword: input.currentPassword,\n newPassword: input.newPassword,\n })\n return { ok: true }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n if (isDeploymentMisconfigured(message)) {\n return {\n ok: false,\n reason: 'unavailable',\n message:\n 'This deployment can’t change your password: its auth signing key is not set. Run `npx @convex-dev/auth`.',\n }\n }\n if (/InvalidSecret|InvalidAccountId|current password|incorrect/i.test(message)) {\n return {\n ok: false,\n reason: 'invalid-credentials',\n message: 'Your current password is incorrect.',\n }\n }\n if (/at least|too short|8 characters|password requirement|weak/i.test(message)) {\n return {\n ok: false,\n reason: 'weak-password',\n message: 'Choose a stronger password (at least 8 characters).',\n }\n }\n return { ok: false, reason: 'unavailable', message: 'Password change is currently unavailable.' }\n }\n }\n\n private toUser(me: MeResult): AuthSession['user'] {\n return {\n id: me.id,\n email: me.email,\n ...(me.name ? { name: me.name } : {}),\n // No global flags: project creation is config/CLI-owned (AD-11), so there\n // is no in-studio `create-projects` capability. Per-project roles come\n // from `projectMembers`.\n globalFlags: [],\n }\n }\n\n private clearToken(): void {\n this.store.removeItem(this.tokenKey)\n this.client.clearAuth()\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { AuthAdapter, AuthSession, SignInCredentials, SignInResult } from './auth-adapter';
1
+ import type { AuthAdapter, AuthSession, ChangePasswordInput, ChangePasswordResult, SignInCredentials, SignInResult, UpdateProfileInput, UpdateProfileResult } from './auth-adapter';
2
2
  /**
3
3
  * Dev-only unattended-auth decorator (testing de-friction). Wraps any
4
4
  * {@link AuthAdapter}: when no session can be restored, it transparently signs
@@ -22,4 +22,6 @@ export declare class DevAutoLoginAdapter implements AuthAdapter {
22
22
  signIn(credentials: SignInCredentials): Promise<SignInResult>;
23
23
  signUp(credentials: SignInCredentials): Promise<SignInResult>;
24
24
  signOut(): Promise<void>;
25
+ updateProfile(input: UpdateProfileInput): Promise<UpdateProfileResult>;
26
+ changePassword(input: ChangePasswordInput): Promise<ChangePasswordResult>;
25
27
  }
@@ -36,5 +36,11 @@ export class DevAutoLoginAdapter {
36
36
  signOut() {
37
37
  return this.inner.signOut();
38
38
  }
39
+ updateProfile(input) {
40
+ return this.inner.updateProfile(input);
41
+ }
42
+ changePassword(input) {
43
+ return this.inner.changePassword(input);
44
+ }
39
45
  }
40
46
  //# sourceMappingURL=dev-auto-login-adapter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dev-auto-login-adapter.js","sourceRoot":"","sources":["../../../src/lib/auth/dev-auto-login-adapter.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,mBAAmB;IAEX;IACA;IAFnB,YACmB,KAAkB,EAClB,WAA8B;QAD9B,UAAK,GAAL,KAAK,CAAa;QAClB,gBAAW,GAAX,WAAW,CAAmB;IAC9C,CAAC;IAEJ,KAAK,CAAC,cAAc;QAClB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAA;QAClD,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAA;QAC7B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;QACxD,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA;IAC1C,CAAC;IAED,MAAM,CAAC,WAA8B;QACnC,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;IACvC,CAAC;IAED,MAAM,CAAC,WAA8B;QACnC,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;IACvC,CAAC;IAED,OAAO;QACL,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAA;IAC7B,CAAC;CACF","sourcesContent":["import type { AuthAdapter, AuthSession, SignInCredentials, SignInResult } from './auth-adapter'\n\n/**\n * Dev-only unattended-auth decorator (testing de-friction). Wraps any\n * {@link AuthAdapter}: when no session can be restored, it transparently signs\n * in with the configured dev credentials so the studio boots already\n * authenticated for local/automated testing.\n *\n * This is **not** an auth bypass — it performs a real {@link AuthAdapter.signIn},\n * so the resulting session is a genuine backend session and the server-side\n * role guards still apply (a non-member or wrong password surfaces as the normal\n * signed-out state, not silent access). It is only ever constructed in a dev\n * build (see `resolveDevAutoLogin`), so it cannot reach production.\n *\n * Every method but {@link currentSession} delegates unchanged; `currentSession`\n * adds the auto-sign-in fallback after the wrapped adapter reports no session.\n */\nexport class DevAutoLoginAdapter implements AuthAdapter {\n constructor(\n private readonly inner: AuthAdapter,\n private readonly credentials: SignInCredentials,\n ) {}\n\n async currentSession(): Promise<AuthSession | null> {\n const restored = await this.inner.currentSession()\n if (restored) return restored\n const result = await this.inner.signIn(this.credentials)\n return result.ok ? result.session : null\n }\n\n signIn(credentials: SignInCredentials): Promise<SignInResult> {\n return this.inner.signIn(credentials)\n }\n\n signUp(credentials: SignInCredentials): Promise<SignInResult> {\n return this.inner.signUp(credentials)\n }\n\n signOut(): Promise<void> {\n return this.inner.signOut()\n }\n}\n"]}
1
+ {"version":3,"file":"dev-auto-login-adapter.js","sourceRoot":"","sources":["../../../src/lib/auth/dev-auto-login-adapter.ts"],"names":[],"mappings":"AAWA;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,mBAAmB;IAEX;IACA;IAFnB,YACmB,KAAkB,EAClB,WAA8B;QAD9B,UAAK,GAAL,KAAK,CAAa;QAClB,gBAAW,GAAX,WAAW,CAAmB;IAC9C,CAAC;IAEJ,KAAK,CAAC,cAAc;QAClB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAA;QAClD,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAA;QAC7B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;QACxD,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA;IAC1C,CAAC;IAED,MAAM,CAAC,WAA8B;QACnC,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;IACvC,CAAC;IAED,MAAM,CAAC,WAA8B;QACnC,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;IACvC,CAAC;IAED,OAAO;QACL,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAA;IAC7B,CAAC;IAED,aAAa,CAAC,KAAyB;QACrC,OAAO,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;IACxC,CAAC;IAED,cAAc,CAAC,KAA0B;QACvC,OAAO,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,CAAA;IACzC,CAAC;CACF","sourcesContent":["import type {\n AuthAdapter,\n AuthSession,\n ChangePasswordInput,\n ChangePasswordResult,\n SignInCredentials,\n SignInResult,\n UpdateProfileInput,\n UpdateProfileResult,\n} from './auth-adapter'\n\n/**\n * Dev-only unattended-auth decorator (testing de-friction). Wraps any\n * {@link AuthAdapter}: when no session can be restored, it transparently signs\n * in with the configured dev credentials so the studio boots already\n * authenticated for local/automated testing.\n *\n * This is **not** an auth bypass — it performs a real {@link AuthAdapter.signIn},\n * so the resulting session is a genuine backend session and the server-side\n * role guards still apply (a non-member or wrong password surfaces as the normal\n * signed-out state, not silent access). It is only ever constructed in a dev\n * build (see `resolveDevAutoLogin`), so it cannot reach production.\n *\n * Every method but {@link currentSession} delegates unchanged; `currentSession`\n * adds the auto-sign-in fallback after the wrapped adapter reports no session.\n */\nexport class DevAutoLoginAdapter implements AuthAdapter {\n constructor(\n private readonly inner: AuthAdapter,\n private readonly credentials: SignInCredentials,\n ) {}\n\n async currentSession(): Promise<AuthSession | null> {\n const restored = await this.inner.currentSession()\n if (restored) return restored\n const result = await this.inner.signIn(this.credentials)\n return result.ok ? result.session : null\n }\n\n signIn(credentials: SignInCredentials): Promise<SignInResult> {\n return this.inner.signIn(credentials)\n }\n\n signUp(credentials: SignInCredentials): Promise<SignInResult> {\n return this.inner.signUp(credentials)\n }\n\n signOut(): Promise<void> {\n return this.inner.signOut()\n }\n\n updateProfile(input: UpdateProfileInput): Promise<UpdateProfileResult> {\n return this.inner.updateProfile(input)\n }\n\n changePassword(input: ChangePasswordInput): Promise<ChangePasswordResult> {\n return this.inner.changePassword(input)\n }\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import type { StudioBackendMode } from '../persistence/backend-mode';
2
2
  import type { AuthAdapter } from './auth-adapter';
3
- export type { AuthAdapter, AuthSession, AuthUser, SignInCredentials, SignInResult, } from './auth-adapter';
3
+ export type { AuthAdapter, AuthSession, AuthUser, ChangePasswordInput, ChangePasswordResult, SignInCredentials, SignInResult, UpdateProfileInput, UpdateProfileResult, } from './auth-adapter';
4
4
  export { LocalAuthAdapter, LOCAL_DEV_CREDENTIALS } from './local-auth-adapter';
5
5
  export type { SessionStore } from './local-auth-adapter';
6
6
  export { ConvexAuthAdapter } from './convex-auth-adapter';
@@ -13,7 +13,7 @@ export type { AuthStatus, RouteGuardDecision } from './route-guard';
13
13
  * persistence adapter uses, so the session JWT authenticates every call); the
14
14
  * explicit playground mode means the deterministic local adapter with the
15
15
  * seeded dev users. A misconfigured backend is a hard error here too — never
16
- * a silent local fallback (the `BackendGate` renders it before this runs).
16
+ * a silent local fallback (the `DeploymentSetupGate` renders it before this runs).
17
17
  */
18
18
  export declare function selectAuthAdapter(): AuthAdapter;
19
19
  /** Mirrors `persistenceMode()` — auth and persistence always agree (EC-150). */
@@ -14,7 +14,7 @@ export { SIGN_IN_PATH, isPublicPath, postSignInTarget, resolveRouteGuard, sessio
14
14
  * persistence adapter uses, so the session JWT authenticates every call); the
15
15
  * explicit playground mode means the deterministic local adapter with the
16
16
  * seeded dev users. A misconfigured backend is a hard error here too — never
17
- * a silent local fallback (the `BackendGate` renders it before this runs).
17
+ * a silent local fallback (the `DeploymentSetupGate` renders it before this runs).
18
18
  */
19
19
  export function selectAuthAdapter() {
20
20
  const config = resolveBackendConfig();
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAA;AAC9D,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AACvF,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAGzE,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AASvD,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAE9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,eAAe,CAAA;AAGtB;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,2EAA2E;QAC3E,uEAAuE;QACvE,MAAM,GAAG,GAAG,oBAAoB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAC5C,MAAM,IAAI,GAAG,IAAI,iBAAiB,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,CAAA;QACxE,OAAO,gBAAgB,CAAC,IAAI,CAAC,CAAA;IAC/B,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,gBAAgB,CAAC,IAAI,gBAAgB,EAAE,CAAC,CAAA;IACjF,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;AAChC,CAAC;AAED;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,OAAoB;IAC5C,MAAM,WAAW,GAAG,mBAAmB,EAAE,CAAA;IACzC,OAAO,WAAW,CAAC,CAAC,CAAC,IAAI,mBAAmB,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;AAC9E,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,QAAQ;IACtB,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;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IACzC,OAAO,sBAAsB,CAAC,oBAAoB,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;AACvE,CAAC","sourcesContent":["import { getConvexClient } from '../persistence/convex-client'\nimport { resolveBackendConfig, resolveDevAutoLogin } from '../persistence/backend-mode'\nimport { deploymentUrlForSlug, resolveDeploymentUrl } from '../workspace'\nimport type { StudioBackendMode } from '../persistence/backend-mode'\nimport type { AuthAdapter } from './auth-adapter'\nimport { ConvexAuthAdapter, hasStoredConvexSession } from './convex-auth-adapter'\nimport { DevAutoLoginAdapter } from './dev-auto-login-adapter'\nimport { LocalAuthAdapter } from './local-auth-adapter'\n\nexport type {\n AuthAdapter,\n AuthSession,\n AuthUser,\n SignInCredentials,\n SignInResult,\n} from './auth-adapter'\nexport { LocalAuthAdapter, LOCAL_DEV_CREDENTIALS } from './local-auth-adapter'\nexport type { SessionStore } from './local-auth-adapter'\nexport { ConvexAuthAdapter } from './convex-auth-adapter'\nexport { DevAutoLoginAdapter } from './dev-auto-login-adapter'\nexport {\n SIGN_IN_PATH,\n isPublicPath,\n postSignInTarget,\n resolveRouteGuard,\n sessionPrincipal,\n} from './route-guard'\nexport type { AuthStatus, RouteGuardDecision } from './route-guard'\n\n/**\n * Select the auth backend (EC-150/EC-153), mirroring `selectPersistenceAdapter`\n * exactly: Convex mode means Convex Auth (over the same shared client the\n * persistence adapter uses, so the session JWT authenticates every call); the\n * explicit playground mode means the deterministic local adapter with the\n * seeded dev users. A misconfigured backend is a hard error here too — never\n * a silent local fallback (the `BackendGate` renders it before this runs).\n */\nexport function selectAuthAdapter(): AuthAdapter {\n const config = resolveBackendConfig()\n if (config.kind === 'convex') {\n // The active project (EC-179) selects the deployment; the session token is\n // scoped to that deployment URL so each backend keeps its own session.\n const url = resolveDeploymentUrl(config.url)\n const base = new ConvexAuthAdapter(getConvexClient(url), undefined, url)\n return withDevAutoLogin(base)\n }\n if (config.kind === 'playground') return withDevAutoLogin(new LocalAuthAdapter())\n throw new Error(config.detail)\n}\n\n/**\n * Dev-only: wrap an adapter so the studio auto-signs-in for unattended testing\n * when `VITE_ELYTRA_DEV_LOGIN` is configured in a dev build. No-op otherwise\n * (and dead-code-eliminated in production builds — see `resolveDevAutoLogin`).\n */\nfunction withDevAutoLogin(adapter: AuthAdapter): AuthAdapter {\n const credentials = resolveDevAutoLogin()\n return credentials ? new DevAutoLoginAdapter(adapter, credentials) : adapter\n}\n\n/** Mirrors `persistenceMode()` — auth and persistence always agree (EC-150). */\nexport function authMode(): StudioBackendMode {\n const config = resolveBackendConfig()\n if (config.kind === 'misconfigured') throw new Error(config.detail)\n return config.kind\n}\n\n/**\n * Whether the user has a session on a given project's backend (EC-179). One\n * project per backend, so this is the per-project sign-in state the switcher and\n * launcher surface (\"Sign in\" vs \"Open project\"). Playground mode is a single\n * local backend — always \"signed in\". Best-effort (token presence, see\n * {@link hasStoredConvexSession}); entry still enforces a valid session.\n */\nexport function hasProjectSession(slug: string): boolean {\n const config = resolveBackendConfig()\n if (config.kind !== 'convex') return true\n return hasStoredConvexSession(deploymentUrlForSlug(slug, config.url))\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAA;AAC9D,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AACvF,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAGzE,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAavD,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAE9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,eAAe,CAAA;AAGtB;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,2EAA2E;QAC3E,uEAAuE;QACvE,MAAM,GAAG,GAAG,oBAAoB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAC5C,MAAM,IAAI,GAAG,IAAI,iBAAiB,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,CAAA;QACxE,OAAO,gBAAgB,CAAC,IAAI,CAAC,CAAA;IAC/B,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,gBAAgB,CAAC,IAAI,gBAAgB,EAAE,CAAC,CAAA;IACjF,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;AAChC,CAAC;AAED;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,OAAoB;IAC5C,MAAM,WAAW,GAAG,mBAAmB,EAAE,CAAA;IACzC,OAAO,WAAW,CAAC,CAAC,CAAC,IAAI,mBAAmB,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;AAC9E,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,QAAQ;IACtB,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;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IACzC,OAAO,sBAAsB,CAAC,oBAAoB,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;AACvE,CAAC","sourcesContent":["import { getConvexClient } from '../persistence/convex-client'\nimport { resolveBackendConfig, resolveDevAutoLogin } from '../persistence/backend-mode'\nimport { deploymentUrlForSlug, resolveDeploymentUrl } from '../workspace'\nimport type { StudioBackendMode } from '../persistence/backend-mode'\nimport type { AuthAdapter } from './auth-adapter'\nimport { ConvexAuthAdapter, hasStoredConvexSession } from './convex-auth-adapter'\nimport { DevAutoLoginAdapter } from './dev-auto-login-adapter'\nimport { LocalAuthAdapter } from './local-auth-adapter'\n\nexport type {\n AuthAdapter,\n AuthSession,\n AuthUser,\n ChangePasswordInput,\n ChangePasswordResult,\n SignInCredentials,\n SignInResult,\n UpdateProfileInput,\n UpdateProfileResult,\n} from './auth-adapter'\nexport { LocalAuthAdapter, LOCAL_DEV_CREDENTIALS } from './local-auth-adapter'\nexport type { SessionStore } from './local-auth-adapter'\nexport { ConvexAuthAdapter } from './convex-auth-adapter'\nexport { DevAutoLoginAdapter } from './dev-auto-login-adapter'\nexport {\n SIGN_IN_PATH,\n isPublicPath,\n postSignInTarget,\n resolveRouteGuard,\n sessionPrincipal,\n} from './route-guard'\nexport type { AuthStatus, RouteGuardDecision } from './route-guard'\n\n/**\n * Select the auth backend (EC-150/EC-153), mirroring `selectPersistenceAdapter`\n * exactly: Convex mode means Convex Auth (over the same shared client the\n * persistence adapter uses, so the session JWT authenticates every call); the\n * explicit playground mode means the deterministic local adapter with the\n * seeded dev users. A misconfigured backend is a hard error here too — never\n * a silent local fallback (the `DeploymentSetupGate` renders it before this runs).\n */\nexport function selectAuthAdapter(): AuthAdapter {\n const config = resolveBackendConfig()\n if (config.kind === 'convex') {\n // The active project (EC-179) selects the deployment; the session token is\n // scoped to that deployment URL so each backend keeps its own session.\n const url = resolveDeploymentUrl(config.url)\n const base = new ConvexAuthAdapter(getConvexClient(url), undefined, url)\n return withDevAutoLogin(base)\n }\n if (config.kind === 'playground') return withDevAutoLogin(new LocalAuthAdapter())\n throw new Error(config.detail)\n}\n\n/**\n * Dev-only: wrap an adapter so the studio auto-signs-in for unattended testing\n * when `VITE_ELYTRA_DEV_LOGIN` is configured in a dev build. No-op otherwise\n * (and dead-code-eliminated in production builds — see `resolveDevAutoLogin`).\n */\nfunction withDevAutoLogin(adapter: AuthAdapter): AuthAdapter {\n const credentials = resolveDevAutoLogin()\n return credentials ? new DevAutoLoginAdapter(adapter, credentials) : adapter\n}\n\n/** Mirrors `persistenceMode()` — auth and persistence always agree (EC-150). */\nexport function authMode(): StudioBackendMode {\n const config = resolveBackendConfig()\n if (config.kind === 'misconfigured') throw new Error(config.detail)\n return config.kind\n}\n\n/**\n * Whether the user has a session on a given project's backend (EC-179). One\n * project per backend, so this is the per-project sign-in state the switcher and\n * launcher surface (\"Sign in\" vs \"Open project\"). Playground mode is a single\n * local backend — always \"signed in\". Best-effort (token presence, see\n * {@link hasStoredConvexSession}); entry still enforces a valid session.\n */\nexport function hasProjectSession(slug: string): boolean {\n const config = resolveBackendConfig()\n if (config.kind !== 'convex') return true\n return hasStoredConvexSession(deploymentUrlForSlug(slug, config.url))\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { AuthAdapter, AuthSession, SignInCredentials, SignInResult } from './auth-adapter';
1
+ import type { AuthAdapter, AuthSession, ChangePasswordResult, SignInCredentials, SignInResult, UpdateProfileResult } from './auth-adapter';
2
2
  /**
3
3
  * Deterministic local auth (EC-150) for the in-memory persistence mode: the
4
4
  * whole sign-in → role-enforcement → sign-out flow works offline, in tests,
@@ -54,4 +54,6 @@ export declare class LocalAuthAdapter implements AuthAdapter {
54
54
  signIn(credentials: SignInCredentials): Promise<SignInResult>;
55
55
  signUp(): Promise<SignInResult>;
56
56
  signOut(): Promise<void>;
57
+ updateProfile(): Promise<UpdateProfileResult>;
58
+ changePassword(): Promise<ChangePasswordResult>;
57
59
  }
@@ -74,5 +74,21 @@ export class LocalAuthAdapter {
74
74
  async signOut() {
75
75
  this.store.removeItem(STORAGE_KEY);
76
76
  }
77
+ async updateProfile() {
78
+ // Playground users are fixed, documented fixtures — there is no persistence to
79
+ // write. Surfaced as a state, not a throw, so the account surface can explain it.
80
+ return {
81
+ ok: false,
82
+ reason: 'unsupported',
83
+ message: 'Profile editing isn’t available in playground mode.',
84
+ };
85
+ }
86
+ async changePassword() {
87
+ return {
88
+ ok: false,
89
+ reason: 'unsupported',
90
+ message: 'Password change isn’t available in playground mode.',
91
+ };
92
+ }
77
93
  }
78
94
  //# sourceMappingURL=local-auth-adapter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"local-auth-adapter.js","sourceRoot":"","sources":["../../../src/lib/auth/local-auth-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import { DEV_USERS } from '@elytracms/core/persistence'\nimport type { AuthAdapter, AuthSession, SignInCredentials, SignInResult } from './auth-adapter'\n\n/**\n * Deterministic local auth (EC-150) for the in-memory persistence mode: the\n * whole sign-in → role-enforcement → sign-out flow works offline, in tests,\n * and in fixtures with zero backend setup. User ids are the canonical\n * `DEV_USERS` ids from `@elytracms/core/persistence`, which the seeded\n * `projectMembers` fixtures reference — so `admin@example.com` is admin,\n * `editor@…` editor, `viewer@…` viewer on every sample project.\n *\n * Dev credentials (fixed, documented, local-only):\n * - admin@example.com / elytra-admin\n * - editor@example.com / elytra-editor\n * - viewer@example.com / elytra-viewer\n *\n * Adminness is the `projectMembers` role, not a global flag — project creation\n * is config/CLI-owned now (AD-11), so there is no in-studio `create-projects`\n * capability to grant.\n *\n * The session survives reloads via injected storage (defaults to\n * `sessionStorage` in the browser, an in-memory map elsewhere).\n */\n\nexport const LOCAL_DEV_CREDENTIALS = [\n { user: DEV_USERS.admin, password: 'elytra-admin' },\n { user: DEV_USERS.editor, password: 'elytra-editor' },\n { user: DEV_USERS.viewer, password: 'elytra-viewer' },\n] as const\n\nconst STORAGE_KEY = 'elytra.local-auth.user-id'\n\n/** The storage slice the adapter needs (sessionStorage-compatible). */\nexport interface SessionStore {\n getItem(key: string): string | null\n setItem(key: string, value: string): void\n removeItem(key: string): void\n}\n\nfunction defaultStore(): SessionStore {\n if (typeof window !== 'undefined' && window.sessionStorage) return window.sessionStorage\n const memory = new Map<string, string>()\n return {\n getItem: (key) => memory.get(key) ?? null,\n setItem: (key, value) => void memory.set(key, value),\n removeItem: (key) => void memory.delete(key),\n }\n}\n\nexport class LocalAuthAdapter implements AuthAdapter {\n private readonly store: SessionStore\n\n constructor(store: SessionStore = defaultStore()) {\n this.store = store\n }\n\n async currentSession(): Promise<AuthSession | null> {\n const userId = this.store.getItem(STORAGE_KEY)\n if (!userId) return null\n const entry = LOCAL_DEV_CREDENTIALS.find((c) => c.user.id === userId)\n return entry ? { user: entry.user } : null\n }\n\n async signIn(credentials: SignInCredentials): Promise<SignInResult> {\n const email = credentials.email.trim().toLowerCase()\n const entry = LOCAL_DEV_CREDENTIALS.find((c) => c.user.email === email)\n if (!entry || entry.password !== credentials.password) {\n // Deliberately identical for unknown email vs wrong password.\n return {\n ok: false,\n reason: 'invalid-credentials',\n message: 'Email or password is incorrect.',\n }\n }\n this.store.setItem(STORAGE_KEY, entry.user.id)\n return { ok: true, session: { user: entry.user } }\n }\n\n async signUp(): Promise<SignInResult> {\n // Playground mode has a fixed, documented set of dev users — there is no\n // registration. Surfaced as a state, not a throw, so the sign-in screen can\n // explain it.\n return {\n ok: false,\n reason: 'unavailable',\n message: 'Sign-up isn’t available in playground mode — use the dev credentials below.',\n }\n }\n\n async signOut(): Promise<void> {\n this.store.removeItem(STORAGE_KEY)\n }\n}\n"]}
1
+ {"version":3,"file":"local-auth-adapter.js","sourceRoot":"","sources":["../../../src/lib/auth/local-auth-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import { DEV_USERS } from '@elytracms/core/persistence'\nimport type {\n AuthAdapter,\n AuthSession,\n ChangePasswordResult,\n SignInCredentials,\n SignInResult,\n UpdateProfileResult,\n} from './auth-adapter'\n\n/**\n * Deterministic local auth (EC-150) for the in-memory persistence mode: the\n * whole sign-in → role-enforcement → sign-out flow works offline, in tests,\n * and in fixtures with zero backend setup. User ids are the canonical\n * `DEV_USERS` ids from `@elytracms/core/persistence`, which the seeded\n * `projectMembers` fixtures reference — so `admin@example.com` is admin,\n * `editor@…` editor, `viewer@…` viewer on every sample project.\n *\n * Dev credentials (fixed, documented, local-only):\n * - admin@example.com / elytra-admin\n * - editor@example.com / elytra-editor\n * - viewer@example.com / elytra-viewer\n *\n * Adminness is the `projectMembers` role, not a global flag — project creation\n * is config/CLI-owned now (AD-11), so there is no in-studio `create-projects`\n * capability to grant.\n *\n * The session survives reloads via injected storage (defaults to\n * `sessionStorage` in the browser, an in-memory map elsewhere).\n */\n\nexport const LOCAL_DEV_CREDENTIALS = [\n { user: DEV_USERS.admin, password: 'elytra-admin' },\n { user: DEV_USERS.editor, password: 'elytra-editor' },\n { user: DEV_USERS.viewer, password: 'elytra-viewer' },\n] as const\n\nconst STORAGE_KEY = 'elytra.local-auth.user-id'\n\n/** The storage slice the adapter needs (sessionStorage-compatible). */\nexport interface SessionStore {\n getItem(key: string): string | null\n setItem(key: string, value: string): void\n removeItem(key: string): void\n}\n\nfunction defaultStore(): SessionStore {\n if (typeof window !== 'undefined' && window.sessionStorage) return window.sessionStorage\n const memory = new Map<string, string>()\n return {\n getItem: (key) => memory.get(key) ?? null,\n setItem: (key, value) => void memory.set(key, value),\n removeItem: (key) => void memory.delete(key),\n }\n}\n\nexport class LocalAuthAdapter implements AuthAdapter {\n private readonly store: SessionStore\n\n constructor(store: SessionStore = defaultStore()) {\n this.store = store\n }\n\n async currentSession(): Promise<AuthSession | null> {\n const userId = this.store.getItem(STORAGE_KEY)\n if (!userId) return null\n const entry = LOCAL_DEV_CREDENTIALS.find((c) => c.user.id === userId)\n return entry ? { user: entry.user } : null\n }\n\n async signIn(credentials: SignInCredentials): Promise<SignInResult> {\n const email = credentials.email.trim().toLowerCase()\n const entry = LOCAL_DEV_CREDENTIALS.find((c) => c.user.email === email)\n if (!entry || entry.password !== credentials.password) {\n // Deliberately identical for unknown email vs wrong password.\n return {\n ok: false,\n reason: 'invalid-credentials',\n message: 'Email or password is incorrect.',\n }\n }\n this.store.setItem(STORAGE_KEY, entry.user.id)\n return { ok: true, session: { user: entry.user } }\n }\n\n async signUp(): Promise<SignInResult> {\n // Playground mode has a fixed, documented set of dev users — there is no\n // registration. Surfaced as a state, not a throw, so the sign-in screen can\n // explain it.\n return {\n ok: false,\n reason: 'unavailable',\n message: 'Sign-up isn’t available in playground mode — use the dev credentials below.',\n }\n }\n\n async signOut(): Promise<void> {\n this.store.removeItem(STORAGE_KEY)\n }\n\n async updateProfile(): Promise<UpdateProfileResult> {\n // Playground users are fixed, documented fixtures — there is no persistence to\n // write. Surfaced as a state, not a throw, so the account surface can explain it.\n return {\n ok: false,\n reason: 'unsupported',\n message: 'Profile editing isn’t available in playground mode.',\n }\n }\n\n async changePassword(): Promise<ChangePasswordResult> {\n return {\n ok: false,\n reason: 'unsupported',\n message: 'Password change isn’t available in playground mode.',\n }\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import type { CmsDocument, CollectionDef, FieldDef, LocaleConfig } from '@elytracms/core/cms-core';
2
+ import type { CmsDocument, CollectionDef, FieldDef, LocaleConfig, LocalizedString } from '@elytracms/core/cms-core';
3
3
  import type { FilterOperator, FilterScalar } from '@elytracms/core/content';
4
4
  /**
5
5
  * The pure table-view model for the CMS collection table (EC-169).
@@ -152,7 +152,8 @@ export declare function defaultViewState(collection: CollectionDef): TableViewSt
152
152
  */
153
153
  export declare const configViewSchema: z.ZodObject<{
154
154
  id: z.ZodOptional<z.ZodString>;
155
- name: z.ZodString;
155
+ name: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
156
+ default: z.ZodOptional<z.ZodBoolean>;
156
157
  filters: z.ZodOptional<z.ZodArray<z.ZodObject<{
157
158
  field: z.ZodString;
158
159
  op: z.ZodEnum<{
@@ -176,11 +177,16 @@ export declare const configViewSchema: z.ZodObject<{
176
177
  hidden: z.ZodOptional<z.ZodArray<z.ZodString>>;
177
178
  }, z.core.$strip>;
178
179
  export type ConfigView = z.infer<typeof configViewSchema>;
179
- /** A resolved view: identity + the table state it restores (matches `SavedView`). */
180
+ /**
181
+ * A resolved view: identity + the table state it restores (matches `SavedView`).
182
+ * `name` stays a {@link LocalizedString} — resolved at the render site (EC-343), like
183
+ * `form.label`. `default` marks the initial-view seed (see {@link defaultConfigViewId}).
184
+ */
180
185
  export interface ResolvedView {
181
186
  id: string;
182
- name: string;
187
+ name: LocalizedString;
183
188
  state: TableViewState;
189
+ default?: boolean;
184
190
  }
185
191
  /**
186
192
  * Resolve a collection's config-declared `views` into full {@link ResolvedView}s
@@ -191,6 +197,29 @@ export interface ResolvedView {
191
197
  * code — the runtime never mutates them.
192
198
  */
193
199
  export declare function configViewsFor(collection: CollectionDef): ResolvedView[];
200
+ /**
201
+ * The id of the config view flagged `default: true` (EC-343) — the seed for a
202
+ * collection's INITIAL active view. When several are flagged (a misconfiguration,
203
+ * reported by {@link configViewIssues}) the FIRST in declaration order wins
204
+ * deterministically; when none is flagged the caller falls back to the built-in
205
+ * "All" lens. `undefined` when the collection declares no default view.
206
+ */
207
+ export declare function defaultConfigViewId(collection: CollectionDef): string | undefined;
208
+ /** A config-level problem with a collection's declared views (EC-343). */
209
+ export interface ConfigViewIssue {
210
+ code: 'duplicate-default';
211
+ message: string;
212
+ /** The offending view ids (all views flagged `default`, in declaration order). */
213
+ viewIds: string[];
214
+ }
215
+ /**
216
+ * Validate a collection's config-declared views (EC-343): at most one may set
217
+ * `default: true`. A duplicate is returned as an explicit, structured issue —
218
+ * never silently swallowed — so the studio can surface it (the first default still
219
+ * wins deterministically for seeding, per {@link defaultConfigViewId}). Pure and
220
+ * schema-only; the DOM-free counterpart of the "failures are first-class" contract.
221
+ */
222
+ export declare function configViewIssues(collection: CollectionDef): ConfigViewIssue[];
194
223
  /** Deep-cloned canonical form (stable key order via the Zod shape). */
195
224
  export declare function normalizeViewState(state: TableViewState): TableViewState;
196
225
  /** Structural equality on normalized states — drives the "unsaved changes" affordance. */
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { documentDefaultLocale, resolveField, titleFieldOf } from '@elytracms/core/cms-core';
2
+ import { documentDefaultLocale, localizedStringSchema, resolveField, resolveLabel, titleFieldOf, } from '@elytracms/core/cms-core';
3
3
  import { localizedLabel } from '@elytracms/studio/lib/i18n';
4
4
  import { FILTER_OPERATORS, compareListEntries, filterScalarOf, filterableFields, matchesCondition, validateWhere, } from '@elytracms/core/content';
5
5
  /**
@@ -237,18 +237,27 @@ export const ALL_VIEW_ID = 'all';
237
237
  * and any named view may list them in its `columns`; they're just off by default.
238
238
  */
239
239
  const NON_DEFAULT_COLUMN_TYPES = new Set(['blocks', 'richText', 'object']);
240
- /** The collection's composite (non-scalar) field names — hidden in the default view. */
241
- function nonScalarColumns(collection) {
242
- return collection.fields.filter((f) => NON_DEFAULT_COLUMN_TYPES.has(f.type)).map((f) => f.name);
240
+ /**
241
+ * Field names OFF the built-in "All" lens by default: the composite editing surfaces
242
+ * (blocks/richText/object) — always noise in a scalar list — PLUS any scalar the
243
+ * schema opts out with `form.column: false` (EC-343). Both only affect the default
244
+ * columns: the field stays in the picker one toggle away, its form and filterability
245
+ * are untouched, and a named view's explicit `columns` list ignores this entirely.
246
+ */
247
+ function defaultHiddenColumns(collection) {
248
+ return collection.fields
249
+ .filter((f) => NON_DEFAULT_COLUMN_TYPES.has(f.type) || f.form?.column === false)
250
+ .map((f) => f.name);
243
251
  }
244
252
  export function defaultViewState(collection) {
245
253
  return {
246
254
  filters: [],
247
255
  sort: null,
248
- // Every field keeps a slot (order + picker), but the composite surfaces start
249
- // hidden so the default lens is the collection's scalars (EC-246b).
256
+ // Every field keeps a slot (order + picker), but the composite surfaces and any
257
+ // `form.column: false` scalars start hidden so the default lens is the
258
+ // collection's chosen scalar columns (EC-246b / EC-343).
250
259
  columns: orderedColumnNames(collection),
251
- hidden: nonScalarColumns(collection),
260
+ hidden: defaultHiddenColumns(collection),
252
261
  };
253
262
  }
254
263
  /**
@@ -261,16 +270,32 @@ export function defaultViewState(collection) {
261
270
  export const configViewSchema = z.object({
262
271
  /** Optional stable id; defaults to a slug of `name`. */
263
272
  id: z.string().min(1).optional(),
264
- name: z.string().min(1),
273
+ /**
274
+ * View label; localizable (EC-343): a plain string or a `{ locale }` map, resolved
275
+ * at the render site via the same `resolveLabel` path as `form.label` (EC-241).
276
+ */
277
+ name: localizedStringSchema,
278
+ /**
279
+ * When true this view seeds the collection's INITIAL active view (EC-343) — the
280
+ * curated lens an editor lands on before picking one. At most one per collection;
281
+ * a duplicate is a visible validation issue ({@link configViewIssues}), never
282
+ * silent. The built-in "All" lens stays available and stays the fallback.
283
+ */
284
+ default: z.boolean().optional(),
265
285
  filters: z.array(filterClauseSchema).optional(),
266
286
  sort: viewSortSchema.nullable().optional(),
267
287
  /** Column order; when omitted, the view inherits the collection's default columns. */
268
288
  columns: z.array(z.string().min(1)).optional(),
269
289
  hidden: z.array(z.string().min(1)).optional(),
270
290
  });
271
- /** Slug used to derive a config view's stable id from its name. */
291
+ /**
292
+ * Slug used to derive a config view's stable id from its name. A localized name
293
+ * (EC-343) slugs its DEFAULT-locale text so the id is stable across the active UI
294
+ * locale (an id must not shift when a German editor loads the same config).
295
+ */
272
296
  function configViewId(name, index) {
273
- const slug = name
297
+ const text = resolveLabel(name) ?? '';
298
+ const slug = text
274
299
  .toLowerCase()
275
300
  .replace(/[^a-z0-9]+/g, '-')
276
301
  .replace(/^-+|-+$/g, '');
@@ -303,6 +328,7 @@ export function configViewsFor(collection) {
303
328
  resolved.push({
304
329
  id,
305
330
  name: view.name,
331
+ ...(view.default ? { default: true } : {}),
306
332
  state: normalizeViewState({
307
333
  filters: view.filters ?? [],
308
334
  sort: view.sort ?? null,
@@ -313,6 +339,36 @@ export function configViewsFor(collection) {
313
339
  });
314
340
  return resolved;
315
341
  }
342
+ /**
343
+ * The id of the config view flagged `default: true` (EC-343) — the seed for a
344
+ * collection's INITIAL active view. When several are flagged (a misconfiguration,
345
+ * reported by {@link configViewIssues}) the FIRST in declaration order wins
346
+ * deterministically; when none is flagged the caller falls back to the built-in
347
+ * "All" lens. `undefined` when the collection declares no default view.
348
+ */
349
+ export function defaultConfigViewId(collection) {
350
+ return configViewsFor(collection).find((view) => view.default)?.id;
351
+ }
352
+ /**
353
+ * Validate a collection's config-declared views (EC-343): at most one may set
354
+ * `default: true`. A duplicate is returned as an explicit, structured issue —
355
+ * never silently swallowed — so the studio can surface it (the first default still
356
+ * wins deterministically for seeding, per {@link defaultConfigViewId}). Pure and
357
+ * schema-only; the DOM-free counterpart of the "failures are first-class" contract.
358
+ */
359
+ export function configViewIssues(collection) {
360
+ const defaults = configViewsFor(collection).filter((view) => view.default);
361
+ if (defaults.length <= 1)
362
+ return [];
363
+ const viewIds = defaults.map((view) => view.id);
364
+ return [
365
+ {
366
+ code: 'duplicate-default',
367
+ message: `Collection "${collection.id}" declares ${defaults.length} default views (${viewIds.join(', ')}); at most one view may set \`default: true\`. Seeding "${viewIds[0]}".`,
368
+ viewIds,
369
+ },
370
+ ];
371
+ }
316
372
  /**
317
373
  * The default column order: the schema's title field first (it becomes the
318
374
  * default title/link column — EC-213 drops the separate primary-column