@robelest/convex-auth 0.0.4-preview.16 → 0.0.4-preview.18

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 (88) hide show
  1. package/dist/authorization/index.d.ts +17 -0
  2. package/dist/authorization/index.d.ts.map +1 -0
  3. package/dist/authorization/index.js +17 -0
  4. package/dist/authorization/index.js.map +1 -0
  5. package/dist/client/index.js +73 -0
  6. package/dist/client/index.js.map +1 -1
  7. package/dist/component/_generated/component.d.ts +52 -0
  8. package/dist/component/_generated/component.d.ts.map +1 -1
  9. package/dist/component/convex.config.d.ts +2 -2
  10. package/dist/component/convex.config.d.ts.map +1 -1
  11. package/dist/component/index.js +2 -2
  12. package/dist/component/model.d.ts +75 -75
  13. package/dist/component/model.d.ts.map +1 -1
  14. package/dist/component/model.js +2 -0
  15. package/dist/component/model.js.map +1 -1
  16. package/dist/component/public/factors.d.ts.map +1 -1
  17. package/dist/component/public/factors.js +1 -1
  18. package/dist/component/public/factors.js.map +1 -1
  19. package/dist/component/public/groups.d.ts +10 -2
  20. package/dist/component/public/groups.d.ts.map +1 -1
  21. package/dist/component/public/groups.js +92 -8
  22. package/dist/component/public/groups.js.map +1 -1
  23. package/dist/component/public/identity.d.ts.map +1 -1
  24. package/dist/component/public/identity.js +3 -3
  25. package/dist/component/public/identity.js.map +1 -1
  26. package/dist/component/public/shared.d.ts +4 -4
  27. package/dist/component/public/shared.d.ts.map +1 -1
  28. package/dist/component/public.d.ts +3 -3
  29. package/dist/component/public.js +2 -2
  30. package/dist/component/schema.d.ts +13 -2
  31. package/dist/component/schema.js +7 -5
  32. package/dist/component/schema.js.map +1 -1
  33. package/dist/component/server/auth.d.ts +4 -25
  34. package/dist/component/server/auth.d.ts.map +1 -1
  35. package/dist/component/server/auth.js +4 -10
  36. package/dist/component/server/auth.js.map +1 -1
  37. package/dist/component/server/domains/core.js +196 -131
  38. package/dist/component/server/domains/core.js.map +1 -1
  39. package/dist/component/server/factory.d.ts +3 -3
  40. package/dist/component/server/signin.js +20 -1
  41. package/dist/component/server/signin.js.map +1 -1
  42. package/dist/server/auth.d.ts +4 -25
  43. package/dist/server/auth.d.ts.map +1 -1
  44. package/dist/server/auth.js +4 -10
  45. package/dist/server/auth.js.map +1 -1
  46. package/dist/server/domains/core.d.ts +73 -58
  47. package/dist/server/domains/core.d.ts.map +1 -1
  48. package/dist/server/domains/core.js +196 -131
  49. package/dist/server/domains/core.js.map +1 -1
  50. package/dist/server/http.d.ts +2 -2
  51. package/dist/server/http.d.ts.map +1 -1
  52. package/dist/server/index.d.ts +108 -69
  53. package/dist/server/index.d.ts.map +1 -1
  54. package/dist/server/index.js +138 -54
  55. package/dist/server/index.js.map +1 -1
  56. package/dist/server/mutations/code.d.ts +9 -9
  57. package/dist/server/mutations/index.d.ts +69 -69
  58. package/dist/server/mutations/invalidate.d.ts +4 -4
  59. package/dist/server/mutations/invalidate.d.ts.map +1 -1
  60. package/dist/server/mutations/oauth.d.ts +7 -7
  61. package/dist/server/mutations/register.d.ts +9 -9
  62. package/dist/server/mutations/retrieve.d.ts +6 -6
  63. package/dist/server/mutations/retrieve.d.ts.map +1 -1
  64. package/dist/server/mutations/signature.d.ts +4 -4
  65. package/dist/server/mutations/signature.d.ts.map +1 -1
  66. package/dist/server/mutations/signin.d.ts +5 -5
  67. package/dist/server/mutations/signin.d.ts.map +1 -1
  68. package/dist/server/mutations/verify.d.ts +7 -7
  69. package/dist/server/signin.js +20 -1
  70. package/dist/server/signin.js.map +1 -1
  71. package/dist/server/version.d.ts +1 -1
  72. package/dist/server/version.js +1 -1
  73. package/dist/server/version.js.map +1 -1
  74. package/package.json +9 -5
  75. package/src/authorization/index.ts +37 -0
  76. package/src/client/index.ts +122 -0
  77. package/src/component/_generated/component.ts +64 -0
  78. package/src/component/index.ts +1 -1
  79. package/src/component/model.ts +2 -0
  80. package/src/component/public/factors.ts +3 -2
  81. package/src/component/public/groups.ts +142 -8
  82. package/src/component/public/identity.ts +9 -6
  83. package/src/component/schema.ts +14 -2
  84. package/src/server/auth.ts +9 -61
  85. package/src/server/domains/core.ts +235 -210
  86. package/src/server/index.ts +158 -75
  87. package/src/server/signin.ts +52 -0
  88. package/src/server/version.ts +1 -1
@@ -1,4 +1,3 @@
1
- import { AuthError, Fx } from "../fx.js";
2
1
  import { TOKEN_SUB_CLAIM_DIVIDER, generateRandomString, sha256 } from "../utils.js";
3
2
  import { buildScopeChecker, checkKeyRateLimit, generateApiKey, hashApiKey } from "../keys.js";
4
3
  import { materializeProvider } from "../providers.js";
@@ -12,25 +11,39 @@ function createCoreDomains(deps) {
12
11
  const { config, getAuth, callInvalidateSessions, callCreateAccountFromCredentials, callRetrieveAccountWithCredentials, callModifyAccount, getEnrichCtx, inviteTokenAlphabet, inviteTokenLength } = deps;
13
12
  const roleDefinitions = config.authorization.roles;
14
13
  const getRoleDefinition = (roleId) => {
15
- const role = roleDefinitions[roleId];
16
- if (!role) throw new AuthError("INVALID_PARAMETERS", `Unknown roleId "${roleId}".`).toConvexError();
17
- return role;
14
+ return roleDefinitions[roleId] ?? null;
18
15
  };
19
16
  const normalizeRoleIds = (roleIds) => {
20
17
  const normalized = Array.from(new Set(roleIds ?? []));
21
- for (const roleId of normalized) getRoleDefinition(roleId);
22
- return normalized;
18
+ const invalid = normalized.filter((id) => getRoleDefinition(id) === null);
19
+ if (invalid.length > 0) return {
20
+ ok: false,
21
+ invalidRoleIds: invalid
22
+ };
23
+ return {
24
+ ok: true,
25
+ roleIds: normalized
26
+ };
23
27
  };
24
28
  const resolveGrantedPermissions = (roleIds) => {
25
29
  const grants = /* @__PURE__ */ new Set();
26
30
  for (const roleId of roleIds ?? []) {
27
31
  const role = getRoleDefinition(roleId);
32
+ if (role === null) continue;
28
33
  for (const grant of role.grants) grants.add(grant);
29
34
  }
30
35
  return Array.from(grants).sort();
31
36
  };
37
+ const AUTH_CACHE = Symbol("__convexAuthCache");
38
+ function cache(ctx) {
39
+ if (!ctx[AUTH_CACHE]) ctx[AUTH_CACHE] = {
40
+ users: /* @__PURE__ */ new Map(),
41
+ groups: /* @__PURE__ */ new Map()
42
+ };
43
+ return ctx[AUTH_CACHE];
44
+ }
32
45
  const user = {
33
- current: async (ctx, request) => {
46
+ id: async (ctx, request) => {
34
47
  const identity = await ctx.auth.getUserIdentity();
35
48
  if (identity !== null) {
36
49
  const [userId] = identity.subject.split(TOKEN_SUB_CLAIM_DIVIDER);
@@ -40,30 +53,27 @@ function createCoreDomains(deps) {
40
53
  const authHeader = request.headers.get("Authorization");
41
54
  if (authHeader?.startsWith("Bearer sk_")) {
42
55
  const rawKey = authHeader.slice(7);
43
- try {
44
- return (await getAuth().key.verify(ctx, rawKey)).userId;
45
- } catch {
46
- return null;
47
- }
56
+ const result = await getAuth().key.verify(ctx, rawKey);
57
+ if (result.ok) return result.userId;
58
+ return null;
48
59
  }
49
60
  }
50
61
  return null;
51
62
  },
52
- require: async (ctx, request) => {
53
- const userId = await user.current(ctx, request);
54
- if (userId === null) throw new AuthError("NOT_SIGNED_IN").toConvexError();
55
- return userId;
56
- },
57
63
  get: async (ctx, userId) => {
58
- return await ctx.runQuery(config.component.public.userGetById, { userId });
64
+ const c = cache(ctx);
65
+ if (c.users.has(userId)) return c.users.get(userId);
66
+ const result = await ctx.runQuery(config.component.public.userGetById, { userId });
67
+ c.users.set(userId, result);
68
+ return result;
59
69
  },
60
70
  list: async (ctx, opts = {}) => {
61
71
  return await ctx.runQuery(config.component.public.userList, opts);
62
72
  },
63
73
  viewer: async (ctx) => {
64
- const userId = await user.current(ctx);
74
+ const userId = await user.id(ctx);
65
75
  if (userId === null) return null;
66
- return await ctx.runQuery(config.component.public.userGetById, { userId });
76
+ return await user.get(ctx, userId);
67
77
  },
68
78
  update: async (ctx, userId, data) => {
69
79
  await ctx.runMutation(config.component.public.userPatch, {
@@ -116,7 +126,10 @@ function createCoreDomains(deps) {
116
126
  ctx.runQuery(config.component.public.totpListByUserId, { userId })
117
127
  ]);
118
128
  const totalLinked = sessions.length + accounts.length + keys.length + members.length + passkeys.length + totps.length;
119
- if (!cascade && totalLinked > 0) throw new AuthError("INVALID_PARAMETERS", `Cannot delete user with ${totalLinked} linked records. Pass { cascade: true } to delete all linked records, or remove them manually first.`).toConvexError();
129
+ if (!cascade && totalLinked > 0) return {
130
+ ok: false,
131
+ code: "INVALID_PARAMETERS"
132
+ };
120
133
  const deletions = [];
121
134
  for (const s of sessions) deletions.push(ctx.runMutation(config.component.public.sessionDelete, { sessionId: s._id }));
122
135
  for (const a of accounts) deletions.push(ctx.runMutation(config.component.public.accountDelete, { accountId: a._id }));
@@ -163,7 +176,7 @@ function createCoreDomains(deps) {
163
176
  },
164
177
  get: async (ctx, args) => {
165
178
  const result = await callRetrieveAccountWithCredentials(ctx, args);
166
- if (typeof result === "string") throw new AuthError("ACCOUNT_NOT_FOUND", result).toConvexError();
179
+ if (typeof result === "string") return null;
167
180
  return result;
168
181
  },
169
182
  update: async (ctx, args) => {
@@ -175,8 +188,14 @@ function createCoreDomains(deps) {
175
188
  },
176
189
  delete: async (ctx, accountId) => {
177
190
  const doc = await ctx.runQuery(config.component.public.accountGetById, { accountId });
178
- if (doc === null) throw new AuthError("ACCOUNT_NOT_FOUND", "Account not found.").toConvexError();
179
- if ((await ctx.runQuery(config.component.public.accountListByUser, { userId: doc.userId })).length <= 1) throw new AuthError("INVALID_PARAMETERS", "Cannot unlink the user's only account. This would lock them out.").toConvexError();
191
+ if (doc === null) return {
192
+ ok: false,
193
+ code: "ACCOUNT_NOT_FOUND"
194
+ };
195
+ if ((await ctx.runQuery(config.component.public.accountListByUser, { userId: doc.userId })).length <= 1) return {
196
+ ok: false,
197
+ code: "INVALID_PARAMETERS"
198
+ };
180
199
  await ctx.runMutation(config.component.public.accountDelete, { accountId });
181
200
  return {
182
201
  ok: true,
@@ -232,7 +251,11 @@ function createCoreDomains(deps) {
232
251
  };
233
252
  },
234
253
  get: async (ctx, groupId) => {
235
- return await ctx.runQuery(config.component.public.groupGet, { groupId });
254
+ const c = cache(ctx);
255
+ if (c.groups.has(groupId)) return c.groups.get(groupId);
256
+ const result = await ctx.runQuery(config.component.public.groupGet, { groupId });
257
+ c.groups.set(groupId, result);
258
+ return result;
236
259
  },
237
260
  list: async (ctx, opts) => {
238
261
  return await ctx.runQuery(config.component.public.groupList, {
@@ -301,12 +324,17 @@ function createCoreDomains(deps) {
301
324
  };
302
325
  const member = {
303
326
  create: async (ctx, data) => {
304
- const roleIds = normalizeRoleIds(data.roleIds);
327
+ const normalized = normalizeRoleIds(data.roleIds);
328
+ if (!normalized.ok) return {
329
+ ok: false,
330
+ code: "INVALID_ROLE_IDS",
331
+ invalidRoleIds: normalized.invalidRoleIds
332
+ };
305
333
  return {
306
334
  ok: true,
307
335
  memberId: await ctx.runMutation(config.component.public.memberAdd, {
308
336
  ...data,
309
- roleIds
337
+ roleIds: normalized.roleIds
310
338
  })
311
339
  };
312
340
  },
@@ -334,7 +362,15 @@ function createCoreDomains(deps) {
334
362
  },
335
363
  update: async (ctx, memberId, data) => {
336
364
  const nextData = { ...data };
337
- if ("roleIds" in nextData) nextData.roleIds = normalizeRoleIds(Array.isArray(nextData.roleIds) ? nextData.roleIds : void 0);
365
+ if ("roleIds" in nextData) {
366
+ const normalized = normalizeRoleIds(Array.isArray(nextData.roleIds) ? nextData.roleIds : void 0);
367
+ if (!normalized.ok) return {
368
+ ok: false,
369
+ code: "INVALID_ROLE_IDS",
370
+ invalidRoleIds: normalized.invalidRoleIds
371
+ };
372
+ nextData.roleIds = normalized.roleIds;
373
+ }
338
374
  await ctx.runMutation(config.component.public.memberUpdate, {
339
375
  memberId,
340
376
  data: nextData
@@ -344,54 +380,25 @@ function createCoreDomains(deps) {
344
380
  memberId
345
381
  };
346
382
  },
347
- inherit: async (ctx, opts) => {
348
- const requestedRoleIds = normalizeRoleIds(opts.roleIds);
383
+ resolve: async (ctx, opts) => {
384
+ const normalized = normalizeRoleIds(opts.roleIds);
385
+ if (!normalized.ok) return {
386
+ ok: false,
387
+ code: "INVALID_ROLE_IDS",
388
+ invalidRoleIds: normalized.invalidRoleIds
389
+ };
390
+ const requestedRoleIds = normalized.roleIds;
349
391
  const roleFilter = requestedRoleIds.length > 0 ? new Set(requestedRoleIds) : null;
350
392
  const requiredGrants = Array.from(new Set(opts.grants ?? []));
351
393
  const maxDepth = Math.max(0, Math.floor(opts.maxDepth ?? 32));
352
- const visited = /* @__PURE__ */ new Set();
353
- const traversedGroupIds = [];
354
- let currentGroupId = opts.groupId;
355
- let depth = 0;
356
- let cycleDetected = false;
357
- let maxDepthReached = false;
358
- while (currentGroupId !== void 0) {
359
- if (depth > maxDepth) {
360
- maxDepthReached = true;
361
- break;
362
- }
363
- if (visited.has(currentGroupId)) {
364
- cycleDetected = true;
365
- break;
366
- }
367
- visited.add(currentGroupId);
368
- traversedGroupIds.push(currentGroupId);
369
- const membership = await member.getByUserAndGroup(ctx, {
370
- userId: opts.userId,
371
- groupId: currentGroupId
372
- });
373
- const membershipRoleIds = membership?.roleIds ?? [];
374
- const membershipGrants = resolveGrantedPermissions(membershipRoleIds);
375
- if (membership !== null && (roleFilter === null || membershipRoleIds.some((roleId) => roleFilter.has(roleId))) && requiredGrants.every((grant) => membershipGrants.includes(grant))) return {
376
- requestedGroupId: opts.groupId,
377
- matchedGroupId: currentGroupId,
378
- membership,
379
- roleIds: membershipRoleIds,
380
- grants: membershipGrants,
381
- missingGrants: [],
382
- depth,
383
- isDirect: depth === 0,
384
- isInherited: depth > 0,
385
- traversedGroupIds,
386
- cycleDetected: false,
387
- maxDepthReached: false
388
- };
389
- const doc = await group.get(ctx, currentGroupId);
390
- if (doc === null || doc.parentGroupId === void 0) break;
391
- currentGroupId = doc.parentGroupId;
392
- depth += 1;
393
- }
394
- return {
394
+ const result = await ctx.runQuery(config.component.public.memberResolve, {
395
+ userId: opts.userId,
396
+ groupId: opts.groupId,
397
+ maxDepth,
398
+ ancestry: true
399
+ });
400
+ const traversedGroupIds = result.traversedGroupIds ?? [];
401
+ if (result.membership === null) return {
395
402
  requestedGroupId: opts.groupId,
396
403
  matchedGroupId: null,
397
404
  membership: null,
@@ -402,62 +409,103 @@ function createCoreDomains(deps) {
402
409
  isDirect: false,
403
410
  isInherited: false,
404
411
  traversedGroupIds,
405
- cycleDetected,
406
- maxDepthReached
412
+ cycleDetected: false,
413
+ maxDepthReached: false
407
414
  };
408
- },
409
- require: async (ctx, opts) => {
410
- const result = await member.inherit(ctx, opts);
411
- if (result.membership === null) throw new AuthError("FORBIDDEN", `User ${opts.userId} has no membership on group ${opts.groupId} or its ancestors.`).toConvexError();
412
- return {
413
- membership: result.membership,
415
+ const membershipRoleIds = result.membership.roleIds ?? [];
416
+ const membershipGrants = resolveGrantedPermissions(membershipRoleIds);
417
+ if (roleFilter !== null && !membershipRoleIds.some((roleId) => roleFilter.has(roleId))) return {
418
+ requestedGroupId: opts.groupId,
419
+ matchedGroupId: null,
420
+ membership: null,
421
+ roleIds: [],
422
+ grants: [],
423
+ missingGrants: requiredGrants,
424
+ depth: null,
425
+ isDirect: false,
426
+ isInherited: false,
427
+ traversedGroupIds,
428
+ cycleDetected: false,
429
+ maxDepthReached: false
430
+ };
431
+ const missingGrants = requiredGrants.filter((grant) => !membershipGrants.includes(grant));
432
+ if (missingGrants.length > 0) return {
433
+ requestedGroupId: opts.groupId,
414
434
  matchedGroupId: result.matchedGroupId,
415
- roleIds: result.roleIds,
416
- grants: result.grants,
435
+ membership: result.membership,
436
+ roleIds: membershipRoleIds,
437
+ grants: membershipGrants,
438
+ missingGrants,
439
+ depth: result.depth,
417
440
  isDirect: result.isDirect,
418
441
  isInherited: result.isInherited,
419
- depth: result.depth
442
+ traversedGroupIds,
443
+ cycleDetected: false,
444
+ maxDepthReached: false
420
445
  };
421
- }
422
- };
423
- const access = {
424
- check: async (ctx, opts) => {
425
- const requiredGrants = Array.from(new Set(opts.grants));
426
- const result = await member.inherit(ctx, {
427
- userId: opts.userId,
428
- groupId: opts.groupId,
429
- grants: requiredGrants,
430
- maxDepth: opts.maxDepth
431
- });
432
- const missingGrants = requiredGrants.filter((grant) => !result.grants.includes(grant));
433
446
  return {
434
- ok: result.membership !== null && missingGrants.length === 0,
435
- membership: result.membership,
447
+ requestedGroupId: opts.groupId,
436
448
  matchedGroupId: result.matchedGroupId,
437
- roleIds: result.roleIds,
438
- grants: result.grants,
439
- missingGrants,
449
+ membership: result.membership,
450
+ roleIds: membershipRoleIds,
451
+ grants: membershipGrants,
452
+ missingGrants: [],
453
+ depth: result.depth,
440
454
  isDirect: result.isDirect,
441
455
  isInherited: result.isInherited,
442
- depth: result.depth
456
+ traversedGroupIds,
457
+ cycleDetected: false,
458
+ maxDepthReached: false
443
459
  };
444
- },
445
- require: async (ctx, opts) => {
446
- const result = await access.check(ctx, opts);
447
- if (!result.ok) throw new AuthError("FORBIDDEN", `User ${opts.userId} is missing required grants on group ${opts.groupId}: ${result.missingGrants.join(", ")}.`).toConvexError();
448
- return result;
449
460
  }
450
461
  };
462
+ const access = { check: async (ctx, opts) => {
463
+ const requiredGrants = Array.from(new Set(opts.grants));
464
+ const result = await member.resolve(ctx, {
465
+ userId: opts.userId,
466
+ groupId: opts.groupId,
467
+ grants: requiredGrants,
468
+ maxDepth: opts.maxDepth
469
+ });
470
+ if ("code" in result && result.code === "INVALID_ROLE_IDS") return {
471
+ ok: false,
472
+ membership: null,
473
+ matchedGroupId: null,
474
+ roleIds: [],
475
+ grants: [],
476
+ missingGrants: requiredGrants,
477
+ isDirect: false,
478
+ isInherited: false,
479
+ depth: null
480
+ };
481
+ const missingGrants = requiredGrants.filter((grant) => !result.grants.includes(grant));
482
+ return {
483
+ ok: result.membership !== null && missingGrants.length === 0,
484
+ membership: result.membership,
485
+ matchedGroupId: result.matchedGroupId,
486
+ roleIds: result.roleIds,
487
+ grants: result.grants,
488
+ missingGrants,
489
+ isDirect: result.isDirect,
490
+ isInherited: result.isInherited,
491
+ depth: result.depth
492
+ };
493
+ } };
451
494
  const invite = {
452
495
  create: async (ctx, data) => {
453
- const roleIds = normalizeRoleIds(data.roleIds);
496
+ const normalized = normalizeRoleIds(data.roleIds);
497
+ if (!normalized.ok) return {
498
+ ok: false,
499
+ code: "INVALID_ROLE_IDS",
500
+ invalidRoleIds: normalized.invalidRoleIds
501
+ };
454
502
  const token = generateRandomString(inviteTokenLength, inviteTokenAlphabet);
455
503
  const tokenHash = await sha256(token);
456
504
  return {
457
505
  ok: true,
458
506
  inviteId: await ctx.runMutation(config.component.public.inviteCreate, {
459
507
  ...data,
460
- roleIds,
508
+ roleIds: normalized.roleIds,
461
509
  tokenHash,
462
510
  status: "pending"
463
511
  }),
@@ -532,27 +580,38 @@ function createCoreDomains(deps) {
532
580
  verify: async (ctx, rawKey) => {
533
581
  const hashedKey = await hashApiKey(rawKey);
534
582
  const doc = await ctx.runQuery(config.component.public.keyGetByHashedKey, { hashedKey });
535
- return Fx.run(Fx.gen(function* () {
536
- yield* Fx.guard(!doc, Fx.fail(new AuthError("INVALID_API_KEY")));
537
- const k = doc;
538
- yield* Fx.guard(k.revoked, Fx.fail(new AuthError("API_KEY_REVOKED")));
539
- yield* Fx.guard(!!(k.expiresAt && k.expiresAt < Date.now()), Fx.fail(new AuthError("API_KEY_EXPIRED")));
540
- const patchData = { lastUsedAt: Date.now() };
541
- if (k.rateLimit) {
542
- const { limited, newState } = checkKeyRateLimit(k.rateLimit, k.rateLimitState ?? void 0);
543
- yield* Fx.guard(limited, Fx.fail(new AuthError("API_KEY_RATE_LIMITED")));
544
- patchData.rateLimitState = newState;
545
- }
546
- yield* Fx.promise(() => ctx.runMutation(config.component.public.keyPatch, {
547
- keyId: k._id,
548
- data: patchData
549
- }));
550
- return {
551
- userId: k.userId,
552
- keyId: k._id,
553
- scopes: buildScopeChecker(k.scopes)
583
+ if (!doc) return {
584
+ ok: false,
585
+ code: "INVALID_API_KEY"
586
+ };
587
+ const k = doc;
588
+ if (k.revoked) return {
589
+ ok: false,
590
+ code: "API_KEY_REVOKED"
591
+ };
592
+ if (k.expiresAt && k.expiresAt < Date.now()) return {
593
+ ok: false,
594
+ code: "API_KEY_EXPIRED"
595
+ };
596
+ const patchData = { lastUsedAt: Date.now() };
597
+ if (k.rateLimit) {
598
+ const { limited, newState } = checkKeyRateLimit(k.rateLimit, k.rateLimitState ?? void 0);
599
+ if (limited) return {
600
+ ok: false,
601
+ code: "API_KEY_RATE_LIMITED"
554
602
  };
555
- }).pipe(Fx.recover((e) => Fx.fatal(e.toConvexError()))));
603
+ patchData.rateLimitState = newState;
604
+ }
605
+ await ctx.runMutation(config.component.public.keyPatch, {
606
+ keyId: k._id,
607
+ data: patchData
608
+ });
609
+ return {
610
+ ok: true,
611
+ userId: k.userId,
612
+ keyId: k._id,
613
+ scopes: buildScopeChecker(k.scopes)
614
+ };
556
615
  },
557
616
  list: async (ctx, opts) => {
558
617
  return await ctx.runQuery(config.component.public.keyList, {
@@ -595,8 +654,14 @@ function createCoreDomains(deps) {
595
654
  },
596
655
  rotate: async (ctx, keyId, opts) => {
597
656
  const existing = await ctx.runQuery(config.component.public.keyGetById, { keyId });
598
- if (!existing) throw new AuthError("INVALID_PARAMETERS", "API key not found.").toConvexError();
599
- if (existing.revoked === true) throw new AuthError("API_KEY_REVOKED", "Cannot rotate a key that is already revoked.").toConvexError();
657
+ if (!existing) return {
658
+ ok: false,
659
+ code: "INVALID_PARAMETERS"
660
+ };
661
+ if (existing.revoked === true) return {
662
+ ok: false,
663
+ code: "API_KEY_REVOKED"
664
+ };
600
665
  await ctx.runMutation(config.component.public.keyPatch, {
601
666
  keyId,
602
667
  data: { revoked: true }