@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
@@ -9,6 +9,7 @@ import {
9
9
  internalMutationGeneric,
10
10
  } from "convex/server";
11
11
  import { ConvexError, GenericId, v } from "convex/values";
12
+ import { throwAuthError, isAuthError } from "../errors.js";
12
13
  import { parse as parseCookies, serialize as serializeCookie } from "cookie";
13
14
  import { redirectToParamCookie, useRedirectToParam } from "../cookies.js";
14
15
  import { FunctionReferenceFromExport } from "../convex_types.js";
@@ -44,6 +45,13 @@ import {
44
45
  } from "./mutations/index.js";
45
46
  import { signInImpl } from "./signIn.js";
46
47
  import { redirectAbsoluteUrl, setURLSearchParam } from "./redirects.js";
48
+ import {
49
+ generateApiKey,
50
+ hashApiKey,
51
+ buildScopeChecker,
52
+ validateScopes,
53
+ checkKeyRateLimit,
54
+ } from "./apiKey.js";
47
55
  import { getAuthorizationUrl } from "../oauth/authorizationUrl.js";
48
56
  import {
49
57
  defaultCookiesOptions,
@@ -106,11 +114,11 @@ export function Auth(config_: ConvexAuthConfig) {
106
114
  ) => {
107
115
  const provider = getProvider(id, allowExtraProviders);
108
116
  if (provider === undefined) {
109
- const message =
117
+ const detail =
110
118
  `Provider \`${id}\` is not configured, ` +
111
119
  `available providers are ${listAvailableProviders(config, allowExtraProviders)}.`;
112
- logWithLevel(LOG_LEVELS.ERROR, message);
113
- throw new Error(message);
120
+ logWithLevel(LOG_LEVELS.ERROR, detail);
121
+ throwAuthError("PROVIDER_NOT_CONFIGURED", detail, { provider: id });
114
122
  }
115
123
  return provider;
116
124
  };
@@ -139,6 +147,9 @@ export function Auth(config_: ConvexAuthConfig) {
139
147
  /**
140
148
  * Get the current user's ID from the auth context, or `null` if
141
149
  * not signed in.
150
+ *
151
+ * @param ctx - Any Convex context with an `auth` field (query, mutation, or action).
152
+ * @returns The user's `Id<"user">`, or `null` when unauthenticated.
142
153
  */
143
154
  current: async (ctx: { auth: Auth }) => {
144
155
  const identity = await ctx.auth.getUserIdentity();
@@ -151,24 +162,35 @@ export function Auth(config_: ConvexAuthConfig) {
151
162
  /**
152
163
  * Get the current user's ID, or throw if not signed in.
153
164
  * Use this when authentication is required.
165
+ *
166
+ * @param ctx - Any Convex context with an `auth` field.
167
+ * @returns The user's `Id<"user">`.
168
+ * @throws `ConvexError` with code `NOT_SIGNED_IN` when unauthenticated.
154
169
  */
155
170
  require: async (ctx: { auth: Auth }) => {
156
171
  const identity = await ctx.auth.getUserIdentity();
157
172
  if (identity === null) {
158
- throw new Error("Not signed in");
173
+ throwAuthError("NOT_SIGNED_IN");
159
174
  }
160
175
  const [userId] = identity.subject.split(TOKEN_SUB_CLAIM_DIVIDER);
161
176
  return userId as GenericId<"user">;
162
177
  },
163
178
  /**
164
179
  * Retrieve a user document by their ID.
180
+ *
181
+ * @param ctx - Convex context with `runQuery`.
182
+ * @param userId - The user document ID.
183
+ * @returns The user document, or `null` if not found.
165
184
  */
166
185
  get: async (ctx: ComponentReadCtx, userId: string) => {
167
186
  return await ctx.runQuery(config.component.public.userGetById, { userId });
168
187
  },
169
188
  /**
170
189
  * Get the currently signed-in user's document, or `null` if not
171
- * signed in. Convenience method combining `current` + `get`.
190
+ * signed in. Convenience combining `current()` + `get()`.
191
+ *
192
+ * @param ctx - Convex context with `auth` and `runQuery`.
193
+ * @returns The user document, or `null` when unauthenticated.
172
194
  */
173
195
  viewer: async (ctx: ComponentAuthReadCtx) => {
174
196
  const userId = await auth.user.current(ctx);
@@ -177,6 +199,23 @@ export function Auth(config_: ConvexAuthConfig) {
177
199
  }
178
200
  return await ctx.runQuery(config.component.public.userGetById, { userId });
179
201
  },
202
+ /**
203
+ * Update a user document with partial data.
204
+ *
205
+ * @param ctx - Convex context with `runMutation`.
206
+ * @param userId - The user document ID.
207
+ * @param data - Partial data to merge into the user document.
208
+ */
209
+ patch: async (
210
+ ctx: ComponentCtx,
211
+ userId: string,
212
+ data: Record<string, unknown>,
213
+ ) => {
214
+ await ctx.runMutation(config.component.public.userPatch, {
215
+ userId,
216
+ data,
217
+ });
218
+ },
180
219
  /**
181
220
  * Query a user's group memberships.
182
221
  */
@@ -208,6 +247,9 @@ export function Auth(config_: ConvexAuthConfig) {
208
247
  /**
209
248
  * Get the current session ID from the auth context, or `null` if
210
249
  * not signed in.
250
+ *
251
+ * @param ctx - Any Convex context with an `auth` field.
252
+ * @returns The session's `Id<"session">`, or `null` when unauthenticated.
211
253
  */
212
254
  current: async (ctx: { auth: Auth }) => {
213
255
  const identity = await ctx.auth.getUserIdentity();
@@ -219,6 +261,10 @@ export function Auth(config_: ConvexAuthConfig) {
219
261
  },
220
262
  /**
221
263
  * Invalidate sessions for a user, optionally preserving specific sessions.
264
+ *
265
+ * @param ctx - Convex action context.
266
+ * @param args.userId - The user whose sessions to invalidate.
267
+ * @param args.except - Session IDs to preserve (e.g. the current session).
222
268
  */
223
269
  invalidate: async <DataModel extends GenericDataModel>(
224
270
  ctx: GenericActionCtx<DataModel>,
@@ -234,6 +280,10 @@ export function Auth(config_: ConvexAuthConfig) {
234
280
  account: {
235
281
  /**
236
282
  * Create an account and user for a credentials provider.
283
+ *
284
+ * @param ctx - Convex action context.
285
+ * @param args - Provider ID, account credentials, profile data, and link flags.
286
+ * @returns `{ account, user }` — the created account and user documents.
237
287
  */
238
288
  create: async <DataModel extends GenericDataModel>(
239
289
  ctx: GenericActionCtx<DataModel>,
@@ -244,6 +294,11 @@ export function Auth(config_: ConvexAuthConfig) {
244
294
  },
245
295
  /**
246
296
  * Retrieve an account and user for a credentials provider.
297
+ *
298
+ * @param ctx - Convex action context.
299
+ * @param args - Provider ID and account credentials (id, optional secret).
300
+ * @returns `{ account, user }` — the matched account and user documents.
301
+ * @throws `ConvexError` with code `ACCOUNT_NOT_FOUND` when no match exists.
247
302
  */
248
303
  get: async <DataModel extends GenericDataModel>(
249
304
  ctx: GenericActionCtx<DataModel>,
@@ -252,12 +307,15 @@ export function Auth(config_: ConvexAuthConfig) {
252
307
  const actionCtx = ctx as unknown as ActionCtx;
253
308
  const result = await callRetreiveAccountWithCredentials(actionCtx, args);
254
309
  if (typeof result === "string") {
255
- throw new Error(result);
310
+ throwAuthError("ACCOUNT_NOT_FOUND", result);
256
311
  }
257
312
  return result;
258
313
  },
259
314
  /**
260
- * Update credentials for an existing account.
315
+ * Update credentials (secret) for an existing account.
316
+ *
317
+ * @param ctx - Convex action context.
318
+ * @param args - Provider ID and new account credentials (id + secret).
261
319
  */
262
320
  updateCredentials: async <DataModel extends GenericDataModel>(
263
321
  ctx: GenericActionCtx<DataModel>,
@@ -270,6 +328,11 @@ export function Auth(config_: ConvexAuthConfig) {
270
328
  provider: {
271
329
  /**
272
330
  * Sign in via another provider, typically from a credentials flow.
331
+ *
332
+ * @param ctx - Convex action context.
333
+ * @param provider - The provider config to sign in with.
334
+ * @param args - Optional account ID and params.
335
+ * @returns `{ userId, sessionId }` on success, or `null`.
273
336
  */
274
337
  signIn: async <DataModel extends GenericDataModel>(
275
338
  ctx: GenericActionCtx<DataModel>,
@@ -507,15 +570,17 @@ export function Auth(config_: ConvexAuthConfig) {
507
570
  * the timestamp. If the invite has a group, the caller is responsible
508
571
  * for creating the member record via `auth.group.member.add` in the
509
572
  * same Convex mutation for transactional safety.
510
- *
511
- * @throws ConvexError with code `INVITE_NOT_FOUND` when the invite does
512
- * not exist.
513
- * @throws ConvexError with code `INVITE_NOT_PENDING` when the invite is
514
- * not in `pending` status.
515
573
  *
574
+ * @param ctx - Convex context with `runMutation`.
575
+ * @param inviteId - The invite document ID.
576
+ * @param acceptedByUserId - User accepting the invite (recorded for audit).
577
+ * @throws `ConvexError` with code `INVITE_NOT_FOUND` when the invite does not exist.
578
+ * @throws `ConvexError` with code `INVITE_NOT_PENDING` when the invite is not in `pending` status.
579
+ *
580
+ * @example
516
581
  * ```ts
517
- * export const acceptInvite = mutation({
518
- * args: { inviteId: v.string() },
582
+ * export const acceptInvite = mutation({
583
+ * args: { inviteId: v.string() },
519
584
  * handler: async (ctx, { inviteId }) => {
520
585
  * const userId = await auth.user.require(ctx);
521
586
  * const invite = await auth.invite.get(ctx, inviteId);
@@ -539,14 +604,14 @@ export function Auth(config_: ConvexAuthConfig) {
539
604
  ...(acceptedByUserId ? { acceptedByUserId } : {}),
540
605
  });
541
606
  },
542
- /**
543
- * Revoke a pending invitation.
544
- *
545
- * @throws ConvexError with code `INVITE_NOT_FOUND` when the invite does
546
- * not exist.
547
- * @throws ConvexError with code `INVITE_NOT_PENDING` when the invite is
548
- * not in `pending` status.
549
- */
607
+ /**
608
+ * Revoke a pending invitation.
609
+ *
610
+ * @param ctx - Convex context with `runMutation`.
611
+ * @param inviteId - The invite document ID.
612
+ * @throws `ConvexError` with code `INVITE_NOT_FOUND` when the invite does not exist.
613
+ * @throws `ConvexError` with code `INVITE_NOT_PENDING` when the invite is not in `pending` status.
614
+ */
550
615
  revoke: async (ctx: ComponentCtx, inviteId: string) => {
551
616
  await ctx.runMutation(config.component.public.inviteRevoke, { inviteId });
552
617
  },
@@ -631,6 +696,191 @@ export function Auth(config_: ConvexAuthConfig) {
631
696
  );
632
697
  },
633
698
  },
699
+ /**
700
+ * Manage API keys for programmatic access.
701
+ *
702
+ * Keys use SHA-256 hashing (via `@oslojs/crypto`) and support
703
+ * scoped resource:action permissions with optional per-key rate limiting.
704
+ *
705
+ * ```ts
706
+ * const { keyId, raw } = await auth.key.create(ctx, {
707
+ * userId,
708
+ * name: "CI Pipeline",
709
+ * scopes: [{ resource: "users", actions: ["read", "list"] }],
710
+ * });
711
+ * // raw = "sk_live_abc123..." — show once, never stored
712
+ *
713
+ * const result = await auth.key.verify(ctx, rawKey);
714
+ * result.scopes.can("users", "read"); // true
715
+ * ```
716
+ */
717
+ key: {
718
+ /**
719
+ * Create a new API key. Returns the raw key **once** — it cannot
720
+ * be retrieved again after creation.
721
+ *
722
+ * @param opts.userId - The user this key belongs to.
723
+ * @param opts.name - Human-readable name (e.g. "CI Pipeline").
724
+ * @param opts.scopes - Resource:action permissions for this key.
725
+ * @param opts.rateLimit - Optional per-key rate limit override.
726
+ * @param opts.expiresAt - Optional expiration timestamp.
727
+ * @returns `{ keyId, raw }` where `raw` is the full key string.
728
+ */
729
+ create: async (
730
+ ctx: ComponentCtx,
731
+ opts: {
732
+ userId: string;
733
+ name: string;
734
+ scopes: import("../types.js").KeyScope[];
735
+ rateLimit?: { maxRequests: number; windowMs: number };
736
+ expiresAt?: number;
737
+ },
738
+ ): Promise<{ keyId: string; raw: string }> => {
739
+ const prefix = config.apiKeys?.prefix ?? "sk_live_";
740
+
741
+ // Validate scopes against config if defined
742
+ validateScopes(opts.scopes, config.apiKeys?.scopes);
743
+
744
+ const { raw, hashedKey, displayPrefix } = await generateApiKey(prefix);
745
+
746
+ const keyId = await ctx.runMutation(
747
+ config.component.public.keyInsert,
748
+ {
749
+ userId: opts.userId as any,
750
+ prefix: displayPrefix,
751
+ hashedKey,
752
+ name: opts.name,
753
+ scopes: opts.scopes,
754
+ rateLimit: opts.rateLimit ?? config.apiKeys?.defaultRateLimit,
755
+ expiresAt: opts.expiresAt,
756
+ },
757
+ );
758
+
759
+ return { keyId: keyId as string, raw };
760
+ },
761
+
762
+ /**
763
+ * Verify a raw API key string. Returns the userId and a scope checker
764
+ * if the key is valid, not revoked, not expired, and not rate-limited.
765
+ *
766
+ * Also updates `lastUsedAt` and rate limit state as a side effect.
767
+ *
768
+ * @throws Error if the key is invalid, revoked, expired, or rate-limited.
769
+ */
770
+ verify: async (
771
+ ctx: ComponentCtx,
772
+ rawKey: string,
773
+ ): Promise<{
774
+ userId: string;
775
+ keyId: string;
776
+ scopes: import("../types.js").ScopeChecker;
777
+ }> => {
778
+ const hashedKey = await hashApiKey(rawKey);
779
+
780
+ const key = await ctx.runQuery(
781
+ config.component.public.keyGetByHashedKey,
782
+ { hashedKey },
783
+ );
784
+ if (!key) {
785
+ throwAuthError("INVALID_API_KEY");
786
+ }
787
+ if (key.revoked) {
788
+ throwAuthError("API_KEY_REVOKED");
789
+ }
790
+ if (key.expiresAt && key.expiresAt < Date.now()) {
791
+ throwAuthError("API_KEY_EXPIRED");
792
+ }
793
+
794
+ // Check per-key rate limit
795
+ const patchData: Record<string, any> = { lastUsedAt: Date.now() };
796
+
797
+ if (key.rateLimit) {
798
+ const { limited, newState } = checkKeyRateLimit(
799
+ key.rateLimit,
800
+ key.rateLimitState ?? undefined,
801
+ );
802
+ if (limited) {
803
+ throwAuthError("API_KEY_RATE_LIMITED");
804
+ }
805
+ patchData.rateLimitState = newState;
806
+ }
807
+
808
+ // Update lastUsedAt (and rate limit state if applicable)
809
+ await ctx.runMutation(config.component.public.keyPatch, {
810
+ keyId: key._id,
811
+ data: patchData,
812
+ });
813
+
814
+ return {
815
+ userId: key.userId as string,
816
+ keyId: key._id as string,
817
+ scopes: buildScopeChecker(key.scopes),
818
+ };
819
+ },
820
+
821
+ /**
822
+ * List all API keys for a user.
823
+ * Never includes the raw key — only the display prefix.
824
+ */
825
+ list: async (ctx: ComponentReadCtx, opts: { userId: string }) => {
826
+ return await ctx.runQuery(
827
+ config.component.public.keyListByUserId,
828
+ { userId: opts.userId as any },
829
+ );
830
+ },
831
+
832
+ /**
833
+ * Get a single API key by its document ID.
834
+ * Returns `null` if not found.
835
+ */
836
+ get: async (ctx: ComponentReadCtx, keyId: string) => {
837
+ return await ctx.runQuery(
838
+ config.component.public.keyGetById,
839
+ { keyId: keyId as any },
840
+ );
841
+ },
842
+
843
+ /**
844
+ * Update an API key's metadata (name, scopes, rate limit).
845
+ */
846
+ update: async (
847
+ ctx: ComponentCtx,
848
+ keyId: string,
849
+ data: {
850
+ name?: string;
851
+ scopes?: import("../types.js").KeyScope[];
852
+ rateLimit?: { maxRequests: number; windowMs: number };
853
+ },
854
+ ) => {
855
+ if (data.scopes) {
856
+ validateScopes(data.scopes, config.apiKeys?.scopes);
857
+ }
858
+ await ctx.runMutation(config.component.public.keyPatch, {
859
+ keyId: keyId as any,
860
+ data,
861
+ });
862
+ },
863
+
864
+ /**
865
+ * Revoke an API key (soft delete). The key record is preserved
866
+ * for audit purposes but can no longer be used for authentication.
867
+ */
868
+ revoke: async (ctx: ComponentCtx, keyId: string) => {
869
+ await ctx.runMutation(config.component.public.keyPatch, {
870
+ keyId: keyId as any,
871
+ data: { revoked: true },
872
+ });
873
+ },
874
+
875
+ /**
876
+ * Hard delete an API key record.
877
+ */
878
+ remove: async (ctx: ComponentCtx, keyId: string) => {
879
+ await ctx.runMutation(config.component.public.keyDelete, {
880
+ keyId: keyId as any,
881
+ });
882
+ },
883
+ },
634
884
  /**
635
885
  * Add HTTP actions for JWT verification and OAuth sign-in.
636
886
  *
@@ -707,11 +957,11 @@ export function Auth(config_: ConvexAuthConfig) {
707
957
  const pathParts = url.pathname.split("/");
708
958
  const providerId = pathParts.at(-1)!;
709
959
  if (providerId === null) {
710
- throw new Error("Missing provider id");
960
+ throwAuthError("OAUTH_MISSING_PROVIDER");
711
961
  }
712
962
  const verifier = url.searchParams.get("code");
713
963
  if (verifier === null) {
714
- throw new Error("Missing sign-in verifier");
964
+ throwAuthError("OAUTH_MISSING_VERIFIER");
715
965
  }
716
966
  const provider = getProviderOrThrow(
717
967
  providerId,
@@ -800,8 +1050,10 @@ export function Auth(config_: ConvexAuthConfig) {
800
1050
  );
801
1051
 
802
1052
  if (typeof id !== "string") {
803
- throw new Error(
1053
+ throwAuthError(
1054
+ "OAUTH_INVALID_PROFILE",
804
1055
  `The profile method of the ${providerId} config must return a string ID`,
1056
+ { provider: providerId },
805
1057
  );
806
1058
  }
807
1059
 
@@ -914,7 +1166,7 @@ export function Auth(config_: ConvexAuthConfig) {
914
1166
  return { totpSetup: { uri: result.uri, secret: result.secret, totpId: result.totpId }, verifier: result.verifier };
915
1167
  default: {
916
1168
  const _typecheck: never = result;
917
- throw new Error(`Unexpected result from signIn, ${result as any}`);
1169
+ throwAuthError("INTERNAL_ERROR", `Unexpected result from signIn, ${result as any}`);
918
1170
  }
919
1171
  }
920
1172
  },
@@ -951,10 +1203,18 @@ function convertErrorsToResponse(
951
1203
  try {
952
1204
  return await action(ctx, request);
953
1205
  } catch (error) {
954
- if (error instanceof ConvexError) {
1206
+ if (isAuthError(error)) {
1207
+ return new Response(
1208
+ JSON.stringify({ code: error.data.code, message: error.data.message }),
1209
+ {
1210
+ status: errorStatusCode,
1211
+ headers: { "Content-Type": "application/json" },
1212
+ },
1213
+ );
1214
+ } else if (error instanceof ConvexError) {
955
1215
  return new Response(null, {
956
1216
  status: errorStatusCode,
957
- statusText: error.data,
1217
+ statusText: typeof error.data === "string" ? error.data : "Error",
958
1218
  });
959
1219
  } else {
960
1220
  logError(error);
@@ -7,6 +7,7 @@ import { getAuthSessionId } from "../sessions.js";
7
7
  import { LOG_LEVELS, logWithLevel, maybeRedact } from "../utils.js";
8
8
  import { authDb } from "../db.js";
9
9
  import { AUTH_STORE_REF } from "./storeRef.js";
10
+ import { throwAuthError } from "../../errors.js";
10
11
 
11
12
  export const createAccountFromCredentialsArgs = v.object({
12
13
  provider: v.string(),
@@ -53,7 +54,7 @@ export async function createAccountFromCredentialsImpl(
53
54
  existingAccount.secret ?? "",
54
55
  ))
55
56
  ) {
56
- throw new Error(`Account ${account.id} already exists`);
57
+ throwAuthError("ACCOUNT_ALREADY_EXISTS", `Account ${account.id} already exists`);
57
58
  }
58
59
  return {
59
60
  account: existingAccount,
@@ -5,6 +5,7 @@ import { LOG_LEVELS, logWithLevel, maybeRedact } from "../utils.js";
5
5
  import * as Provider from "../provider.js";
6
6
  import { authDb } from "../db.js";
7
7
  import { AUTH_STORE_REF } from "./storeRef.js";
8
+ import { throwAuthError } from "../../errors.js";
8
9
 
9
10
  export const modifyAccountArgs = v.object({
10
11
  provider: v.string(),
@@ -28,9 +29,7 @@ export async function modifyAccountImpl(
28
29
  });
29
30
  const existingAccount = await db.accounts.get(provider, account.id);
30
31
  if (existingAccount === null) {
31
- throw new Error(
32
- `Cannot modify account with ID ${account.id} because it does not exist`,
33
- );
32
+ throwAuthError("ACCOUNT_NOT_FOUND", `Cannot modify account with ID ${account.id} because it does not exist`);
34
33
  }
35
34
  await db.accounts.patch(existingAccount._id, {
36
35
  secret: await hash(getProviderOrThrow(provider), account.secret),
@@ -6,6 +6,7 @@ import { upsertUserAndAccount } from "../users.js";
6
6
  import { generateRandomString, logWithLevel, sha256 } from "../utils.js";
7
7
  import { authDb } from "../db.js";
8
8
  import { AUTH_STORE_REF } from "./storeRef.js";
9
+ import { throwAuthError } from "../../errors.js";
9
10
 
10
11
  const OAUTH_SIGN_IN_EXPIRATION_MS = 1000 * 60 * 2; // 2 minutes
11
12
 
@@ -32,7 +33,7 @@ export async function userOAuthImpl(
32
33
 
33
34
  const verifier = await db.verifiers.getBySignature(signature);
34
35
  if (verifier === null) {
35
- throw new Error("Invalid state");
36
+ throwAuthError("OAUTH_INVALID_STATE");
36
37
  }
37
38
 
38
39
  const { accountId } = await upsertUserAndAccount(
@@ -3,6 +3,7 @@ import { ActionCtx, MutationCtx } from "../types.js";
3
3
  import * as Provider from "../provider.js";
4
4
  import { authDb } from "../db.js";
5
5
  import { AUTH_STORE_REF } from "./storeRef.js";
6
+ import { throwAuthError } from "../../errors.js";
6
7
 
7
8
  export const verifierSignatureArgs = v.object({
8
9
  verifier: v.string(),
@@ -20,7 +21,7 @@ export async function verifierSignatureImpl(
20
21
  const db = authDb(ctx, config);
21
22
  const verifierDoc = await db.verifiers.getById(verifier as GenericId<"verifier">);
22
23
  if (verifierDoc === null) {
23
- throw new Error("Invalid verifier");
24
+ throwAuthError("INVALID_VERIFIER");
24
25
  }
25
26
  return await db.verifiers.patch(verifierDoc._id, { signature });
26
27
  }