@robelest/convex-auth 0.0.4-preview.13 → 0.0.4-preview.15

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 (323) hide show
  1. package/README.md +140 -9
  2. package/dist/bin.cjs +5957 -5478
  3. package/dist/client/index.d.ts +3 -7
  4. package/dist/client/index.d.ts.map +1 -1
  5. package/dist/client/index.js +27 -26
  6. package/dist/client/index.js.map +1 -1
  7. package/dist/component/_generated/api.d.ts +14 -0
  8. package/dist/component/_generated/api.d.ts.map +1 -1
  9. package/dist/component/_generated/api.js.map +1 -1
  10. package/dist/component/_generated/component.d.ts +1513 -3
  11. package/dist/component/_generated/component.d.ts.map +1 -1
  12. package/dist/component/convex.config.d.ts +2 -2
  13. package/dist/component/convex.config.d.ts.map +1 -1
  14. package/dist/component/model.d.ts +153 -0
  15. package/dist/component/model.d.ts.map +1 -0
  16. package/dist/component/model.js +327 -0
  17. package/dist/component/model.js.map +1 -0
  18. package/dist/component/providers/sso.d.ts +1 -1
  19. package/dist/component/public/enterprise.d.ts +49 -0
  20. package/dist/component/public/enterprise.d.ts.map +1 -0
  21. package/dist/component/public/enterprise.js +450 -0
  22. package/dist/component/public/enterprise.js.map +1 -0
  23. package/dist/component/public/factors.d.ts +52 -0
  24. package/dist/component/public/factors.d.ts.map +1 -0
  25. package/dist/component/public/factors.js +285 -0
  26. package/dist/component/public/factors.js.map +1 -0
  27. package/dist/component/public/groups.d.ts +118 -0
  28. package/dist/component/public/groups.d.ts.map +1 -0
  29. package/dist/component/public/groups.js +599 -0
  30. package/dist/component/public/groups.js.map +1 -0
  31. package/dist/component/public/identity.d.ts +93 -0
  32. package/dist/component/public/identity.d.ts.map +1 -0
  33. package/dist/component/public/identity.js +426 -0
  34. package/dist/component/public/identity.js.map +1 -0
  35. package/dist/component/public/keys.d.ts +41 -0
  36. package/dist/component/public/keys.d.ts.map +1 -0
  37. package/dist/component/public/keys.js +157 -0
  38. package/dist/component/public/keys.js.map +1 -0
  39. package/dist/component/public/shared.d.ts +26 -0
  40. package/dist/component/public/shared.d.ts.map +1 -0
  41. package/dist/component/public/shared.js +32 -0
  42. package/dist/component/public/shared.js.map +1 -0
  43. package/dist/component/public.d.ts +9 -321
  44. package/dist/component/public.d.ts.map +1 -1
  45. package/dist/component/public.js +6 -2145
  46. package/dist/component/schema.d.ts +368 -258
  47. package/dist/component/schema.js +23 -27
  48. package/dist/component/schema.js.map +1 -1
  49. package/dist/component/server/auth.d.ts +42 -7
  50. package/dist/component/server/auth.d.ts.map +1 -1
  51. package/dist/component/server/auth.js +70 -6
  52. package/dist/component/server/auth.js.map +1 -1
  53. package/dist/component/server/cookies.js +3 -0
  54. package/dist/component/server/cookies.js.map +1 -1
  55. package/dist/component/server/db.js +1 -0
  56. package/dist/component/server/db.js.map +1 -1
  57. package/dist/component/server/device.js +3 -1
  58. package/dist/component/server/device.js.map +1 -1
  59. package/dist/component/server/domains/core.js +466 -0
  60. package/dist/component/server/domains/core.js.map +1 -0
  61. package/dist/component/server/domains/sso.js +689 -0
  62. package/dist/component/server/domains/sso.js.map +1 -0
  63. package/dist/component/server/factory.d.ts +136 -0
  64. package/dist/component/server/factory.d.ts.map +1 -0
  65. package/dist/component/server/factory.js +1128 -0
  66. package/dist/component/server/factory.js.map +1 -0
  67. package/dist/component/server/fx.js +2 -1
  68. package/dist/component/server/fx.js.map +1 -1
  69. package/dist/component/server/http.js +287 -0
  70. package/dist/component/server/http.js.map +1 -0
  71. package/dist/component/server/identity.js +13 -0
  72. package/dist/component/server/identity.js.map +1 -0
  73. package/dist/component/server/keys.js +4 -0
  74. package/dist/component/server/keys.js.map +1 -1
  75. package/dist/component/server/mutations/account.js +1 -1
  76. package/dist/component/server/mutations/index.js +2 -2
  77. package/dist/component/server/mutations/index.js.map +1 -1
  78. package/dist/component/server/mutations/invalidate.js +1 -1
  79. package/dist/component/server/mutations/oauth.js +10 -7
  80. package/dist/component/server/mutations/oauth.js.map +1 -1
  81. package/dist/component/server/mutations/refresh.js +1 -1
  82. package/dist/component/server/mutations/register.js +1 -1
  83. package/dist/component/server/mutations/retrieve.js +1 -1
  84. package/dist/component/server/mutations/signature.js +1 -1
  85. package/dist/component/server/mutations/store.js +6 -3
  86. package/dist/component/server/mutations/store.js.map +1 -1
  87. package/dist/component/server/mutations/verify.js +1 -1
  88. package/dist/component/server/oauth.js +3 -0
  89. package/dist/component/server/oauth.js.map +1 -1
  90. package/dist/component/server/passkey.js +3 -2
  91. package/dist/component/server/passkey.js.map +1 -1
  92. package/dist/component/server/provider.js +2 -0
  93. package/dist/component/server/provider.js.map +1 -1
  94. package/dist/component/server/providers.js +3 -0
  95. package/dist/component/server/providers.js.map +1 -1
  96. package/dist/component/server/ratelimit.js +3 -0
  97. package/dist/component/server/ratelimit.js.map +1 -1
  98. package/dist/component/server/redirects.js +2 -0
  99. package/dist/component/server/redirects.js.map +1 -1
  100. package/dist/component/server/refresh.js +5 -0
  101. package/dist/component/server/refresh.js.map +1 -1
  102. package/dist/component/server/sessions.js +5 -0
  103. package/dist/component/server/sessions.js.map +1 -1
  104. package/dist/component/server/signin.js +2 -1
  105. package/dist/component/server/signin.js.map +1 -1
  106. package/dist/component/server/sso.js +166 -19
  107. package/dist/component/server/sso.js.map +1 -1
  108. package/dist/component/server/tokens.js +1 -0
  109. package/dist/component/server/tokens.js.map +1 -1
  110. package/dist/component/server/totp.js +4 -2
  111. package/dist/component/server/totp.js.map +1 -1
  112. package/dist/component/server/types.d.ts +50 -35
  113. package/dist/component/server/types.d.ts.map +1 -1
  114. package/dist/component/server/types.js.map +1 -1
  115. package/dist/component/server/users.js +1 -0
  116. package/dist/component/server/users.js.map +1 -1
  117. package/dist/component/server/utils.js +44 -2
  118. package/dist/component/server/utils.js.map +1 -1
  119. package/dist/providers/anonymous.d.ts +1 -1
  120. package/dist/providers/credentials.d.ts +1 -1
  121. package/dist/providers/password.d.ts +1 -1
  122. package/dist/providers/sso.d.ts +1 -1
  123. package/dist/providers/sso.js.map +1 -1
  124. package/dist/server/auth.d.ts +44 -9
  125. package/dist/server/auth.d.ts.map +1 -1
  126. package/dist/server/auth.js +70 -6
  127. package/dist/server/auth.js.map +1 -1
  128. package/dist/server/cookies.d.ts +1 -38
  129. package/dist/server/cookies.js +3 -0
  130. package/dist/server/cookies.js.map +1 -1
  131. package/dist/server/db.d.ts +1 -125
  132. package/dist/server/db.js +1 -0
  133. package/dist/server/db.js.map +1 -1
  134. package/dist/server/device.d.ts +1 -24
  135. package/dist/server/device.js +3 -1
  136. package/dist/server/device.js.map +1 -1
  137. package/dist/server/domains/core.d.ts +320 -0
  138. package/dist/server/domains/core.d.ts.map +1 -0
  139. package/dist/server/domains/core.js +466 -0
  140. package/dist/server/domains/core.js.map +1 -0
  141. package/dist/server/domains/sso.d.ts +340 -0
  142. package/dist/server/domains/sso.d.ts.map +1 -0
  143. package/dist/server/domains/sso.js +689 -0
  144. package/dist/server/domains/sso.js.map +1 -0
  145. package/dist/server/enterpriseValidators.d.ts +1 -0
  146. package/dist/server/enterpriseValidators.js +56 -0
  147. package/dist/server/enterpriseValidators.js.map +1 -0
  148. package/dist/server/factory.d.ts +136 -0
  149. package/dist/server/factory.d.ts.map +1 -0
  150. package/dist/server/factory.js +1128 -0
  151. package/dist/server/factory.js.map +1 -0
  152. package/dist/server/fx.d.ts +1 -16
  153. package/dist/server/fx.d.ts.map +1 -1
  154. package/dist/server/fx.js +1 -0
  155. package/dist/server/fx.js.map +1 -1
  156. package/dist/server/http.d.ts +59 -0
  157. package/dist/server/http.d.ts.map +1 -0
  158. package/dist/server/http.js +287 -0
  159. package/dist/server/http.js.map +1 -0
  160. package/dist/server/identity.d.ts +1 -0
  161. package/dist/server/identity.js +13 -0
  162. package/dist/server/identity.js.map +1 -0
  163. package/dist/server/index.d.ts +432 -1
  164. package/dist/server/index.d.ts.map +1 -1
  165. package/dist/server/index.js +486 -36
  166. package/dist/server/index.js.map +1 -1
  167. package/dist/server/keys.d.ts +1 -57
  168. package/dist/server/keys.js +4 -0
  169. package/dist/server/keys.js.map +1 -1
  170. package/dist/server/mutations/account.d.ts +7 -7
  171. package/dist/server/mutations/account.d.ts.map +1 -1
  172. package/dist/server/mutations/code.d.ts +13 -13
  173. package/dist/server/mutations/index.d.ts +107 -107
  174. package/dist/server/mutations/index.d.ts.map +1 -1
  175. package/dist/server/mutations/index.js +1 -1
  176. package/dist/server/mutations/index.js.map +1 -1
  177. package/dist/server/mutations/invalidate.d.ts +5 -5
  178. package/dist/server/mutations/oauth.d.ts +10 -10
  179. package/dist/server/mutations/oauth.d.ts.map +1 -1
  180. package/dist/server/mutations/oauth.js +9 -6
  181. package/dist/server/mutations/oauth.js.map +1 -1
  182. package/dist/server/mutations/refresh.d.ts +4 -4
  183. package/dist/server/mutations/register.d.ts +12 -12
  184. package/dist/server/mutations/register.d.ts.map +1 -1
  185. package/dist/server/mutations/retrieve.d.ts +1 -1
  186. package/dist/server/mutations/signature.d.ts +5 -5
  187. package/dist/server/mutations/signature.d.ts.map +1 -1
  188. package/dist/server/mutations/signin.d.ts +1 -1
  189. package/dist/server/mutations/signout.d.ts +1 -1
  190. package/dist/server/mutations/store.d.ts +3 -2
  191. package/dist/server/mutations/store.d.ts.map +1 -1
  192. package/dist/server/mutations/store.js +6 -3
  193. package/dist/server/mutations/store.js.map +1 -1
  194. package/dist/server/mutations/verifier.d.ts +1 -1
  195. package/dist/server/mutations/verify.d.ts +4 -4
  196. package/dist/server/oauth.d.ts +1 -59
  197. package/dist/server/oauth.js +3 -0
  198. package/dist/server/oauth.js.map +1 -1
  199. package/dist/server/passkey.d.ts.map +1 -1
  200. package/dist/server/passkey.js +3 -2
  201. package/dist/server/passkey.js.map +1 -1
  202. package/dist/server/provider.d.ts +1 -14
  203. package/dist/server/provider.d.ts.map +1 -1
  204. package/dist/server/provider.js +2 -0
  205. package/dist/server/provider.js.map +1 -1
  206. package/dist/server/providers.js +3 -0
  207. package/dist/server/providers.js.map +1 -1
  208. package/dist/server/ratelimit.d.ts +1 -22
  209. package/dist/server/ratelimit.js +3 -0
  210. package/dist/server/ratelimit.js.map +1 -1
  211. package/dist/server/redirects.d.ts +1 -10
  212. package/dist/server/redirects.js +2 -0
  213. package/dist/server/redirects.js.map +1 -1
  214. package/dist/server/refresh.d.ts +1 -37
  215. package/dist/server/refresh.js +5 -0
  216. package/dist/server/refresh.js.map +1 -1
  217. package/dist/server/sessions.d.ts +1 -28
  218. package/dist/server/sessions.js +5 -0
  219. package/dist/server/sessions.js.map +1 -1
  220. package/dist/server/signin.d.ts +1 -55
  221. package/dist/server/signin.js +2 -1
  222. package/dist/server/signin.js.map +1 -1
  223. package/dist/server/sso.d.ts +1 -348
  224. package/dist/server/sso.js +165 -18
  225. package/dist/server/sso.js.map +1 -1
  226. package/dist/server/templates.d.ts +1 -21
  227. package/dist/server/templates.js +1 -0
  228. package/dist/server/templates.js.map +1 -1
  229. package/dist/server/tokens.d.ts +1 -11
  230. package/dist/server/tokens.js +1 -0
  231. package/dist/server/tokens.js.map +1 -1
  232. package/dist/server/totp.d.ts +1 -23
  233. package/dist/server/totp.js +4 -2
  234. package/dist/server/totp.js.map +1 -1
  235. package/dist/server/types.d.ts +55 -71
  236. package/dist/server/types.d.ts.map +1 -1
  237. package/dist/server/types.js.map +1 -1
  238. package/dist/server/users.d.ts +1 -31
  239. package/dist/server/users.js +1 -0
  240. package/dist/server/users.js.map +1 -1
  241. package/dist/server/utils.d.ts +1 -27
  242. package/dist/server/utils.js +44 -2
  243. package/dist/server/utils.js.map +1 -1
  244. package/dist/server/version.d.ts +1 -1
  245. package/dist/server/version.js +1 -1
  246. package/dist/server/version.js.map +1 -1
  247. package/package.json +4 -5
  248. package/src/cli/bin.ts +5 -0
  249. package/src/cli/index.ts +22 -9
  250. package/src/cli/keys.ts +3 -0
  251. package/src/client/index.ts +36 -37
  252. package/src/component/_generated/api.ts +14 -0
  253. package/src/component/_generated/component.ts +1920 -3
  254. package/src/component/index.ts +2 -0
  255. package/src/component/model.ts +424 -0
  256. package/src/component/public/enterprise.ts +654 -0
  257. package/src/component/public/factors.ts +332 -0
  258. package/src/component/public/groups.ts +951 -0
  259. package/src/component/public/identity.ts +566 -0
  260. package/src/component/public/keys.ts +209 -0
  261. package/src/component/public/shared.ts +117 -0
  262. package/src/component/public.ts +5 -2965
  263. package/src/component/schema.ts +47 -57
  264. package/src/providers/sso.ts +1 -1
  265. package/src/server/auth.ts +192 -9
  266. package/src/server/cookies.ts +3 -0
  267. package/src/server/db.ts +3 -0
  268. package/src/server/device.ts +3 -1
  269. package/src/server/domains/core.ts +916 -0
  270. package/src/server/domains/sso.ts +1462 -0
  271. package/src/server/enterpriseValidators.ts +88 -0
  272. package/src/server/factory.ts +2168 -0
  273. package/src/server/fx.ts +1 -0
  274. package/src/server/http.ts +529 -0
  275. package/src/server/identity.ts +18 -0
  276. package/src/server/index.ts +712 -40
  277. package/src/server/keys.ts +4 -0
  278. package/src/server/mutations/index.ts +1 -1
  279. package/src/server/mutations/oauth.ts +36 -8
  280. package/src/server/mutations/store.ts +6 -3
  281. package/src/server/oauth.ts +6 -0
  282. package/src/server/passkey.ts +3 -2
  283. package/src/server/provider.ts +2 -0
  284. package/src/server/providers.ts +3 -0
  285. package/src/server/ratelimit.ts +3 -0
  286. package/src/server/redirects.ts +2 -0
  287. package/src/server/refresh.ts +5 -0
  288. package/src/server/sessions.ts +5 -0
  289. package/src/server/signin.ts +1 -0
  290. package/src/server/sso.ts +251 -17
  291. package/src/server/templates.ts +1 -0
  292. package/src/server/tokens.ts +1 -0
  293. package/src/server/totp.ts +4 -2
  294. package/src/server/types.ts +85 -77
  295. package/src/server/users.ts +1 -0
  296. package/src/server/utils.ts +71 -1
  297. package/src/server/version.ts +1 -1
  298. package/dist/component/public.js.map +0 -1
  299. package/dist/component/server/implementation.d.ts +0 -1264
  300. package/dist/component/server/implementation.d.ts.map +0 -1
  301. package/dist/component/server/implementation.js +0 -2365
  302. package/dist/component/server/implementation.js.map +0 -1
  303. package/dist/server/cookies.d.ts.map +0 -1
  304. package/dist/server/db.d.ts.map +0 -1
  305. package/dist/server/device.d.ts.map +0 -1
  306. package/dist/server/implementation.d.ts +0 -1264
  307. package/dist/server/implementation.d.ts.map +0 -1
  308. package/dist/server/implementation.js +0 -2365
  309. package/dist/server/implementation.js.map +0 -1
  310. package/dist/server/keys.d.ts.map +0 -1
  311. package/dist/server/oauth.d.ts.map +0 -1
  312. package/dist/server/ratelimit.d.ts.map +0 -1
  313. package/dist/server/redirects.d.ts.map +0 -1
  314. package/dist/server/refresh.d.ts.map +0 -1
  315. package/dist/server/sessions.d.ts.map +0 -1
  316. package/dist/server/signin.d.ts.map +0 -1
  317. package/dist/server/sso.d.ts.map +0 -1
  318. package/dist/server/templates.d.ts.map +0 -1
  319. package/dist/server/tokens.d.ts.map +0 -1
  320. package/dist/server/totp.d.ts.map +0 -1
  321. package/dist/server/users.d.ts.map +0 -1
  322. package/dist/server/utils.d.ts.map +0 -1
  323. package/src/server/implementation.ts +0 -5336
@@ -0,0 +1,916 @@
1
+ import { Auth, GenericActionCtx, GenericDataModel } from "convex/server";
2
+ import { GenericId } from "convex/values";
3
+
4
+ import { AuthError, Fx } from "../fx";
5
+ import {
6
+ buildScopeChecker,
7
+ checkKeyRateLimit,
8
+ generateApiKey,
9
+ hashApiKey,
10
+ } from "../keys";
11
+ import { materializeProvider } from "../providers";
12
+ import { signInImpl } from "../signin";
13
+ import type {
14
+ AuthProviderConfig,
15
+ KeyDoc,
16
+ KeyScope,
17
+ ScopeChecker,
18
+ UserOrderBy,
19
+ UserWhere,
20
+ } from "../types";
21
+ import {
22
+ generateRandomString,
23
+ sha256,
24
+ TOKEN_SUB_CLAIM_DIVIDER,
25
+ } from "../utils";
26
+
27
+ type ComponentCtx = Pick<
28
+ GenericActionCtx<GenericDataModel>,
29
+ "runQuery" | "runMutation"
30
+ >;
31
+ type ComponentReadCtx = Pick<GenericActionCtx<GenericDataModel>, "runQuery">;
32
+ type ComponentAuthReadCtx = ComponentReadCtx & { auth: Auth };
33
+ type AccountCredentials = { id: string; secret?: string };
34
+ type CreateAccountArgs = {
35
+ provider: string;
36
+ account: AccountCredentials;
37
+ profile: Record<string, unknown>;
38
+ shouldLinkViaEmail?: boolean;
39
+ shouldLinkViaPhone?: boolean;
40
+ };
41
+ type RetrieveAccountArgs = { provider: string; account: AccountCredentials };
42
+ type UpdateAccountCredentialsArgs = {
43
+ provider: string;
44
+ account: { id: string; secret: string };
45
+ };
46
+
47
+ type CoreDeps = {
48
+ config: any;
49
+ getAuth: () => any;
50
+ callInvalidateSessions: <DataModel extends GenericDataModel>(
51
+ ctx: GenericActionCtx<DataModel>,
52
+ args: { userId: GenericId<"User">; except?: GenericId<"Session">[] },
53
+ ) => Promise<void>;
54
+ callCreateAccountFromCredentials: <DataModel extends GenericDataModel>(
55
+ ctx: GenericActionCtx<DataModel>,
56
+ args: CreateAccountArgs,
57
+ ) => Promise<any>;
58
+ callRetrieveAccountWithCredentials: <DataModel extends GenericDataModel>(
59
+ ctx: GenericActionCtx<DataModel>,
60
+ args: RetrieveAccountArgs,
61
+ ) => Promise<any>;
62
+ callModifyAccount: <DataModel extends GenericDataModel>(
63
+ ctx: GenericActionCtx<DataModel>,
64
+ args: UpdateAccountCredentialsArgs,
65
+ ) => Promise<void>;
66
+ getEnrichCtx: () => <DataModel extends GenericDataModel>(
67
+ ctx: GenericActionCtx<DataModel>,
68
+ ) => any;
69
+ inviteTokenAlphabet: string;
70
+ inviteTokenLength: number;
71
+ };
72
+
73
+ /**
74
+ * Build the core auth domains that back the canonical app API surface.
75
+ */
76
+ export function createCoreDomains(deps: CoreDeps) {
77
+ const {
78
+ config,
79
+ getAuth,
80
+ callInvalidateSessions,
81
+ callCreateAccountFromCredentials,
82
+ callRetrieveAccountWithCredentials,
83
+ callModifyAccount,
84
+ getEnrichCtx,
85
+ inviteTokenAlphabet,
86
+ inviteTokenLength,
87
+ } = deps;
88
+
89
+ const user = {
90
+ current: async (
91
+ ctx: { auth: Auth } & Partial<ComponentCtx>,
92
+ request?: Request,
93
+ ): Promise<string | null> => {
94
+ const identity = await ctx.auth.getUserIdentity();
95
+ if (identity !== null) {
96
+ const [userId] = identity.subject.split(TOKEN_SUB_CLAIM_DIVIDER);
97
+ return userId;
98
+ }
99
+ if (request !== undefined && "runMutation" in ctx && ctx.runMutation) {
100
+ const authHeader = request.headers.get("Authorization");
101
+ if (authHeader?.startsWith("Bearer sk_")) {
102
+ const rawKey = authHeader.slice(7);
103
+ try {
104
+ const result = await getAuth().key.verify(
105
+ ctx as ComponentCtx,
106
+ rawKey,
107
+ );
108
+ return result.userId;
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+ }
114
+ return null;
115
+ },
116
+ require: async (
117
+ ctx: { auth: Auth } & Partial<ComponentCtx>,
118
+ request?: Request,
119
+ ): Promise<string> => {
120
+ const userId = await user.current(ctx, request);
121
+ if (userId === null) {
122
+ throw new AuthError("NOT_SIGNED_IN").toConvexError();
123
+ }
124
+ return userId;
125
+ },
126
+ get: async (ctx: ComponentReadCtx, userId: string) => {
127
+ return await ctx.runQuery(config.component.public.userGetById, {
128
+ userId,
129
+ });
130
+ },
131
+ list: async (
132
+ ctx: ComponentReadCtx,
133
+ opts: {
134
+ where?: UserWhere;
135
+ limit?: number;
136
+ cursor?: string | null;
137
+ orderBy?: UserOrderBy;
138
+ order?: "asc" | "desc";
139
+ } = {},
140
+ ) => {
141
+ return await ctx.runQuery(config.component.public.userList, opts);
142
+ },
143
+ viewer: async (ctx: ComponentAuthReadCtx) => {
144
+ const userId = await user.current(ctx);
145
+ if (userId === null) return null;
146
+ return await ctx.runQuery(config.component.public.userGetById, {
147
+ userId,
148
+ });
149
+ },
150
+ patch: async (
151
+ ctx: ComponentCtx,
152
+ userId: string,
153
+ data: Record<string, unknown>,
154
+ ) => {
155
+ await ctx.runMutation(config.component.public.userPatch, {
156
+ userId,
157
+ data,
158
+ });
159
+ },
160
+ setActiveGroup: async (
161
+ ctx: ComponentCtx,
162
+ opts: { userId: string; groupId: string | null },
163
+ ) => {
164
+ const doc = await user.get(ctx, opts.userId);
165
+ const existingExtend =
166
+ doc !== null &&
167
+ doc.extend !== null &&
168
+ typeof doc.extend === "object" &&
169
+ !Array.isArray(doc.extend)
170
+ ? { ...(doc.extend as Record<string, unknown>) }
171
+ : {};
172
+ if (opts.groupId === null) {
173
+ const { lastActiveGroup: _omit, ...rest } = existingExtend;
174
+ await user.patch(ctx, opts.userId, { extend: rest });
175
+ return;
176
+ }
177
+ await user.patch(ctx, opts.userId, {
178
+ extend: { ...existingExtend, lastActiveGroup: opts.groupId },
179
+ });
180
+ },
181
+ getActiveGroup: async (
182
+ ctx: ComponentReadCtx,
183
+ opts: { userId: string },
184
+ ): Promise<string | null> => {
185
+ const doc = await user.get(ctx, opts.userId);
186
+ if (
187
+ doc !== null &&
188
+ doc.extend !== null &&
189
+ typeof doc.extend === "object" &&
190
+ !Array.isArray(doc.extend)
191
+ ) {
192
+ const val = (doc.extend as Record<string, unknown>).lastActiveGroup;
193
+ if (typeof val === "string") return val;
194
+ }
195
+ return null;
196
+ },
197
+ remove: async (
198
+ ctx: ComponentCtx,
199
+ userId: string,
200
+ opts?: { cascade?: boolean },
201
+ ): Promise<void> => {
202
+ const cascade = opts?.cascade !== false;
203
+ const [sessions, accounts, keys, members, passkeys, totps] =
204
+ await Promise.all([
205
+ ctx.runQuery(config.component.public.sessionListByUser, {
206
+ userId,
207
+ }) as Promise<Array<{ _id: string }>>,
208
+ ctx.runQuery(config.component.public.accountListByUser, {
209
+ userId,
210
+ }) as Promise<Array<{ _id: string }>>,
211
+ ctx.runQuery(config.component.public.keyListByUserId, {
212
+ userId,
213
+ }) as Promise<Array<{ _id: string }>>,
214
+ ctx.runQuery(config.component.public.memberListByUser, {
215
+ userId,
216
+ }) as Promise<Array<{ _id: string }>>,
217
+ ctx.runQuery(config.component.public.passkeyListByUserId, {
218
+ userId,
219
+ }) as Promise<Array<{ _id: string }>>,
220
+ ctx.runQuery(config.component.public.totpListByUserId, {
221
+ userId,
222
+ }) as Promise<Array<{ _id: string }>>,
223
+ ]);
224
+ const totalLinked =
225
+ sessions.length +
226
+ accounts.length +
227
+ keys.length +
228
+ members.length +
229
+ passkeys.length +
230
+ totps.length;
231
+ if (!cascade && totalLinked > 0) {
232
+ throw new AuthError(
233
+ "INVALID_PARAMETERS",
234
+ `Cannot delete user with ${totalLinked} linked records. Pass { cascade: true } to delete all linked records, or remove them manually first.`,
235
+ ).toConvexError();
236
+ }
237
+ const deletions: Promise<unknown>[] = [];
238
+ for (const s of sessions)
239
+ deletions.push(
240
+ ctx.runMutation(config.component.public.sessionDelete, {
241
+ sessionId: s._id,
242
+ }),
243
+ );
244
+ for (const a of accounts)
245
+ deletions.push(
246
+ ctx.runMutation(config.component.public.accountDelete, {
247
+ accountId: a._id,
248
+ }),
249
+ );
250
+ for (const k of keys)
251
+ deletions.push(
252
+ ctx.runMutation(config.component.public.keyDelete, { keyId: k._id }),
253
+ );
254
+ for (const m of members)
255
+ deletions.push(
256
+ ctx.runMutation(config.component.public.memberRemove, {
257
+ memberId: m._id,
258
+ }),
259
+ );
260
+ for (const p of passkeys)
261
+ deletions.push(
262
+ ctx.runMutation(config.component.public.passkeyDelete, {
263
+ passkeyId: p._id,
264
+ }),
265
+ );
266
+ for (const t of totps)
267
+ deletions.push(
268
+ ctx.runMutation(config.component.public.totpDelete, {
269
+ totpId: t._id,
270
+ }),
271
+ );
272
+ await Promise.all(deletions);
273
+ await ctx.runMutation(config.component.public.userDelete, { userId });
274
+ },
275
+ };
276
+
277
+ const session = {
278
+ current: async (ctx: { auth: Auth }) => {
279
+ const identity = await ctx.auth.getUserIdentity();
280
+ if (identity === null) return null;
281
+ const [, sessionId] = identity.subject.split(TOKEN_SUB_CLAIM_DIVIDER);
282
+ return sessionId as GenericId<"Session">;
283
+ },
284
+ invalidate: async <DataModel extends GenericDataModel>(
285
+ ctx: GenericActionCtx<DataModel>,
286
+ args: { userId: GenericId<"User">; except?: GenericId<"Session">[] },
287
+ ) => callInvalidateSessions(ctx, args),
288
+ get: async (ctx: ComponentReadCtx, sessionId: string) => {
289
+ return await ctx.runQuery(config.component.public.sessionGetById, {
290
+ sessionId,
291
+ });
292
+ },
293
+ list: async (ctx: ComponentReadCtx, opts: { userId: string }) => {
294
+ return await ctx.runQuery(config.component.public.sessionListByUser, {
295
+ userId: opts.userId,
296
+ });
297
+ },
298
+ };
299
+
300
+ const account = {
301
+ create: async <DataModel extends GenericDataModel>(
302
+ ctx: GenericActionCtx<DataModel>,
303
+ args: CreateAccountArgs,
304
+ ) => {
305
+ return await callCreateAccountFromCredentials(ctx, args);
306
+ },
307
+ get: async <DataModel extends GenericDataModel>(
308
+ ctx: GenericActionCtx<DataModel>,
309
+ args: RetrieveAccountArgs,
310
+ ) => {
311
+ const result = await callRetrieveAccountWithCredentials(ctx, args);
312
+ if (typeof result === "string") {
313
+ throw new AuthError("ACCOUNT_NOT_FOUND", result).toConvexError();
314
+ }
315
+ return result;
316
+ },
317
+ update: async <DataModel extends GenericDataModel>(
318
+ ctx: GenericActionCtx<DataModel>,
319
+ args: UpdateAccountCredentialsArgs,
320
+ ): Promise<void> => {
321
+ return await callModifyAccount(ctx, args);
322
+ },
323
+ remove: async (ctx: ComponentCtx, accountId: string): Promise<void> => {
324
+ const doc = await ctx.runQuery(config.component.public.accountGetById, {
325
+ accountId,
326
+ });
327
+ if (doc === null) {
328
+ throw new AuthError(
329
+ "ACCOUNT_NOT_FOUND",
330
+ "Account not found.",
331
+ ).toConvexError();
332
+ }
333
+ const allAccounts = (await ctx.runQuery(
334
+ config.component.public.accountListByUser,
335
+ { userId: (doc as any).userId },
336
+ )) as Array<{ _id: string }>;
337
+ if (allAccounts.length <= 1) {
338
+ throw new AuthError(
339
+ "INVALID_PARAMETERS",
340
+ "Cannot unlink the user's only account. This would lock them out.",
341
+ ).toConvexError();
342
+ }
343
+ await ctx.runMutation(config.component.public.accountDelete, {
344
+ accountId,
345
+ });
346
+ },
347
+ listPasskeys: async (ctx: ComponentReadCtx, opts: { userId: string }) => {
348
+ return await ctx.runQuery(
349
+ config.component.public.passkeyListByUserId,
350
+ opts,
351
+ );
352
+ },
353
+ renamePasskey: async (
354
+ ctx: ComponentCtx,
355
+ passkeyId: string,
356
+ name: string,
357
+ ) => {
358
+ await ctx.runMutation(config.component.public.passkeyUpdateMeta, {
359
+ passkeyId,
360
+ data: { name },
361
+ });
362
+ },
363
+ removePasskey: async (ctx: ComponentCtx, passkeyId: string) => {
364
+ await ctx.runMutation(config.component.public.passkeyDelete, {
365
+ passkeyId,
366
+ });
367
+ },
368
+ listTotps: async (ctx: ComponentReadCtx, opts: { userId: string }) => {
369
+ return await ctx.runQuery(config.component.public.totpListByUserId, opts);
370
+ },
371
+ removeTotp: async (ctx: ComponentCtx, totpId: string) => {
372
+ await ctx.runMutation(config.component.public.totpDelete, { totpId });
373
+ },
374
+ };
375
+
376
+ const provider = {
377
+ signIn: async <DataModel extends GenericDataModel>(
378
+ ctx: GenericActionCtx<DataModel>,
379
+ providerConfig: AuthProviderConfig,
380
+ args: {
381
+ accountId?: GenericId<"Account">;
382
+ params?: Record<string, unknown>;
383
+ },
384
+ ) => {
385
+ const result = await signInImpl(
386
+ getEnrichCtx()(ctx),
387
+ materializeProvider(providerConfig),
388
+ args as {
389
+ accountId?: GenericId<"Account">;
390
+ params?: Record<string, any>;
391
+ },
392
+ { generateTokens: false, allowExtraProviders: true },
393
+ );
394
+ return result.kind === "signedIn"
395
+ ? result.signedIn !== null
396
+ ? {
397
+ userId: result.signedIn.userId,
398
+ sessionId: result.signedIn.sessionId,
399
+ }
400
+ : null
401
+ : null;
402
+ },
403
+ };
404
+
405
+ const group = {
406
+ create: async (
407
+ ctx: ComponentCtx,
408
+ data: {
409
+ name: string;
410
+ slug?: string;
411
+ type?: string;
412
+ parentGroupId?: string;
413
+ tags?: Array<{ key: string; value: string }>;
414
+ extend?: Record<string, unknown>;
415
+ },
416
+ ): Promise<string> => {
417
+ return (await ctx.runMutation(
418
+ config.component.public.groupCreate,
419
+ data,
420
+ )) as string;
421
+ },
422
+ get: async (ctx: ComponentReadCtx, groupId: string) => {
423
+ return await ctx.runQuery(config.component.public.groupGet, { groupId });
424
+ },
425
+ list: async (
426
+ ctx: ComponentReadCtx,
427
+ opts?: {
428
+ where?: {
429
+ slug?: string;
430
+ type?: string;
431
+ parentGroupId?: string;
432
+ name?: string;
433
+ isRoot?: boolean;
434
+ tagsAll?: Array<{ key: string; value: string }>;
435
+ tagsAny?: Array<{ key: string; value: string }>;
436
+ };
437
+ limit?: number;
438
+ cursor?: string | null;
439
+ orderBy?: "_creationTime" | "name" | "slug" | "type";
440
+ order?: "asc" | "desc";
441
+ },
442
+ ) => {
443
+ return await ctx.runQuery(config.component.public.groupList, {
444
+ where: opts?.where,
445
+ limit: opts?.limit,
446
+ cursor: opts?.cursor,
447
+ orderBy: opts?.orderBy,
448
+ order: opts?.order,
449
+ });
450
+ },
451
+ update: async (
452
+ ctx: ComponentCtx,
453
+ groupId: string,
454
+ data: Record<string, unknown>,
455
+ ) => {
456
+ await ctx.runMutation(config.component.public.groupUpdate, {
457
+ groupId,
458
+ data,
459
+ });
460
+ },
461
+ delete: async (ctx: ComponentCtx, groupId: string) => {
462
+ await ctx.runMutation(config.component.public.groupDelete, { groupId });
463
+ },
464
+ ancestors: async (
465
+ ctx: ComponentReadCtx,
466
+ opts: { groupId: string; maxDepth?: number; includeSelf?: boolean },
467
+ ) => {
468
+ const maxDepth = Math.max(0, Math.floor(opts.maxDepth ?? 32));
469
+ const visited = new Set<string>();
470
+ const ancestors: any[] = [];
471
+ let cycleDetected = false;
472
+ let maxDepthReached = false;
473
+ let currentGroupId: string | undefined = opts.groupId;
474
+ let depth = 0;
475
+ let isFirst = true;
476
+ while (currentGroupId !== undefined) {
477
+ if (depth > maxDepth) {
478
+ maxDepthReached = true;
479
+ break;
480
+ }
481
+ if (visited.has(currentGroupId)) {
482
+ cycleDetected = true;
483
+ break;
484
+ }
485
+ visited.add(currentGroupId);
486
+ const doc = await group.get(ctx, currentGroupId);
487
+ if (doc === null) break;
488
+ if (isFirst) {
489
+ isFirst = false;
490
+ if (opts.includeSelf) ancestors.push(doc);
491
+ currentGroupId = doc.parentGroupId;
492
+ depth += 1;
493
+ continue;
494
+ }
495
+ ancestors.push(doc);
496
+ currentGroupId = doc.parentGroupId;
497
+ depth += 1;
498
+ }
499
+ return { ancestors, cycleDetected, maxDepthReached };
500
+ },
501
+ };
502
+
503
+ const member = {
504
+ add: async (
505
+ ctx: ComponentCtx,
506
+ data: {
507
+ groupId: string;
508
+ userId: string;
509
+ role?: string;
510
+ status?: string;
511
+ extend?: Record<string, unknown>;
512
+ },
513
+ ): Promise<string> => {
514
+ return (await ctx.runMutation(
515
+ config.component.public.memberAdd,
516
+ data,
517
+ )) as string;
518
+ },
519
+ get: async (ctx: ComponentReadCtx, memberId: string) => {
520
+ return await ctx.runQuery(config.component.public.memberGet, {
521
+ memberId,
522
+ });
523
+ },
524
+ getByUserAndGroup: async (
525
+ ctx: ComponentReadCtx,
526
+ opts: { userId: string; groupId: string },
527
+ ) => {
528
+ return await ctx.runQuery(
529
+ config.component.public.memberGetByGroupAndUser,
530
+ opts,
531
+ );
532
+ },
533
+ list: async (
534
+ ctx: ComponentReadCtx,
535
+ opts?: {
536
+ where?: {
537
+ groupId?: string;
538
+ userId?: string;
539
+ role?: string;
540
+ status?: string;
541
+ };
542
+ limit?: number;
543
+ cursor?: string | null;
544
+ orderBy?: "_creationTime" | "role" | "status";
545
+ order?: "asc" | "desc";
546
+ },
547
+ ) => {
548
+ return await ctx.runQuery(config.component.public.memberList, {
549
+ where: opts?.where,
550
+ limit: opts?.limit,
551
+ cursor: opts?.cursor,
552
+ orderBy: opts?.orderBy,
553
+ order: opts?.order,
554
+ });
555
+ },
556
+ remove: async (ctx: ComponentCtx, memberId: string) => {
557
+ await ctx.runMutation(config.component.public.memberRemove, { memberId });
558
+ },
559
+ update: async (
560
+ ctx: ComponentCtx,
561
+ memberId: string,
562
+ data: Record<string, unknown>,
563
+ ) => {
564
+ await ctx.runMutation(config.component.public.memberUpdate, {
565
+ memberId,
566
+ data,
567
+ });
568
+ },
569
+ inherit: async (
570
+ ctx: ComponentReadCtx,
571
+ opts: {
572
+ userId: string;
573
+ groupId: string;
574
+ roles?: string[];
575
+ maxDepth?: number;
576
+ },
577
+ ) => {
578
+ const roleFilter =
579
+ opts.roles !== undefined && opts.roles.length > 0
580
+ ? new Set(opts.roles)
581
+ : null;
582
+ const maxDepth = Math.max(0, Math.floor(opts.maxDepth ?? 32));
583
+ const visited = new Set<string>();
584
+ const traversedGroupIds: string[] = [];
585
+ let currentGroupId: string | undefined = opts.groupId;
586
+ let depth = 0;
587
+ let cycleDetected = false;
588
+ let maxDepthReached = false;
589
+ while (currentGroupId !== undefined) {
590
+ if (depth > maxDepth) {
591
+ maxDepthReached = true;
592
+ break;
593
+ }
594
+ if (visited.has(currentGroupId)) {
595
+ cycleDetected = true;
596
+ break;
597
+ }
598
+ visited.add(currentGroupId);
599
+ traversedGroupIds.push(currentGroupId);
600
+ const membership = await member.getByUserAndGroup(ctx, {
601
+ userId: opts.userId,
602
+ groupId: currentGroupId,
603
+ });
604
+ if (
605
+ membership !== null &&
606
+ (roleFilter === null || roleFilter.has(membership.role))
607
+ ) {
608
+ return {
609
+ requestedGroupId: opts.groupId,
610
+ matchedGroupId: currentGroupId,
611
+ membership,
612
+ depth,
613
+ isDirect: depth === 0,
614
+ isInherited: depth > 0,
615
+ traversedGroupIds,
616
+ cycleDetected: false,
617
+ maxDepthReached: false,
618
+ };
619
+ }
620
+ const doc = await group.get(ctx, currentGroupId);
621
+ if (doc === null || doc.parentGroupId === undefined) break;
622
+ currentGroupId = doc.parentGroupId;
623
+ depth += 1;
624
+ }
625
+ return {
626
+ requestedGroupId: opts.groupId,
627
+ matchedGroupId: null,
628
+ membership: null,
629
+ depth: null,
630
+ isDirect: false,
631
+ isInherited: false,
632
+ traversedGroupIds,
633
+ cycleDetected,
634
+ maxDepthReached,
635
+ };
636
+ },
637
+ require: async (
638
+ ctx: ComponentReadCtx,
639
+ opts: {
640
+ userId: string;
641
+ groupId: string;
642
+ roles?: string[];
643
+ maxDepth?: number;
644
+ },
645
+ ) => {
646
+ const result = await member.inherit(ctx, opts);
647
+ if (result.membership === null) {
648
+ throw new AuthError(
649
+ "FORBIDDEN",
650
+ `User ${opts.userId} has no membership on group ${opts.groupId} or its ancestors.`,
651
+ ).toConvexError();
652
+ }
653
+ return {
654
+ membership: result.membership,
655
+ matchedGroupId: result.matchedGroupId,
656
+ isDirect: result.isDirect,
657
+ isInherited: result.isInherited,
658
+ depth: result.depth,
659
+ };
660
+ },
661
+ };
662
+
663
+ const invite = {
664
+ create: async (
665
+ ctx: ComponentCtx,
666
+ data: {
667
+ groupId?: string;
668
+ invitedByUserId?: string;
669
+ email?: string;
670
+ role?: string;
671
+ expiresTime?: number;
672
+ extend?: Record<string, unknown>;
673
+ },
674
+ ): Promise<{ inviteId: string; token: string }> => {
675
+ const token = generateRandomString(
676
+ inviteTokenLength,
677
+ inviteTokenAlphabet,
678
+ );
679
+ const tokenHash = await sha256(token);
680
+ const inviteId = (await ctx.runMutation(
681
+ config.component.public.inviteCreate,
682
+ { ...data, tokenHash, status: "pending" },
683
+ )) as string;
684
+ return { inviteId, token };
685
+ },
686
+ get: async (ctx: ComponentReadCtx, inviteId: string) => {
687
+ return await ctx.runQuery(config.component.public.inviteGet, {
688
+ inviteId,
689
+ });
690
+ },
691
+ token: {
692
+ get: async (ctx: ComponentReadCtx, token: string) => {
693
+ const tokenHash = await sha256(token);
694
+ return await ctx.runQuery(
695
+ config.component.public.inviteGetByTokenHash,
696
+ { tokenHash },
697
+ );
698
+ },
699
+ accept: async (
700
+ ctx: ComponentCtx,
701
+ args: { token: string; acceptedByUserId: string },
702
+ ) => {
703
+ const tokenHash = await sha256(args.token);
704
+ return await ctx.runMutation(
705
+ config.component.public.inviteAcceptByToken,
706
+ { tokenHash, acceptedByUserId: args.acceptedByUserId },
707
+ );
708
+ },
709
+ },
710
+ list: async (
711
+ ctx: ComponentReadCtx,
712
+ opts?: {
713
+ where?: {
714
+ tokenHash?: string;
715
+ groupId?: string;
716
+ status?: "pending" | "accepted" | "revoked" | "expired";
717
+ email?: string;
718
+ invitedByUserId?: string;
719
+ role?: string;
720
+ acceptedByUserId?: string;
721
+ };
722
+ limit?: number;
723
+ cursor?: string | null;
724
+ orderBy?:
725
+ | "_creationTime"
726
+ | "status"
727
+ | "email"
728
+ | "expiresTime"
729
+ | "acceptedTime";
730
+ order?: "asc" | "desc";
731
+ },
732
+ ) => {
733
+ return await ctx.runQuery(config.component.public.inviteList, {
734
+ where: opts?.where,
735
+ limit: opts?.limit,
736
+ cursor: opts?.cursor,
737
+ orderBy: opts?.orderBy,
738
+ order: opts?.order,
739
+ });
740
+ },
741
+ accept: async (
742
+ ctx: ComponentCtx,
743
+ inviteId: string,
744
+ acceptedByUserId?: string,
745
+ ) => {
746
+ await ctx.runMutation(config.component.public.inviteAccept, {
747
+ inviteId,
748
+ ...(acceptedByUserId ? { acceptedByUserId } : {}),
749
+ });
750
+ },
751
+ revoke: async (ctx: ComponentCtx, inviteId: string) => {
752
+ await ctx.runMutation(config.component.public.inviteRevoke, { inviteId });
753
+ },
754
+ };
755
+
756
+ const key = {
757
+ create: async (
758
+ ctx: ComponentCtx,
759
+ opts: {
760
+ userId: string;
761
+ name: string;
762
+ scopes: KeyScope[];
763
+ rateLimit?: { maxRequests: number; windowMs: number };
764
+ expiresAt?: number;
765
+ metadata?: Record<string, unknown>;
766
+ },
767
+ ): Promise<{ keyId: string; raw: string }> => {
768
+ const { raw, hashedKey, displayPrefix } = await generateApiKey("sk_");
769
+ const keyId = (await ctx.runMutation(config.component.public.keyInsert, {
770
+ userId: opts.userId,
771
+ prefix: displayPrefix,
772
+ hashedKey,
773
+ name: opts.name,
774
+ scopes: opts.scopes,
775
+ rateLimit: opts.rateLimit,
776
+ expiresAt: opts.expiresAt,
777
+ metadata: opts.metadata,
778
+ })) as string;
779
+ return { keyId, raw };
780
+ },
781
+ verify: async (
782
+ ctx: ComponentCtx,
783
+ rawKey: string,
784
+ ): Promise<{ userId: string; keyId: string; scopes: ScopeChecker }> => {
785
+ const hashedKey = await hashApiKey(rawKey);
786
+ const doc = (await ctx.runQuery(
787
+ config.component.public.keyGetByHashedKey,
788
+ { hashedKey },
789
+ )) as KeyDoc | null;
790
+ return Fx.run(
791
+ Fx.gen(function* () {
792
+ yield* Fx.guard(!doc, Fx.fail(new AuthError("INVALID_API_KEY")));
793
+ const k = doc!;
794
+ yield* Fx.guard(k.revoked, Fx.fail(new AuthError("API_KEY_REVOKED")));
795
+ yield* Fx.guard(
796
+ !!(k.expiresAt && k.expiresAt < Date.now()),
797
+ Fx.fail(new AuthError("API_KEY_EXPIRED")),
798
+ );
799
+ const patchData: Record<string, unknown> = { lastUsedAt: Date.now() };
800
+ if (k.rateLimit) {
801
+ const { limited, newState } = checkKeyRateLimit(
802
+ k.rateLimit,
803
+ k.rateLimitState ?? undefined,
804
+ );
805
+ yield* Fx.guard(
806
+ limited,
807
+ Fx.fail(new AuthError("API_KEY_RATE_LIMITED")),
808
+ );
809
+ patchData.rateLimitState = newState;
810
+ }
811
+ yield* Fx.promise(() =>
812
+ ctx.runMutation(config.component.public.keyPatch, {
813
+ keyId: k._id,
814
+ data: patchData,
815
+ }),
816
+ );
817
+ return {
818
+ userId: k.userId,
819
+ keyId: k._id,
820
+ scopes: buildScopeChecker(k.scopes),
821
+ };
822
+ }).pipe(Fx.recover((e) => Fx.fatal(e.toConvexError()))),
823
+ );
824
+ },
825
+ list: async (
826
+ ctx: ComponentReadCtx,
827
+ opts?: {
828
+ where?: {
829
+ userId?: string;
830
+ revoked?: boolean;
831
+ name?: string;
832
+ prefix?: string;
833
+ };
834
+ limit?: number;
835
+ cursor?: string | null;
836
+ orderBy?:
837
+ | "_creationTime"
838
+ | "name"
839
+ | "lastUsedAt"
840
+ | "expiresAt"
841
+ | "revoked";
842
+ order?: "asc" | "desc";
843
+ },
844
+ ) => {
845
+ return await ctx.runQuery(config.component.public.keyList, {
846
+ where: opts?.where,
847
+ limit: opts?.limit,
848
+ cursor: opts?.cursor,
849
+ orderBy: opts?.orderBy,
850
+ order: opts?.order,
851
+ });
852
+ },
853
+ get: async (
854
+ ctx: ComponentReadCtx,
855
+ keyId: string,
856
+ ): Promise<KeyDoc | null> => {
857
+ return (await ctx.runQuery(config.component.public.keyGetById, {
858
+ keyId,
859
+ })) as KeyDoc | null;
860
+ },
861
+ update: async (
862
+ ctx: ComponentCtx,
863
+ keyId: string,
864
+ data: {
865
+ name?: string;
866
+ scopes?: KeyScope[];
867
+ rateLimit?: { maxRequests: number; windowMs: number };
868
+ },
869
+ ) => {
870
+ await ctx.runMutation(config.component.public.keyPatch, { keyId, data });
871
+ },
872
+ revoke: async (ctx: ComponentCtx, keyId: string) => {
873
+ await ctx.runMutation(config.component.public.keyPatch, {
874
+ keyId,
875
+ data: { revoked: true },
876
+ });
877
+ },
878
+ remove: async (ctx: ComponentCtx, keyId: string) => {
879
+ await ctx.runMutation(config.component.public.keyDelete, { keyId });
880
+ },
881
+ rotate: async (
882
+ ctx: ComponentCtx,
883
+ keyId: string,
884
+ opts?: { name?: string; expiresAt?: number },
885
+ ): Promise<{ keyId: string; raw: string }> => {
886
+ const existing = await ctx.runQuery(config.component.public.keyGetById, {
887
+ keyId,
888
+ });
889
+ if (!existing)
890
+ throw new AuthError(
891
+ "INVALID_PARAMETERS",
892
+ "API key not found.",
893
+ ).toConvexError();
894
+ if ((existing as any).revoked === true) {
895
+ throw new AuthError(
896
+ "API_KEY_REVOKED",
897
+ "Cannot rotate a key that is already revoked.",
898
+ ).toConvexError();
899
+ }
900
+ await ctx.runMutation(config.component.public.keyPatch, {
901
+ keyId,
902
+ data: { revoked: true },
903
+ });
904
+ return await key.create(ctx, {
905
+ userId: (existing as any).userId,
906
+ name: opts?.name ?? (existing as any).name,
907
+ scopes: (existing as any).scopes ?? [],
908
+ rateLimit: (existing as any).rateLimit,
909
+ expiresAt: opts?.expiresAt,
910
+ metadata: (existing as any).metadata,
911
+ });
912
+ },
913
+ };
914
+
915
+ return { user, session, account, provider, group, member, invite, key };
916
+ }