@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.
- package/convex/auth.ts +61 -12
- package/convex/authority.ts +40 -2
- package/convex/functions.ts +9 -0
- package/convex/invites.ts +75 -0
- package/convex/members.ts +44 -18
- package/convex/schema.ts +37 -0
- package/convex/users.ts +89 -2
- package/dist/components/account/account-settings.d.ts +13 -0
- package/dist/components/account/account-settings.js +54 -0
- package/dist/components/account/account-settings.js.map +1 -0
- package/dist/components/account/credential-panel.d.ts +14 -0
- package/dist/components/account/credential-panel.js +52 -0
- package/dist/components/account/credential-panel.js.map +1 -0
- package/dist/components/account/self-account-schema.d.ts +19 -0
- package/dist/components/account/self-account-schema.js +52 -0
- package/dist/components/account/self-account-schema.js.map +1 -0
- package/dist/components/account/self-account-view-source.d.ts +35 -0
- package/dist/components/account/self-account-view-source.js +110 -0
- package/dist/components/account/self-account-view-source.js.map +1 -0
- package/dist/components/auth-context.d.ts +5 -1
- package/dist/components/auth-context.js +9 -0
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +7 -2
- package/dist/components/cms/cms-store.js +35 -4
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/view-bar.js +4 -2
- package/dist/components/cms/view-bar.js.map +1 -1
- package/dist/components/current-user-menu.js +11 -2
- package/dist/components/current-user-menu.js.map +1 -1
- package/dist/components/deployment-setup-gate.d.ts +31 -0
- package/dist/components/deployment-setup-gate.js +92 -0
- package/dist/components/deployment-setup-gate.js.map +1 -0
- package/dist/components/project-session.d.ts +1 -1
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/use-hydration-phase.js +1 -1
- package/dist/components/use-hydration-phase.js.map +1 -1
- package/dist/components/users/invite-user-bar.d.ts +3 -0
- package/dist/components/users/invite-user-bar.js +91 -0
- package/dist/components/users/invite-user-bar.js.map +1 -0
- package/dist/components/users/users-module.js +13 -2
- package/dist/components/users/users-module.js.map +1 -1
- package/dist/lib/auth/auth-adapter.d.ts +50 -2
- package/dist/lib/auth/auth-adapter.js.map +1 -1
- package/dist/lib/auth/convex-auth-adapter.d.ts +28 -1
- package/dist/lib/auth/convex-auth-adapter.js +113 -1
- package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
- package/dist/lib/auth/dev-auto-login-adapter.d.ts +3 -1
- package/dist/lib/auth/dev-auto-login-adapter.js +6 -0
- package/dist/lib/auth/dev-auto-login-adapter.js.map +1 -1
- package/dist/lib/auth/index.d.ts +2 -2
- package/dist/lib/auth/index.js +1 -1
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/auth/local-auth-adapter.d.ts +3 -1
- package/dist/lib/auth/local-auth-adapter.js +16 -0
- package/dist/lib/auth/local-auth-adapter.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +33 -4
- package/dist/lib/cms-views/view-model.js +66 -10
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +3 -2
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +45 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +1 -1
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/backend-mode.d.ts +1 -1
- package/dist/lib/persistence/backend-mode.js +1 -1
- package/dist/lib/persistence/backend-mode.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +2 -1
- package/dist/lib/persistence/convex-adapter.js +69 -1
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.d.ts +36 -0
- package/dist/lib/persistence/deployment-readiness.js +26 -0
- package/dist/lib/persistence/deployment-readiness.js.map +1 -0
- package/dist/lib/persistence/index.d.ts +3 -1
- package/dist/lib/persistence/index.js +4 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.js +1 -1
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/dist/lib/workspace/workspace-source.d.ts +1 -1
- package/dist/lib/workspace/workspace-source.js +1 -1
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- 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":"
|
|
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"]}
|
package/dist/lib/auth/index.d.ts
CHANGED
|
@@ -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 `
|
|
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). */
|
package/dist/lib/auth/index.js
CHANGED
|
@@ -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 `
|
|
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;
|
|
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":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAA;
|
|
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
|
-
/**
|
|
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:
|
|
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
|
-
/**
|
|
241
|
-
|
|
242
|
-
|
|
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
|
|
249
|
-
// hidden so the default lens is the
|
|
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:
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|