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

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