@elytracms/studio 0.0.55 → 0.0.56

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 CHANGED
@@ -1,7 +1,8 @@
1
1
  import { convexAuth } from '@convex-dev/auth/server'
2
2
  import { Password } from '@convex-dev/auth/providers/Password'
3
+ import { actionGeneric } from 'convex/server'
3
4
  import type { AnyDataModel, GenericMutationCtx } from 'convex/server'
4
- import { ConvexError } from 'convex/values'
5
+ import { ConvexError, v } from 'convex/values'
5
6
  import type { GenericId } from 'convex/values'
6
7
  import { classifyInviteSignup, normalizeInviteEmail } from '@elytracms/core/persistence'
7
8
  import type { InviteSignupDecision } from '@elytracms/core/persistence'
@@ -9,6 +10,92 @@ import type { InviteSignupDecision } from '@elytracms/core/persistence'
9
10
  /** The provider list `convexAuth` accepts, derived from its own signature. */
10
11
  type ConvexAuthProviders = Parameters<typeof convexAuth>[0]['providers']
11
12
 
13
+ /**
14
+ * EC-363 — stable, secret-free sign-in failure codes. A PRODUCTION Convex
15
+ * deployment redacts a thrown error's `.message` to a generic "Server Error"
16
+ * before it reaches the client; only a `ConvexError`'s `.data` survives. But
17
+ * `@convex-dev/auth@0.0.94` throws its credential/env failures as PLAIN
18
+ * `Error`s, so on prod every wrong password collapsed onto the client's
19
+ * honest-generic fallback (EC-362's key finding). These codes ride in
20
+ * `ConvexError.data` across that redaction so the sign-in screen can tell the
21
+ * truth. Codes ONLY — never the raw library message (it can carry the account
22
+ * id / env-var names).
23
+ */
24
+ export type StudioAuthErrorCode =
25
+ | 'invalid-credentials'
26
+ | 'too-many-attempts'
27
+ | 'account-exists'
28
+ | 'deployment-misconfigured'
29
+
30
+ /**
31
+ * Map a `@convex-dev/auth` plain-`Error` message to a stable code — or `null`
32
+ * when unrecognized (the caller then rethrows the original error untouched, so
33
+ * unknown failures keep today's honest-generic behavior; never misclassify).
34
+ *
35
+ * The matched throw sites, pinned in the installed `@convex-dev/auth@0.0.94`:
36
+ *
37
+ * - `retrieveAccount` → `throw new Error(result)` with `result` ∈
38
+ * `"InvalidSecret" | "InvalidAccountId" | "TooManyFailedAttempts"`
39
+ * (`dist/server/implementation/index.js`; the strings come from
40
+ * `mutations/retrieveAccountWithCredentials.js`). Thrown INSIDE the signIn
41
+ * action.
42
+ * - The `Password` provider's own in-action throws: `"Invalid password"`
43
+ * (default password requirements at sign-up) and `"Invalid credentials"`
44
+ * (`dist/providers/Password.js`).
45
+ * - `requireEnv` → ``throw new Error(`Missing environment variable \`${name}\``)``
46
+ * (`dist/server/utils.js`) and jose's `importPKCS8` PKCS8/private-key parse
47
+ * errors — the EC-257 misconfigured-deployment family. These fire inside the
48
+ * library's `auth:store` MUTATION (`generateToken`); the backend propagates
49
+ * sub-call errors to the calling action UNREDACTED even on prod (redaction is
50
+ * client-boundary only — `RedactedJsError` is constructed solely in the
51
+ * sync-worker/HTTP-API crates), so the action-seam catch still sees the token.
52
+ * - `createAccountFromCredentials` → ``Account ${account.id} already exists``
53
+ * (`dist/server/implementation/mutations/createAccountFromCredentials.js`),
54
+ * also via the store mutation.
55
+ *
56
+ * Order matters: the env/signing-key family is checked FIRST (those throws fire
57
+ * after password validation and must never degrade to a credential code —
58
+ * mirrors the client ladder, EC-257).
59
+ */
60
+ export function classifyAuthLibraryError(message: string): StudioAuthErrorCode | null {
61
+ if (/Missing environment variable|JWT_PRIVATE_KEY|JWKS|PKCS8|private key/i.test(message)) {
62
+ return 'deployment-misconfigured'
63
+ }
64
+ if (/TooManyFailedAttempts/.test(message)) return 'too-many-attempts'
65
+ if (/already exists/.test(message)) return 'account-exists'
66
+ if (/InvalidSecret|InvalidAccountId|Invalid credentials|Invalid password/.test(message)) {
67
+ return 'invalid-credentials'
68
+ }
69
+ return null
70
+ }
71
+
72
+ /**
73
+ * EC-363 — the coding step for the signIn wrap: return a `ConvexError` whose
74
+ * `.data` carries a {@link StudioAuthErrorCode} for a recognized library throw,
75
+ * or the ORIGINAL error untouched for everything else.
76
+ *
77
+ * - An error already carrying `.data` (a `ConvexError`, e.g. the invite gate's
78
+ * `NOT_INVITED_REASON` crossing the store-mutation boundary) passes through
79
+ * unchanged — its data already survives prod redaction. Duck-typed, not
80
+ * `instanceof` (dual module instances, EC-357/EC-362).
81
+ * - Unrecognized errors pass through unchanged: which cases fail and how they
82
+ * read today is preserved exactly; when in doubt, the client's honest generic.
83
+ */
84
+ export function toCodedSignInError(error: unknown): unknown {
85
+ if (
86
+ typeof error === 'object' &&
87
+ error !== null &&
88
+ 'data' in error &&
89
+ (error as { data: unknown }).data !== undefined
90
+ ) {
91
+ return error
92
+ }
93
+ const message = error instanceof Error ? error.message : typeof error === 'string' ? error : ''
94
+ const code = classifyAuthLibraryError(message)
95
+ if (code === null) return error
96
+ return new ConvexError({ code })
97
+ }
98
+
12
99
  /** The (non-schema-bound) mutation ctx the `convexAuth` callbacks receive. */
13
100
  type AuthCallbackCtx = GenericMutationCtx<AnyDataModel>
14
101
 
@@ -156,7 +243,7 @@ export async function gateSessionCreation(
156
243
  * project-owned `AuthAdapter`, so swapping providers later is one factory call, not UI.
157
244
  */
158
245
  export function defineStudioAuth(options?: { providers?: ConvexAuthProviders }) {
159
- return convexAuth({
246
+ const api = convexAuth({
160
247
  providers: options?.providers ?? [Password],
161
248
  callbacks: {
162
249
  /** EC-358: reject uninvited account creation BEFORE any row commits. */
@@ -169,4 +256,60 @@ export function defineStudioAuth(options?: { providers?: ConvexAuthProviders })
169
256
  },
170
257
  },
171
258
  })
259
+
260
+ // EC-363: wrap the `auth:signIn` ACTION so the library's plain-`Error`
261
+ // credential/env throws reach the client as `ConvexError`s carrying a stable
262
+ // code (`.data` survives prod redaction; `.message` does not). The wrap is a
263
+ // pure delegate — same args validators, same mechanics, same success values;
264
+ // ONLY the thrown-error shape changes, and only for recognized library throws
265
+ // (see `toCodedSignInError`). Re-registering is required because the
266
+ // registered action's `invokeAction` closes over the ORIGINAL handler
267
+ // (mutating `_handler` after the fact would not take effect).
268
+ //
269
+ // `_handler` is set by every `actionGeneric` registration in the installed
270
+ // convex runtime (`dist/esm/server/impl/registration_impl.js`) but is not in
271
+ // the public type — hence the structural cast. The loud throw below runs at
272
+ // module-definition time, so a convex upgrade that drops the seam fails the
273
+ // functions push visibly instead of silently shipping unwrapped errors.
274
+ const rawSignInHandler = (
275
+ api.signIn as unknown as {
276
+ _handler?: (ctx: unknown, args: unknown) => Promise<unknown>
277
+ }
278
+ )._handler
279
+ if (typeof rawSignInHandler !== 'function') {
280
+ throw new Error(
281
+ 'defineStudioAuth: convexAuth().signIn no longer exposes `_handler` — the EC-363 ' +
282
+ 'coded-auth-error wrap cannot attach. Check the installed convex runtime.',
283
+ )
284
+ }
285
+ const signIn = actionGeneric({
286
+ // Mirrors the library's own `signIn` validators exactly (@convex-dev/auth
287
+ // 0.0.94, `dist/server/implementation/index.js`) so argument validation is
288
+ // unchanged.
289
+ args: {
290
+ provider: v.optional(v.string()),
291
+ params: v.optional(v.any()),
292
+ verifier: v.optional(v.string()),
293
+ refreshToken: v.optional(v.string()),
294
+ calledBy: v.optional(v.string()),
295
+ },
296
+ handler: async (ctx, args) => {
297
+ try {
298
+ return await rawSignInHandler(ctx, args)
299
+ } catch (error) {
300
+ const coded = toCodedSignInError(error)
301
+ if (coded !== error) {
302
+ // Keep the ORIGINAL failure visible in the deployment logs — the
303
+ // client now only receives the stable code.
304
+ console.error('auth:signIn failed:', error)
305
+ }
306
+ throw coded
307
+ }
308
+ },
309
+ // The delegate preserves the original action's runtime contract; align the
310
+ // exported type with it so consumers' `convex/auth.ts` re-exports and
311
+ // codegen see the exact same `signIn` surface as before.
312
+ }) as unknown as typeof api.signIn
313
+
314
+ return { ...api, signIn }
172
315
  }
@@ -5,11 +5,15 @@ import type { SessionStore } from './local-auth-adapter.js';
5
5
  * Classify a thrown sign-in error as a DEPLOYMENT-setup failure rather than a
6
6
  * credential problem (EC-257). Matches the Convex Auth signing-path throws:
7
7
  * `requireEnv('JWT_PRIVATE_KEY'|'JWKS'|'CONVEX_SITE_URL')` (→ "Missing environment
8
- * variable …") and `importPKCS8` on a malformed key. Deliberately narrow — a genuine
9
- * wrong-password error carries none of these tokens — and checked BEFORE the credential
10
- * regex so it always wins. Note: a PRODUCTION deployment redacts non-`ConvexError`
11
- * messages to a generic "Server Error", which this can't match; there the honest
12
- * fallback is the neutral "unavailable" branch (never "password too short").
8
+ * variable …") and `importPKCS8` on a malformed key — plus the EC-363 server code
9
+ * `deployment-misconfigured` (`defineStudioAuth` wraps those library throws in a
10
+ * `ConvexError` whose `.data` survives prod redaction, so this classifies on
11
+ * PRODUCTION deployments too once their functions are redeployed). Deliberately
12
+ * narrow — a genuine wrong-password error carries none of these tokens — and checked
13
+ * BEFORE the credential regex so it always wins. On a prod deployment whose
14
+ * functions PREDATE EC-363 the plain-`Error` message is redacted to "Server Error",
15
+ * which this can't match; there the honest fallback is the neutral "unavailable"
16
+ * branch (never "password too short").
13
17
  */
14
18
  export declare function isDeploymentMisconfigured(message: string): boolean;
15
19
  /**
@@ -25,9 +29,11 @@ export declare function isDeploymentMisconfigured(message: string): boolean;
25
29
  * (`dist/esm/browser/http_client.js`, ~L388-396 + L459-461). The server keeps
26
30
  * `errorData` for a `ConvexError` in prod while redacting `errorMessage`. So the
27
31
  * only classifiable text that survives prod is the `.data` of a `ConvexError` —
28
- * e.g. our invite gate's `ConvexError(NOT_INVITED_REASON)`. On DEV deployments
29
- * nothing is redacted, so `.message` also carries the token (the regex ladder
30
- * still matches); non-Convex errors only have a `.message`.
32
+ * our invite gate's `ConvexError(NOT_INVITED_REASON)` and, since EC-363, the
33
+ * coded `ConvexError({ code })` that `defineStudioAuth` wraps around the
34
+ * library's credential/env throws. On DEV deployments nothing is redacted, so
35
+ * `.message` also carries the token (the regex ladder still matches);
36
+ * non-Convex errors only have a `.message`.
31
37
  *
32
38
  * Duck-types the `data` property rather than `instanceof ConvexError`: two
33
39
  * copies of the `convex` module can coexist under Vite (transformed vs
@@ -39,16 +45,25 @@ type SignInFailure = Extract<SignInResult, {
39
45
  ok: false;
40
46
  }>;
41
47
  /**
42
- * Pure sign-in/sign-up error classifier (EC-257 / EC-324 / EC-362). Runs the
43
- * regex ladder over {@link convexErrorClassifierText} output (data-first).
48
+ * Pure sign-in/sign-up error classifier (EC-257 / EC-324 / EC-362 / EC-363).
49
+ * Runs the regex ladder over {@link convexErrorClassifierText} output
50
+ * (data-first).
51
+ *
52
+ * EC-363: `defineStudioAuth` now wraps `@convex-dev/auth`'s plain-`Error`
53
+ * credential/env throws server-side in a `ConvexError` carrying a stable code
54
+ * in `.data` (`{"code":"invalid-credentials" | "too-many-attempts" |
55
+ * "account-exists" | "deployment-misconfigured"}`). The data-first text folds
56
+ * that JSON in, so the SAME ladder classifies the codes (they survive prod
57
+ * redaction) AND the raw dev-shape messages — a deployment whose functions
58
+ * predate EC-363 still classifies exactly as before (dev: full message; prod:
59
+ * the honest generic below).
44
60
  *
45
61
  * Order matters: a DEPLOYMENT-misconfiguration (EC-257) and an uninvited
46
62
  * sign-up (EC-324) are each checked BEFORE the credential regex so neither
47
63
  * degrades to the lying "password too short". The final fallback is HONEST
48
64
  * ("unexpected server error — check the deployment logs"), never "unavailable":
49
- * on a prod deployment the credential/`InvalidAccountId`/env errors thrown by
50
- * `@convex-dev/auth` are PLAIN `Error`s (not `ConvexError`s), so prod redacts
51
- * them to `"Server Error"` with no `.data` and they legitimately land here.
65
+ * unknown errors — and any plain `Error` a pre-EC-363 prod deployment redacts
66
+ * to `"Server Error"` with no `.data` — legitimately land here.
52
67
  */
53
68
  export declare function classifySignInError(flow: 'signIn' | 'signUp', text: string): SignInFailure;
54
69
  type ChangePasswordFailure = Extract<ChangePasswordResult, {
@@ -26,14 +26,18 @@ const TOKEN_STORAGE_PREFIX = 'elytra.convex-auth.jwt';
26
26
  * Classify a thrown sign-in error as a DEPLOYMENT-setup failure rather than a
27
27
  * credential problem (EC-257). Matches the Convex Auth signing-path throws:
28
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").
29
+ * variable …") and `importPKCS8` on a malformed key — plus the EC-363 server code
30
+ * `deployment-misconfigured` (`defineStudioAuth` wraps those library throws in a
31
+ * `ConvexError` whose `.data` survives prod redaction, so this classifies on
32
+ * PRODUCTION deployments too once their functions are redeployed). Deliberately
33
+ * narrow — a genuine wrong-password error carries none of these tokens — and checked
34
+ * BEFORE the credential regex so it always wins. On a prod deployment whose
35
+ * functions PREDATE EC-363 the plain-`Error` message is redacted to "Server Error",
36
+ * which this can't match; there the honest fallback is the neutral "unavailable"
37
+ * branch (never "password too short").
34
38
  */
35
39
  export function isDeploymentMisconfigured(message) {
36
- return /JWT_PRIVATE_KEY|JWKS|CONVEX_SITE_URL|Missing environment variable|PKCS8|private key/i.test(message);
40
+ return /JWT_PRIVATE_KEY|JWKS|CONVEX_SITE_URL|Missing environment variable|PKCS8|private key|deployment-misconfigured/i.test(message);
37
41
  }
38
42
  /**
39
43
  * Reduce a thrown Convex error to the text used for classification (EC-362).
@@ -48,9 +52,11 @@ export function isDeploymentMisconfigured(message) {
48
52
  * (`dist/esm/browser/http_client.js`, ~L388-396 + L459-461). The server keeps
49
53
  * `errorData` for a `ConvexError` in prod while redacting `errorMessage`. So the
50
54
  * only classifiable text that survives prod is the `.data` of a `ConvexError` —
51
- * e.g. our invite gate's `ConvexError(NOT_INVITED_REASON)`. On DEV deployments
52
- * nothing is redacted, so `.message` also carries the token (the regex ladder
53
- * still matches); non-Convex errors only have a `.message`.
55
+ * our invite gate's `ConvexError(NOT_INVITED_REASON)` and, since EC-363, the
56
+ * coded `ConvexError({ code })` that `defineStudioAuth` wraps around the
57
+ * library's credential/env throws. On DEV deployments nothing is redacted, so
58
+ * `.message` also carries the token (the regex ladder still matches);
59
+ * non-Convex errors only have a `.message`.
54
60
  *
55
61
  * Duck-types the `data` property rather than `instanceof ConvexError`: two
56
62
  * copies of the `convex` module can coexist under Vite (transformed vs
@@ -82,16 +88,25 @@ export function convexErrorClassifierText(error) {
82
88
  return parts.join('\n');
83
89
  }
84
90
  /**
85
- * Pure sign-in/sign-up error classifier (EC-257 / EC-324 / EC-362). Runs the
86
- * regex ladder over {@link convexErrorClassifierText} output (data-first).
91
+ * Pure sign-in/sign-up error classifier (EC-257 / EC-324 / EC-362 / EC-363).
92
+ * Runs the regex ladder over {@link convexErrorClassifierText} output
93
+ * (data-first).
94
+ *
95
+ * EC-363: `defineStudioAuth` now wraps `@convex-dev/auth`'s plain-`Error`
96
+ * credential/env throws server-side in a `ConvexError` carrying a stable code
97
+ * in `.data` (`{"code":"invalid-credentials" | "too-many-attempts" |
98
+ * "account-exists" | "deployment-misconfigured"}`). The data-first text folds
99
+ * that JSON in, so the SAME ladder classifies the codes (they survive prod
100
+ * redaction) AND the raw dev-shape messages — a deployment whose functions
101
+ * predate EC-363 still classifies exactly as before (dev: full message; prod:
102
+ * the honest generic below).
87
103
  *
88
104
  * Order matters: a DEPLOYMENT-misconfiguration (EC-257) and an uninvited
89
105
  * sign-up (EC-324) are each checked BEFORE the credential regex so neither
90
106
  * degrades to the lying "password too short". The final fallback is HONEST
91
107
  * ("unexpected server error — check the deployment logs"), never "unavailable":
92
- * on a prod deployment the credential/`InvalidAccountId`/env errors thrown by
93
- * `@convex-dev/auth` are PLAIN `Error`s (not `ConvexError`s), so prod redacts
94
- * them to `"Server Error"` with no `.data` and they legitimately land here.
108
+ * unknown errors — and any plain `Error` a pre-EC-363 prod deployment redacts
109
+ * to `"Server Error"` with no `.data` — legitimately land here.
95
110
  */
96
111
  export function classifySignInError(flow, text) {
97
112
  // EC-257: a DEPLOYMENT-misconfiguration — its auth signing key is unset — must be
@@ -117,6 +132,20 @@ export function classifySignInError(flow, text) {
117
132
  message: 'This deployment is invite-only. Ask an admin to invite your email, then create your account.',
118
133
  };
119
134
  }
135
+ // EC-363: the sign-in rate limit. New deployments code it server-side
136
+ // (`{"code":"too-many-attempts"}` survives prod redaction); dev/old
137
+ // deployments surface the library's plain `TooManyFailedAttempts`. Checked
138
+ // BEFORE the credential regex; truthful wait-and-retry copy on the existing
139
+ // `unavailable` reason (a throttled deployment "can't sign you in right
140
+ // now" — the adapter contract gains no new reason for a transient state).
141
+ if (/too-many-attempts|TooManyFailedAttempts/i.test(text)) {
142
+ return {
143
+ ok: false,
144
+ reason: 'unavailable',
145
+ message: 'Too many failed sign-in attempts — wait a moment, then try again.',
146
+ };
147
+ }
148
+ // The EC-363 code `account-exists` matches via `exists`.
120
149
  if (flow === 'signUp' && /already|exists|InvalidAccountId/i.test(text)) {
121
150
  return {
122
151
  ok: false,
@@ -124,6 +153,7 @@ export function classifySignInError(flow, text) {
124
153
  message: 'An account with this email already exists — sign in instead.',
125
154
  };
126
155
  }
156
+ // The EC-363 code `invalid-credentials` matches via `credential`.
127
157
  if (/InvalidSecret|InvalidAccountId|credential|password/i.test(text)) {
128
158
  return {
129
159
  ok: false,
@@ -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, 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 * Reduce a thrown Convex error to the text used for classification (EC-362).\n *\n * **Read `.data` FIRST, then `.message`** — because a PRODUCTION Convex\n * deployment REDACTS a UDF's thrown-error `.message` to a generic\n * `\"[Request ID: …] Server Error\"`, but a `ConvexError`'s `.data` payload\n * survives to the client. Proven in the installed `convex@1.42.1` browser\n * client: on an `\"error\"` response `ConvexHttpClient.action` throws\n * `forwardErrorData(respJSON.errorData, new ConvexError(respJSON.errorMessage))`,\n * and `forwardErrorData` sets `error.data = jsonToConvex(errorData)`\n * (`dist/esm/browser/http_client.js`, ~L388-396 + L459-461). The server keeps\n * `errorData` for a `ConvexError` in prod while redacting `errorMessage`. So the\n * only classifiable text that survives prod is the `.data` of a `ConvexError` —\n * e.g. our invite gate's `ConvexError(NOT_INVITED_REASON)`. On DEV deployments\n * nothing is redacted, so `.message` also carries the token (the regex ladder\n * still matches); non-Convex errors only have a `.message`.\n *\n * Duck-types the `data` property rather than `instanceof ConvexError`: two\n * copies of the `convex` module can coexist under Vite (transformed vs\n * pre-bundled), so `instanceof` can spuriously fail (EC-357). `.data` may be a\n * string (our gates) or a structured object (future gates) — both are folded in.\n */\nexport function convexErrorClassifierText(error: unknown): string {\n const parts: string[] = []\n if (typeof error === 'object' && error !== null && 'data' in error) {\n const data = (error as { data: unknown }).data\n if (typeof data === 'string') {\n parts.push(data)\n } else if (data !== null && data !== undefined) {\n try {\n parts.push(JSON.stringify(data))\n } catch {\n // Non-serializable data (cycles) — ignore; fall back to the message.\n }\n }\n }\n if (error instanceof Error) parts.push(error.message)\n else if (typeof error === 'string') parts.push(error)\n if (parts.length === 0) parts.push(String(error))\n return parts.join('\\n')\n}\n\ntype SignInFailure = Extract<SignInResult, { ok: false }>\n\n/**\n * Pure sign-in/sign-up error classifier (EC-257 / EC-324 / EC-362). Runs the\n * regex ladder over {@link convexErrorClassifierText} output (data-first).\n *\n * Order matters: a DEPLOYMENT-misconfiguration (EC-257) and an uninvited\n * sign-up (EC-324) are each checked BEFORE the credential regex so neither\n * degrades to the lying \"password too short\". The final fallback is HONEST\n * (\"unexpected server error — check the deployment logs\"), never \"unavailable\":\n * on a prod deployment the credential/`InvalidAccountId`/env errors thrown by\n * `@convex-dev/auth` are PLAIN `Error`s (not `ConvexError`s), so prod redacts\n * them to `\"Server Error\"` with no `.data` and they legitimately land here.\n */\nexport function classifySignInError(flow: 'signIn' | 'signUp', text: string): SignInFailure {\n // EC-257: a DEPLOYMENT-misconfiguration — its auth signing key is unset — 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` never\n // run) the `requireEnv`/`importPKCS8` call throws AFTER the password is validated,\n // so EVERY sign-in fails regardless of the password.\n if (isDeploymentMisconfigured(text)) {\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 (`gateAccountCreation`/\n // `gateSessionCreation` throw `ConvexError(NOT_INVITED_REASON)`, whose `.data`\n // survives to the client even in production). Checked BEFORE the credential regex.\n if (/not-invited|invite-only|invite only/i.test(text)) {\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(text)) {\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(text)) {\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 {\n ok: false,\n reason: 'unavailable',\n message: `${verb} failed (unexpected server error) — check the deployment logs.`,\n }\n}\n\ntype ChangePasswordFailure = Extract<ChangePasswordResult, { ok: false }>\n\n/**\n * Pure password-change error classifier (EC-240 H-tail / EC-362). Same\n * data-first text + honest fallback discipline as {@link classifySignInError};\n * `deployment-misconfigured` collapses onto the `unavailable` reason\n * (`ChangePasswordResult` has no dedicated reason for it) with a specific\n * remediation message.\n */\nexport function classifyChangePasswordError(text: string): ChangePasswordFailure {\n if (isDeploymentMisconfigured(text)) {\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(text)) {\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(text)) {\n return {\n ok: false,\n reason: 'weak-password',\n message: 'Choose a stronger password (at least 8 characters).',\n }\n }\n return {\n ok: false,\n reason: 'unavailable',\n message: 'Password change failed (unexpected server error) — check the deployment logs.',\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 // Classify from the DATA-FIRST text (a `ConvexError`'s `.data` survives prod\n // redaction, the redacted `.message` does not — EC-362), then apply the pure\n // regex ladder. See `convexErrorClassifierText` / `classifySignInError`.\n return classifySignInError(flow, convexErrorClassifierText(error))\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 // Data-first classification (EC-362): the change-password ACTION runs on the\n // same deployment, so a prod redaction erases the thrown `.message` the same\n // way — read a surviving `ConvexError.data` first.\n return classifyChangePasswordError(convexErrorClassifierText(error))\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
+ {"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 — plus the EC-363 server code\n * `deployment-misconfigured` (`defineStudioAuth` wraps those library throws in a\n * `ConvexError` whose `.data` survives prod redaction, so this classifies on\n * PRODUCTION deployments too once their functions are redeployed). Deliberately\n * narrow — a genuine wrong-password error carries none of these tokens — and checked\n * BEFORE the credential regex so it always wins. On a prod deployment whose\n * functions PREDATE EC-363 the plain-`Error` message is redacted to \"Server Error\",\n * which this can't match; there the honest fallback is the neutral \"unavailable\"\n * 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|deployment-misconfigured/i.test(\n message,\n )\n}\n\n/**\n * Reduce a thrown Convex error to the text used for classification (EC-362).\n *\n * **Read `.data` FIRST, then `.message`** — because a PRODUCTION Convex\n * deployment REDACTS a UDF's thrown-error `.message` to a generic\n * `\"[Request ID: …] Server Error\"`, but a `ConvexError`'s `.data` payload\n * survives to the client. Proven in the installed `convex@1.42.1` browser\n * client: on an `\"error\"` response `ConvexHttpClient.action` throws\n * `forwardErrorData(respJSON.errorData, new ConvexError(respJSON.errorMessage))`,\n * and `forwardErrorData` sets `error.data = jsonToConvex(errorData)`\n * (`dist/esm/browser/http_client.js`, ~L388-396 + L459-461). The server keeps\n * `errorData` for a `ConvexError` in prod while redacting `errorMessage`. So the\n * only classifiable text that survives prod is the `.data` of a `ConvexError` —\n * our invite gate's `ConvexError(NOT_INVITED_REASON)` and, since EC-363, the\n * coded `ConvexError({ code })` that `defineStudioAuth` wraps around the\n * library's credential/env throws. On DEV deployments nothing is redacted, so\n * `.message` also carries the token (the regex ladder still matches);\n * non-Convex errors only have a `.message`.\n *\n * Duck-types the `data` property rather than `instanceof ConvexError`: two\n * copies of the `convex` module can coexist under Vite (transformed vs\n * pre-bundled), so `instanceof` can spuriously fail (EC-357). `.data` may be a\n * string (our gates) or a structured object (future gates) — both are folded in.\n */\nexport function convexErrorClassifierText(error: unknown): string {\n const parts: string[] = []\n if (typeof error === 'object' && error !== null && 'data' in error) {\n const data = (error as { data: unknown }).data\n if (typeof data === 'string') {\n parts.push(data)\n } else if (data !== null && data !== undefined) {\n try {\n parts.push(JSON.stringify(data))\n } catch {\n // Non-serializable data (cycles) — ignore; fall back to the message.\n }\n }\n }\n if (error instanceof Error) parts.push(error.message)\n else if (typeof error === 'string') parts.push(error)\n if (parts.length === 0) parts.push(String(error))\n return parts.join('\\n')\n}\n\ntype SignInFailure = Extract<SignInResult, { ok: false }>\n\n/**\n * Pure sign-in/sign-up error classifier (EC-257 / EC-324 / EC-362 / EC-363).\n * Runs the regex ladder over {@link convexErrorClassifierText} output\n * (data-first).\n *\n * EC-363: `defineStudioAuth` now wraps `@convex-dev/auth`'s plain-`Error`\n * credential/env throws server-side in a `ConvexError` carrying a stable code\n * in `.data` (`{\"code\":\"invalid-credentials\" | \"too-many-attempts\" |\n * \"account-exists\" | \"deployment-misconfigured\"}`). The data-first text folds\n * that JSON in, so the SAME ladder classifies the codes (they survive prod\n * redaction) AND the raw dev-shape messages — a deployment whose functions\n * predate EC-363 still classifies exactly as before (dev: full message; prod:\n * the honest generic below).\n *\n * Order matters: a DEPLOYMENT-misconfiguration (EC-257) and an uninvited\n * sign-up (EC-324) are each checked BEFORE the credential regex so neither\n * degrades to the lying \"password too short\". The final fallback is HONEST\n * (\"unexpected server error — check the deployment logs\"), never \"unavailable\":\n * unknown errors — and any plain `Error` a pre-EC-363 prod deployment redacts\n * to `\"Server Error\"` with no `.data` — legitimately land here.\n */\nexport function classifySignInError(flow: 'signIn' | 'signUp', text: string): SignInFailure {\n // EC-257: a DEPLOYMENT-misconfiguration — its auth signing key is unset — 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` never\n // run) the `requireEnv`/`importPKCS8` call throws AFTER the password is validated,\n // so EVERY sign-in fails regardless of the password.\n if (isDeploymentMisconfigured(text)) {\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 (`gateAccountCreation`/\n // `gateSessionCreation` throw `ConvexError(NOT_INVITED_REASON)`, whose `.data`\n // survives to the client even in production). Checked BEFORE the credential regex.\n if (/not-invited|invite-only|invite only/i.test(text)) {\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 // EC-363: the sign-in rate limit. New deployments code it server-side\n // (`{\"code\":\"too-many-attempts\"}` survives prod redaction); dev/old\n // deployments surface the library's plain `TooManyFailedAttempts`. Checked\n // BEFORE the credential regex; truthful wait-and-retry copy on the existing\n // `unavailable` reason (a throttled deployment \"can't sign you in right\n // now\" — the adapter contract gains no new reason for a transient state).\n if (/too-many-attempts|TooManyFailedAttempts/i.test(text)) {\n return {\n ok: false,\n reason: 'unavailable',\n message: 'Too many failed sign-in attempts — wait a moment, then try again.',\n }\n }\n // The EC-363 code `account-exists` matches via `exists`.\n if (flow === 'signUp' && /already|exists|InvalidAccountId/i.test(text)) {\n return {\n ok: false,\n reason: 'account-exists',\n message: 'An account with this email already exists — sign in instead.',\n }\n }\n // The EC-363 code `invalid-credentials` matches via `credential`.\n if (/InvalidSecret|InvalidAccountId|credential|password/i.test(text)) {\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 {\n ok: false,\n reason: 'unavailable',\n message: `${verb} failed (unexpected server error) — check the deployment logs.`,\n }\n}\n\ntype ChangePasswordFailure = Extract<ChangePasswordResult, { ok: false }>\n\n/**\n * Pure password-change error classifier (EC-240 H-tail / EC-362). Same\n * data-first text + honest fallback discipline as {@link classifySignInError};\n * `deployment-misconfigured` collapses onto the `unavailable` reason\n * (`ChangePasswordResult` has no dedicated reason for it) with a specific\n * remediation message.\n */\nexport function classifyChangePasswordError(text: string): ChangePasswordFailure {\n if (isDeploymentMisconfigured(text)) {\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(text)) {\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(text)) {\n return {\n ok: false,\n reason: 'weak-password',\n message: 'Choose a stronger password (at least 8 characters).',\n }\n }\n return {\n ok: false,\n reason: 'unavailable',\n message: 'Password change failed (unexpected server error) — check the deployment logs.',\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 // Classify from the DATA-FIRST text (a `ConvexError`'s `.data` survives prod\n // redaction, the redacted `.message` does not — EC-362), then apply the pure\n // regex ladder. See `convexErrorClassifierText` / `classifySignInError`.\n return classifySignInError(flow, convexErrorClassifierText(error))\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 // Data-first classification (EC-362): the change-password ACTION runs on the\n // same deployment, so a prod redaction erases the thrown `.message` the same\n // way — read a surviving `ConvexError.data` first.\n return classifyChangePasswordError(convexErrorClassifierText(error))\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"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elytracms/studio",
3
3
  "author": "finaldream <hi@finaldream.de>",
4
- "version": "0.0.55",
4
+ "version": "0.0.56",
5
5
  "private": false,
6
6
  "description": "The Elytra studio engine (UI + lib + routes + data-only Convex schema) as an importable package. Extraction staged in EXTRACTION.md (EC-223); Stages 1-2 = UI primitives + router-free lib leaves.",
7
7
  "license": "MIT",
@@ -141,7 +141,7 @@
141
141
  "lucide-react": "^1.23.0",
142
142
  "tailwind-merge": "^3.6.0",
143
143
  "zod": "^4.4.3",
144
- "@elytracms/core": "^0.0.55"
144
+ "@elytracms/core": "^0.0.56"
145
145
  },
146
146
  "peerDependencies": {
147
147
  "@tanstack/react-router": "^1.170.0",