@robelest/convex-auth 0.0.2 → 0.0.3-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/dist/bin.cjs +1 -1
  2. package/dist/client/index.d.ts +33 -9
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +79 -13
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/component.d.ts +48 -0
  7. package/dist/component/_generated/component.d.ts.map +1 -1
  8. package/dist/component/index.d.ts +10 -4
  9. package/dist/component/index.d.ts.map +1 -1
  10. package/dist/component/index.js +8 -3
  11. package/dist/component/index.js.map +1 -1
  12. package/dist/component/public.d.ts +163 -3
  13. package/dist/component/public.d.ts.map +1 -1
  14. package/dist/component/public.js +124 -0
  15. package/dist/component/public.js.map +1 -1
  16. package/dist/component/schema.d.ts +81 -2
  17. package/dist/component/schema.d.ts.map +1 -1
  18. package/dist/component/schema.js +45 -0
  19. package/dist/component/schema.js.map +1 -1
  20. package/dist/providers/anonymous.d.ts +3 -0
  21. package/dist/providers/anonymous.d.ts.map +1 -1
  22. package/dist/providers/anonymous.js +3 -0
  23. package/dist/providers/anonymous.js.map +1 -1
  24. package/dist/providers/credentials.d.ts +3 -0
  25. package/dist/providers/credentials.d.ts.map +1 -1
  26. package/dist/providers/credentials.js +3 -0
  27. package/dist/providers/credentials.js.map +1 -1
  28. package/dist/providers/email.d.ts +3 -0
  29. package/dist/providers/email.d.ts.map +1 -1
  30. package/dist/providers/email.js +3 -0
  31. package/dist/providers/email.js.map +1 -1
  32. package/dist/providers/passkey.d.ts +7 -1
  33. package/dist/providers/passkey.d.ts.map +1 -1
  34. package/dist/providers/passkey.js +7 -1
  35. package/dist/providers/passkey.js.map +1 -1
  36. package/dist/providers/password.d.ts +3 -0
  37. package/dist/providers/password.d.ts.map +1 -1
  38. package/dist/providers/password.js +3 -0
  39. package/dist/providers/password.js.map +1 -1
  40. package/dist/providers/phone.d.ts +3 -0
  41. package/dist/providers/phone.d.ts.map +1 -1
  42. package/dist/providers/phone.js +3 -0
  43. package/dist/providers/phone.js.map +1 -1
  44. package/dist/providers/totp.d.ts +8 -0
  45. package/dist/providers/totp.d.ts.map +1 -1
  46. package/dist/providers/totp.js +8 -0
  47. package/dist/providers/totp.js.map +1 -1
  48. package/dist/server/convex-auth.d.ts +185 -25
  49. package/dist/server/convex-auth.d.ts.map +1 -1
  50. package/dist/server/convex-auth.js +317 -58
  51. package/dist/server/convex-auth.js.map +1 -1
  52. package/dist/server/email-templates.d.ts +18 -0
  53. package/dist/server/email-templates.d.ts.map +1 -0
  54. package/dist/server/email-templates.js +74 -0
  55. package/dist/server/email-templates.js.map +1 -0
  56. package/dist/server/errors.d.ts +146 -0
  57. package/dist/server/errors.d.ts.map +1 -0
  58. package/dist/server/errors.js +176 -0
  59. package/dist/server/errors.js.map +1 -0
  60. package/dist/server/implementation/apiKey.d.ts +74 -0
  61. package/dist/server/implementation/apiKey.d.ts.map +1 -0
  62. package/dist/server/implementation/apiKey.js +139 -0
  63. package/dist/server/implementation/apiKey.js.map +1 -0
  64. package/dist/server/implementation/index.d.ts +151 -14
  65. package/dist/server/implementation/index.d.ts.map +1 -1
  66. package/dist/server/implementation/index.js +216 -24
  67. package/dist/server/implementation/index.js.map +1 -1
  68. package/dist/server/implementation/mutations/createAccountFromCredentials.d.ts.map +1 -1
  69. package/dist/server/implementation/mutations/createAccountFromCredentials.js +2 -1
  70. package/dist/server/implementation/mutations/createAccountFromCredentials.js.map +1 -1
  71. package/dist/server/implementation/mutations/createVerificationCode.d.ts +2 -2
  72. package/dist/server/implementation/mutations/index.d.ts +6 -6
  73. package/dist/server/implementation/mutations/modifyAccount.d.ts.map +1 -1
  74. package/dist/server/implementation/mutations/modifyAccount.js +2 -1
  75. package/dist/server/implementation/mutations/modifyAccount.js.map +1 -1
  76. package/dist/server/implementation/mutations/userOAuth.d.ts.map +1 -1
  77. package/dist/server/implementation/mutations/userOAuth.js +2 -1
  78. package/dist/server/implementation/mutations/userOAuth.js.map +1 -1
  79. package/dist/server/implementation/mutations/verifierSignature.d.ts.map +1 -1
  80. package/dist/server/implementation/mutations/verifierSignature.js +2 -1
  81. package/dist/server/implementation/mutations/verifierSignature.js.map +1 -1
  82. package/dist/server/implementation/passkey.d.ts.map +1 -1
  83. package/dist/server/implementation/passkey.js +28 -29
  84. package/dist/server/implementation/passkey.js.map +1 -1
  85. package/dist/server/implementation/provider.d.ts.map +1 -1
  86. package/dist/server/implementation/provider.js +5 -4
  87. package/dist/server/implementation/provider.js.map +1 -1
  88. package/dist/server/implementation/redirects.d.ts.map +1 -1
  89. package/dist/server/implementation/redirects.js +2 -1
  90. package/dist/server/implementation/redirects.js.map +1 -1
  91. package/dist/server/implementation/refreshTokens.d.ts.map +1 -1
  92. package/dist/server/implementation/refreshTokens.js +2 -1
  93. package/dist/server/implementation/refreshTokens.js.map +1 -1
  94. package/dist/server/implementation/signIn.d.ts.map +1 -1
  95. package/dist/server/implementation/signIn.js +8 -18
  96. package/dist/server/implementation/signIn.js.map +1 -1
  97. package/dist/server/implementation/totp.d.ts.map +1 -1
  98. package/dist/server/implementation/totp.js +16 -17
  99. package/dist/server/implementation/totp.js.map +1 -1
  100. package/dist/server/implementation/users.d.ts.map +1 -1
  101. package/dist/server/implementation/users.js +3 -2
  102. package/dist/server/implementation/users.js.map +1 -1
  103. package/dist/server/index.d.ts +157 -3
  104. package/dist/server/index.d.ts.map +1 -1
  105. package/dist/server/index.js +180 -17
  106. package/dist/server/index.js.map +1 -1
  107. package/dist/server/oauth/authorizationUrl.d.ts.map +1 -1
  108. package/dist/server/oauth/authorizationUrl.js +2 -1
  109. package/dist/server/oauth/authorizationUrl.js.map +1 -1
  110. package/dist/server/oauth/callback.d.ts.map +1 -1
  111. package/dist/server/oauth/callback.js +5 -4
  112. package/dist/server/oauth/callback.js.map +1 -1
  113. package/dist/server/oauth/checks.d.ts.map +1 -1
  114. package/dist/server/oauth/checks.js +2 -1
  115. package/dist/server/oauth/checks.js.map +1 -1
  116. package/dist/server/oauth/convexAuth.d.ts.map +1 -1
  117. package/dist/server/oauth/convexAuth.js +3 -2
  118. package/dist/server/oauth/convexAuth.js.map +1 -1
  119. package/dist/server/provider_utils.d.ts +2 -0
  120. package/dist/server/provider_utils.d.ts.map +1 -1
  121. package/dist/server/types.d.ts +240 -5
  122. package/dist/server/types.d.ts.map +1 -1
  123. package/dist/server/utils.d.ts.map +1 -1
  124. package/dist/server/utils.js +2 -1
  125. package/dist/server/utils.js.map +1 -1
  126. package/dist/server/version.d.ts +2 -0
  127. package/dist/server/version.d.ts.map +1 -0
  128. package/dist/server/version.js +3 -0
  129. package/dist/server/version.js.map +1 -0
  130. package/package.json +7 -2
  131. package/src/cli/index.ts +1 -1
  132. package/src/cli/utils.ts +248 -0
  133. package/src/client/index.ts +105 -15
  134. package/src/component/_generated/component.ts +61 -0
  135. package/src/component/index.ts +11 -2
  136. package/src/component/public.ts +142 -0
  137. package/src/component/schema.ts +52 -0
  138. package/src/providers/anonymous.ts +3 -0
  139. package/src/providers/credentials.ts +3 -0
  140. package/src/providers/email.ts +3 -0
  141. package/src/providers/passkey.ts +8 -1
  142. package/src/providers/password.ts +3 -0
  143. package/src/providers/phone.ts +3 -0
  144. package/src/providers/totp.ts +9 -0
  145. package/src/server/convex-auth.ts +385 -73
  146. package/src/server/email-templates.ts +77 -0
  147. package/src/server/errors.ts +269 -0
  148. package/src/server/implementation/apiKey.ts +186 -0
  149. package/src/server/implementation/index.ts +288 -28
  150. package/src/server/implementation/mutations/createAccountFromCredentials.ts +2 -1
  151. package/src/server/implementation/mutations/modifyAccount.ts +2 -3
  152. package/src/server/implementation/mutations/userOAuth.ts +2 -1
  153. package/src/server/implementation/mutations/verifierSignature.ts +2 -1
  154. package/src/server/implementation/passkey.ts +33 -35
  155. package/src/server/implementation/provider.ts +5 -8
  156. package/src/server/implementation/redirects.ts +2 -3
  157. package/src/server/implementation/refreshTokens.ts +2 -1
  158. package/src/server/implementation/signIn.ts +9 -18
  159. package/src/server/implementation/totp.ts +18 -21
  160. package/src/server/implementation/users.ts +4 -7
  161. package/src/server/index.ts +240 -37
  162. package/src/server/oauth/authorizationUrl.ts +2 -1
  163. package/src/server/oauth/callback.ts +5 -4
  164. package/src/server/oauth/checks.ts +3 -1
  165. package/src/server/oauth/convexAuth.ts +6 -3
  166. package/src/server/types.ts +254 -5
  167. package/src/server/utils.ts +3 -1
  168. package/src/server/version.ts +2 -0
  169. package/dist/server/portal.d.ts +0 -116
  170. package/dist/server/portal.d.ts.map +0 -1
  171. package/dist/server/portal.js +0 -294
  172. package/dist/server/portal.js.map +0 -1
  173. package/src/server/portal.ts +0 -375
@@ -48,6 +48,7 @@ import { AuthDataModel, SessionInfo } from "./types.js";
48
48
  import { callSignIn, callVerifier } from "./mutations/index.js";
49
49
  import { callVerifierSignature } from "./mutations/verifierSignature.js";
50
50
  import { authDb } from "./db.js";
51
+ import { throwAuthError } from "../errors.js";
51
52
 
52
53
 
53
54
  type EnrichedActionCtx = GenericActionCtxWithAuthConfig<AuthDataModel>;
@@ -64,7 +65,8 @@ function resolveRpOptions(provider: PasskeyProviderConfig) {
64
65
  // because the RP ID wouldn't match the page origin.
65
66
  const siteUrl = process.env.SITE_URL;
66
67
  if (!siteUrl && !provider.options.rpId) {
67
- throw new Error(
68
+ throwAuthError(
69
+ "PASSKEY_MISSING_CONFIG",
68
70
  "Passkey provider requires SITE_URL env var (your frontend URL) " +
69
71
  "or explicit rpId / origin in the provider config. " +
70
72
  "CONVEX_SITE_URL cannot be used because WebAuthn RP ID must match the frontend domain.",
@@ -124,10 +126,7 @@ async function handleRegisterOptions(
124
126
  // Passkey registration requires an authenticated user
125
127
  const identity = await ctx.auth.getUserIdentity();
126
128
  if (identity === null) {
127
- throw new Error(
128
- "Passkey registration requires an authenticated user. " +
129
- "Sign in first, then add a passkey to your account.",
130
- );
129
+ throwAuthError("PASSKEY_AUTH_REQUIRED");
131
130
  }
132
131
  const [userId] = identity.subject.split("|");
133
132
 
@@ -215,17 +214,14 @@ async function handleRegisterVerify(
215
214
  // Passkey registration requires an authenticated user
216
215
  const identity = await ctx.auth.getUserIdentity();
217
216
  if (identity === null) {
218
- throw new Error(
219
- "Passkey registration requires an authenticated user. " +
220
- "Sign in first, then add a passkey to your account.",
221
- );
217
+ throwAuthError("PASSKEY_AUTH_REQUIRED");
222
218
  }
223
219
  const [userId] = identity.subject.split("|");
224
220
 
225
221
  const rp = resolveRpOptions(provider);
226
222
 
227
223
  if (!verifierValue) {
228
- throw new Error("Missing verifier for passkey registration");
224
+ throwAuthError("PASSKEY_MISSING_VERIFIER");
229
225
  }
230
226
 
231
227
  // Decode client data
@@ -234,13 +230,14 @@ async function handleRegisterVerify(
234
230
 
235
231
  // Verify client data type is "webauthn.create"
236
232
  if (clientData.type !== ClientDataType.Create) {
237
- throw new Error("Invalid client data type: expected webauthn.create");
233
+ throwAuthError("PASSKEY_INVALID_CLIENT_DATA", "Invalid client data type: expected webauthn.create");
238
234
  }
239
235
 
240
236
  // Verify origin
241
237
  const allowedOrigins = Array.isArray(rp.origin) ? rp.origin : [rp.origin];
242
238
  if (!allowedOrigins.includes(clientData.origin)) {
243
- throw new Error(
239
+ throwAuthError(
240
+ "PASSKEY_INVALID_ORIGIN",
244
241
  `Invalid origin: ${clientData.origin}, expected one of: ${allowedOrigins.join(", ")}`,
245
242
  );
246
243
  }
@@ -254,7 +251,7 @@ async function handleRegisterVerify(
254
251
  { verifierId: verifierValue },
255
252
  );
256
253
  if (!verifierDoc || (verifierDoc as any).signature !== challengeHash) {
257
- throw new Error("Invalid or expired challenge");
254
+ throwAuthError("PASSKEY_INVALID_CHALLENGE");
258
255
  }
259
256
 
260
257
  // Clean up the verifier
@@ -270,21 +267,21 @@ async function handleRegisterVerify(
270
267
 
271
268
  // Verify RP ID hash
272
269
  if (!authenticatorData.verifyRelyingPartyIdHash(rp.rpId)) {
273
- throw new Error("Relying party ID mismatch");
270
+ throwAuthError("PASSKEY_RP_MISMATCH");
274
271
  }
275
272
 
276
273
  // Verify user presence and verification flags
277
274
  if (!authenticatorData.userPresent) {
278
- throw new Error("User presence flag not set");
275
+ throwAuthError("PASSKEY_USER_PRESENCE");
279
276
  }
280
277
  if (rp.userVerification === "required" && !authenticatorData.userVerified) {
281
- throw new Error("User verification required but not performed");
278
+ throwAuthError("PASSKEY_USER_VERIFICATION");
282
279
  }
283
280
 
284
281
  // Extract credential
285
282
  const credential = authenticatorData.credential;
286
283
  if (!credential) {
287
- throw new Error("No credential in attestation");
284
+ throwAuthError("PASSKEY_NO_CREDENTIAL");
288
285
  }
289
286
 
290
287
  const credentialId = encodeBase64urlNoPadding(credential.id);
@@ -320,7 +317,7 @@ async function handleRegisterVerify(
320
317
  const rsaPubKey = new RSAPublicKey(rsa.n, rsa.e);
321
318
  publicKeyBytes = rsaPubKey.encodePKCS1();
322
319
  } else {
323
- throw new Error(`Unsupported algorithm: ${algorithm}`);
320
+ throwAuthError("PASSKEY_UNSUPPORTED_ALGORITHM", `Unsupported algorithm: ${algorithm}`);
324
321
  }
325
322
 
326
323
  const deviceType = params.deviceType ?? "single-device";
@@ -447,7 +444,7 @@ async function handleAuthVerify(
447
444
  const rp = resolveRpOptions(provider);
448
445
 
449
446
  if (!verifierValue) {
450
- throw new Error("Missing verifier for passkey authentication");
447
+ throwAuthError("PASSKEY_MISSING_VERIFIER");
451
448
  }
452
449
 
453
450
  // Decode client data
@@ -456,13 +453,14 @@ async function handleAuthVerify(
456
453
 
457
454
  // Verify client data type is "webauthn.get"
458
455
  if (clientData.type !== ClientDataType.Get) {
459
- throw new Error("Invalid client data type: expected webauthn.get");
456
+ throwAuthError("PASSKEY_INVALID_CLIENT_DATA", "Invalid client data type: expected webauthn.get");
460
457
  }
461
458
 
462
459
  // Verify origin
463
460
  const allowedOrigins = Array.isArray(rp.origin) ? rp.origin : [rp.origin];
464
461
  if (!allowedOrigins.includes(clientData.origin)) {
465
- throw new Error(
462
+ throwAuthError(
463
+ "PASSKEY_INVALID_ORIGIN",
466
464
  `Invalid origin: ${clientData.origin}, expected one of: ${allowedOrigins.join(", ")}`,
467
465
  );
468
466
  }
@@ -476,7 +474,7 @@ async function handleAuthVerify(
476
474
  { verifierId: verifierValue },
477
475
  );
478
476
  if (!verifierDoc || (verifierDoc as any).signature !== challengeHash) {
479
- throw new Error("Invalid or expired challenge");
477
+ throwAuthError("PASSKEY_INVALID_CHALLENGE");
480
478
  }
481
479
 
482
480
  // Clean up the verifier
@@ -488,7 +486,7 @@ async function handleAuthVerify(
488
486
  // Look up the credential
489
487
  const credentialId = params.credentialId;
490
488
  if (!credentialId) {
491
- throw new Error("Missing credential ID");
489
+ throwAuthError("PASSKEY_UNKNOWN_CREDENTIAL", "Missing credential ID");
492
490
  }
493
491
 
494
492
  const passkeyDoc = await ctx.runQuery(
@@ -496,7 +494,7 @@ async function handleAuthVerify(
496
494
  { credentialId },
497
495
  );
498
496
  if (!passkeyDoc) {
499
- throw new Error("Unknown credential");
497
+ throwAuthError("PASSKEY_UNKNOWN_CREDENTIAL", "Unknown credential");
500
498
  }
501
499
  const passkey = passkeyDoc as any;
502
500
 
@@ -506,15 +504,15 @@ async function handleAuthVerify(
506
504
 
507
505
  // Verify RP ID hash
508
506
  if (!authenticatorData.verifyRelyingPartyIdHash(rp.rpId)) {
509
- throw new Error("Relying party ID mismatch");
507
+ throwAuthError("PASSKEY_RP_MISMATCH");
510
508
  }
511
509
 
512
510
  // Verify user presence
513
511
  if (!authenticatorData.userPresent) {
514
- throw new Error("User presence flag not set");
512
+ throwAuthError("PASSKEY_USER_PRESENCE");
515
513
  }
516
514
  if (rp.userVerification === "required" && !authenticatorData.userVerified) {
517
- throw new Error("User verification required but not performed");
515
+ throwAuthError("PASSKEY_USER_VERIFICATION");
518
516
  }
519
517
 
520
518
  // Verify signature
@@ -538,7 +536,7 @@ async function handleAuthVerify(
538
536
  ecdsaSignature,
539
537
  );
540
538
  if (!valid) {
541
- throw new Error("Invalid signature");
539
+ throwAuthError("PASSKEY_INVALID_SIGNATURE");
542
540
  }
543
541
  } else if (passkey.algorithm === coseAlgorithmRS256) {
544
542
  // RSA PKCS#1 v1.5 with SHA-256 verification
@@ -552,10 +550,10 @@ async function handleAuthVerify(
552
550
  signature,
553
551
  );
554
552
  if (!valid) {
555
- throw new Error("Invalid signature");
553
+ throwAuthError("PASSKEY_INVALID_SIGNATURE");
556
554
  }
557
555
  } else {
558
- throw new Error(`Unsupported algorithm: ${passkey.algorithm}`);
556
+ throwAuthError("PASSKEY_UNSUPPORTED_ALGORITHM", `Unsupported algorithm: ${passkey.algorithm}`);
559
557
  }
560
558
 
561
559
  // Verify counter (clone detection)
@@ -565,9 +563,7 @@ async function handleAuthVerify(
565
563
  authenticatorData.signatureCounter !== 0 &&
566
564
  authenticatorData.signatureCounter <= passkey.counter
567
565
  ) {
568
- throw new Error(
569
- "Authenticator counter did not increase — possible credential cloning detected",
570
- );
566
+ throwAuthError("PASSKEY_COUNTER_ERROR");
571
567
  }
572
568
 
573
569
  // Update counter and last used timestamp
@@ -611,7 +607,8 @@ export async function handlePasskey(
611
607
  > {
612
608
  const flow = args.params?.flow;
613
609
  if (!flow) {
614
- throw new Error(
610
+ throwAuthError(
611
+ "PASSKEY_MISSING_FLOW",
615
612
  "Missing `flow` parameter. Expected one of: register-options, register-verify, auth-options, auth-verify",
616
613
  );
617
614
  }
@@ -626,7 +623,8 @@ export async function handlePasskey(
626
623
  case "auth-verify":
627
624
  return handleAuthVerify(ctx, provider, args.params ?? {}, args.verifier);
628
625
  default:
629
- throw new Error(
626
+ throwAuthError(
627
+ "PASSKEY_UNKNOWN_FLOW",
630
628
  `Unknown passkey flow: ${flow}. Expected one of: register-options, register-verify, auth-options, auth-verify`,
631
629
  );
632
630
  }
@@ -1,15 +1,14 @@
1
1
  import { AuthProviderMaterializedConfig } from "../types.js";
2
2
  import { ConvexAuthMaterializedConfig } from "../types.js";
3
+ import { throwAuthError } from "../errors.js";
3
4
 
4
5
  export async function hash(provider: any, secret: string) {
5
6
  if (provider.type !== "credentials") {
6
- throw new Error(`Provider ${provider.id} is not a credentials provider`);
7
+ throwAuthError("INVALID_CREDENTIALS_PROVIDER", `Provider ${provider.id} is not a credentials provider`, { provider: provider.id });
7
8
  }
8
9
  const hashSecretFn = provider.crypto?.hashSecret;
9
10
  if (hashSecretFn === undefined) {
10
- throw new Error(
11
- `Provider ${provider.id} does not have a \`crypto.hashSecret\` function`,
12
- );
11
+ throwAuthError("MISSING_CRYPTO_FUNCTION", `Provider ${provider.id} does not have a \`crypto.hashSecret\` function`, { provider: provider.id });
13
12
  }
14
13
  return await hashSecretFn(secret);
15
14
  }
@@ -20,13 +19,11 @@ export async function verify(
20
19
  hash: string,
21
20
  ) {
22
21
  if (provider.type !== "credentials") {
23
- throw new Error(`Provider ${provider.id} is not a credentials provider`);
22
+ throwAuthError("INVALID_CREDENTIALS_PROVIDER", `Provider ${provider.id} is not a credentials provider`, { provider: provider.id });
24
23
  }
25
24
  const verifySecretFn = provider.crypto?.verifySecret;
26
25
  if (verifySecretFn === undefined) {
27
- throw new Error(
28
- `Provider ${provider.id} does not have a \`crypto.verifySecret\` function`,
29
- );
26
+ throwAuthError("MISSING_CRYPTO_FUNCTION", `Provider ${provider.id} does not have a \`crypto.verifySecret\` function`, { provider: provider.id });
30
27
  }
31
28
  return await verifySecretFn(secret, hash);
32
29
  }
@@ -1,5 +1,6 @@
1
1
  import { ConvexAuthMaterializedConfig } from "../types.js";
2
2
  import { requireEnv } from "../utils.js";
3
+ import { throwAuthError } from "../errors.js";
3
4
 
4
5
  export async function redirectAbsoluteUrl(
5
6
  config: ConvexAuthMaterializedConfig,
@@ -7,9 +8,7 @@ export async function redirectAbsoluteUrl(
7
8
  ) {
8
9
  if (params.redirectTo !== undefined) {
9
10
  if (typeof params.redirectTo !== "string") {
10
- throw new Error(
11
- `Expected \`redirectTo\` to be a string, got ${params.redirectTo as any}`,
12
- );
11
+ throwAuthError("INVALID_REDIRECT", `Expected \`redirectTo\` to be a string, got ${params.redirectTo as any}`);
13
12
  }
14
13
  const redirectCallback =
15
14
  config.callbacks?.redirect ?? defaultRedirectCallback;
@@ -1,5 +1,6 @@
1
1
  import { GenericId } from "convex/values";
2
2
  import { ConvexAuthConfig } from "../types.js";
3
+ import { throwAuthError } from "../errors.js";
3
4
  import { Doc, MutationCtx } from "./types.js";
4
5
  import {
5
6
  LOG_LEVELS,
@@ -47,7 +48,7 @@ export const parseRefreshToken = (
47
48
  } => {
48
49
  const [refreshTokenId, sessionId] = refreshToken.split(REFRESH_TOKEN_DIVIDER);
49
50
  if (!refreshTokenId || !sessionId) {
50
- throw new Error(`Can't parse refresh token: ${maybeRedact(refreshToken)}`);
51
+ throwAuthError("INVALID_REFRESH_TOKEN", `Can't parse refresh token: ${maybeRedact(refreshToken)}`);
51
52
  }
52
53
  return {
53
54
  refreshTokenId: refreshTokenId as GenericId<"token">,
@@ -28,6 +28,7 @@ import { OAuth2Config, OIDCConfig } from "@auth/core/providers/oauth.js";
28
28
  import { generateRandomString } from "./utils.js";
29
29
  import { handlePasskey } from "./passkey.js";
30
30
  import { handleTotp, checkTotpRequired } from "./totp.js";
31
+ import { throwAuthError } from "../errors.js";
31
32
 
32
33
  const DEFAULT_EMAIL_VERIFICATION_CODE_DURATION_S = 60 * 60 * 24; // 24 hours
33
34
 
@@ -82,9 +83,7 @@ export async function signInImpl(
82
83
  }
83
84
 
84
85
  if (provider === null) {
85
- throw new Error(
86
- "Cannot sign in: Missing `provider`, `params.code` or `refreshToken`",
87
- );
86
+ throwAuthError("SIGN_IN_MISSING_PARAMS");
88
87
  }
89
88
  if (provider.type === "email" || provider.type === "phone") {
90
89
  return handleEmailAndPhoneProvider(ctx, provider, args, options);
@@ -102,7 +101,8 @@ export async function signInImpl(
102
101
  return handleTotp(ctx, provider, args);
103
102
  }
104
103
  const _typecheck: never = provider;
105
- throw new Error(
104
+ throwAuthError(
105
+ "UNSUPPORTED_PROVIDER_TYPE",
106
106
  `Provider type ${(provider as any).type} is not supported yet`,
107
107
  );
108
108
  }
@@ -130,7 +130,7 @@ async function handleEmailAndPhoneProvider(
130
130
  allowExtraProviders: options.allowExtraProviders,
131
131
  });
132
132
  if (result === null) {
133
- throw new Error("Could not verify code");
133
+ throwAuthError("INVALID_VERIFICATION_CODE");
134
134
  }
135
135
  return {
136
136
  kind: "signedIn",
@@ -170,20 +170,10 @@ async function handleEmailAndPhoneProvider(
170
170
  await provider.sendVerificationRequest(
171
171
  {
172
172
  ...verificationArgs,
173
- provider: {
174
- ...provider,
175
- from:
176
- // Simplifies demo configuration of Resend
177
- provider.from === "Auth.js <no-reply@authjs.dev>" &&
178
- provider.id === "resend"
179
- ? "My App <onboarding@resend.dev>"
180
- : provider.from,
181
- },
182
- request: new Request("http://localhost"), // TODO: Document
173
+ provider,
174
+ request: new Request("http://localhost"),
183
175
  theme: ctx.auth.config.theme,
184
176
  },
185
- // @ts-expect-error Figure out typing for email providers so they can
186
- // access ctx.
187
177
  ctx,
188
178
  );
189
179
  } else if (provider.type === "phone") {
@@ -281,7 +271,8 @@ async function handleOAuthProvider(
281
271
  redirect.searchParams.set("code", verifier);
282
272
  if (args.params?.redirectTo !== undefined) {
283
273
  if (typeof args.params.redirectTo !== "string") {
284
- throw new Error(
274
+ throwAuthError(
275
+ "INVALID_REDIRECT",
285
276
  `Expected \`redirectTo\` to be a string, got ${args.params.redirectTo}`,
286
277
  );
287
278
  }
@@ -25,6 +25,7 @@ import {
25
25
  import { AuthDataModel, SessionInfo } from "./types.js";
26
26
  import { callSignIn, callVerifier } from "./mutations/index.js";
27
27
  import { callVerifierSignature } from "./mutations/verifierSignature.js";
28
+ import { throwAuthError } from "../errors.js";
28
29
 
29
30
  type EnrichedActionCtx = GenericActionCtxWithAuthConfig<AuthDataModel>;
30
31
 
@@ -53,10 +54,7 @@ async function handleSetup(
53
54
  // TOTP enrollment requires an authenticated user
54
55
  const identity = await ctx.auth.getUserIdentity();
55
56
  if (identity === null) {
56
- throw new Error(
57
- "TOTP enrollment requires an authenticated user. " +
58
- "Sign in first, then add TOTP to your account.",
59
- );
57
+ throwAuthError("TOTP_AUTH_REQUIRED");
60
58
  }
61
59
  const [userId] = identity.subject.split("|");
62
60
 
@@ -143,21 +141,18 @@ async function handleConfirm(
143
141
  // TOTP confirmation requires an authenticated user
144
142
  const identity = await ctx.auth.getUserIdentity();
145
143
  if (identity === null) {
146
- throw new Error(
147
- "TOTP confirmation requires an authenticated user. " +
148
- "Sign in first, then confirm your TOTP enrollment.",
149
- );
144
+ throwAuthError("TOTP_AUTH_REQUIRED");
150
145
  }
151
146
  const [userId] = identity.subject.split("|");
152
147
 
153
148
  if (!verifierValue) {
154
- throw new Error("Missing verifier");
149
+ throwAuthError("TOTP_MISSING_VERIFIER");
155
150
  }
156
151
  if (!params.code) {
157
- throw new Error("Missing `code` parameter");
152
+ throwAuthError("TOTP_MISSING_CODE");
158
153
  }
159
154
  if (!params.totpId) {
160
- throw new Error("Missing `totpId` parameter");
155
+ throwAuthError("TOTP_MISSING_ID");
161
156
  }
162
157
 
163
158
  // Look up the TOTP record
@@ -166,10 +161,10 @@ async function handleConfirm(
166
161
  { totpId: params.totpId },
167
162
  );
168
163
  if (!totpDoc) {
169
- throw new Error("TOTP enrollment not found");
164
+ throwAuthError("TOTP_NOT_FOUND");
170
165
  }
171
166
  if ((totpDoc as any).verified) {
172
- throw new Error("TOTP enrollment is already verified");
167
+ throwAuthError("TOTP_ALREADY_VERIFIED");
173
168
  }
174
169
 
175
170
  // Extract the secret from the TOTP record
@@ -184,7 +179,7 @@ async function handleConfirm(
184
179
  30,
185
180
  );
186
181
  if (!valid) {
187
- throw new Error("Invalid TOTP code");
182
+ throwAuthError("TOTP_INVALID_CODE");
188
183
  }
189
184
 
190
185
  // Mark the enrollment as verified
@@ -225,10 +220,10 @@ async function handleVerify(
225
220
  verifierValue: string | undefined,
226
221
  ): Promise<{ kind: "signedIn"; signedIn: SessionInfo | null }> {
227
222
  if (!verifierValue) {
228
- throw new Error("Missing verifier");
223
+ throwAuthError("TOTP_MISSING_VERIFIER");
229
224
  }
230
225
  if (!params.code) {
231
- throw new Error("Missing `code` parameter");
226
+ throwAuthError("TOTP_MISSING_CODE");
232
227
  }
233
228
 
234
229
  // Look up the verifier to retrieve the stored userId
@@ -237,7 +232,7 @@ async function handleVerify(
237
232
  { verifierId: verifierValue },
238
233
  );
239
234
  if (!verifierDoc) {
240
- throw new Error("Invalid or expired verifier");
235
+ throwAuthError("TOTP_INVALID_VERIFIER");
241
236
  }
242
237
 
243
238
  // Parse the signature to extract userId
@@ -250,7 +245,7 @@ async function handleVerify(
250
245
  { userId: userId as any },
251
246
  );
252
247
  if (!totpDoc) {
253
- throw new Error("No TOTP enrollment found");
248
+ throwAuthError("TOTP_NO_ENROLLMENT");
254
249
  }
255
250
 
256
251
  // Extract the secret from the TOTP record
@@ -265,7 +260,7 @@ async function handleVerify(
265
260
  30,
266
261
  );
267
262
  if (!valid) {
268
- throw new Error("Invalid TOTP code");
263
+ throwAuthError("TOTP_INVALID_CODE");
269
264
  }
270
265
 
271
266
  // Update last used timestamp
@@ -317,7 +312,8 @@ export async function handleTotp(
317
312
  > {
318
313
  const flow = args.params?.flow;
319
314
  if (!flow) {
320
- throw new Error(
315
+ throwAuthError(
316
+ "TOTP_MISSING_FLOW",
321
317
  "Missing `flow` parameter. Expected one of: setup, confirm, verify",
322
318
  );
323
319
  }
@@ -340,7 +336,8 @@ export async function handleTotp(
340
336
  args.verifier,
341
337
  );
342
338
  default:
343
- throw new Error(
339
+ throwAuthError(
340
+ "TOTP_UNKNOWN_FLOW",
344
341
  `Unknown TOTP flow: ${flow}. Expected one of: setup, confirm, verify`,
345
342
  );
346
343
  }
@@ -3,6 +3,7 @@ import { Doc, MutationCtx } from "./types.js";
3
3
  import { AuthProviderMaterializedConfig, ConvexAuthConfig } from "../types.js";
4
4
  import { LOG_LEVELS, logWithLevel } from "./utils.js";
5
5
  import { authDb } from "./db.js";
6
+ import { throwAuthError } from "../errors.js";
6
7
 
7
8
  type CreateOrUpdateUserArgs = {
8
9
  type: "oauth" | "credentials" | "email" | "phone" | "verification";
@@ -137,12 +138,10 @@ async function defaultCreateOrUpdateUser(
137
138
  try {
138
139
  await db.users.patch(userId, userData);
139
140
  } catch (error) {
140
- throw new Error(
141
- `Could not update user document with ID \`${userId}\`, ` +
141
+ throwAuthError("USER_UPDATE_FAILED", `Could not update user document with ID \`${userId}\`, ` +
142
142
  `either the user has been deleted but their account has not, ` +
143
143
  `or the profile data doesn't match the \`users\` table schema: ` +
144
- `${(error as Error).message}`,
145
- );
144
+ `${(error as Error).message}`);
146
145
  }
147
146
  } else {
148
147
  userId = (await db.users.insert(userData)) as GenericId<"user">;
@@ -231,9 +230,7 @@ export async function getAccountOrThrow(
231
230
  ) {
232
231
  const existingAccount = await authDb(ctx, config).accounts.getById(existingAccountId);
233
232
  if (existingAccount === null) {
234
- throw new Error(
235
- `Expected an account to exist for ID "${existingAccountId}"`,
236
- );
233
+ throwAuthError("ACCOUNT_NOT_FOUND", `Expected an account to exist for ID "${existingAccountId}"`);
237
234
  }
238
235
  return existingAccount;
239
236
  }