@robelest/convex-auth 0.0.3-preview → 0.0.3-preview.3

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 (304) hide show
  1. package/dist/bin.cjs +15 -15
  2. package/dist/client/index.d.ts +40 -12
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +73 -12
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/api.d.ts +2 -2
  7. package/dist/component/_generated/api.d.ts.map +1 -1
  8. package/dist/component/_generated/component.d.ts +1 -1
  9. package/dist/component/_generated/component.d.ts.map +1 -1
  10. package/dist/component/{portalBridge.d.ts → bridge.d.ts} +2 -2
  11. package/dist/component/bridge.d.ts.map +1 -0
  12. package/dist/component/{portalBridge.js → bridge.js} +2 -2
  13. package/dist/component/bridge.js.map +1 -0
  14. package/dist/component/index.d.ts +11 -4
  15. package/dist/component/index.d.ts.map +1 -1
  16. package/dist/component/index.js +8 -2
  17. package/dist/component/index.js.map +1 -1
  18. package/dist/component/public.d.ts +24 -17
  19. package/dist/component/public.d.ts.map +1 -1
  20. package/dist/component/public.js +23 -4
  21. package/dist/component/public.js.map +1 -1
  22. package/dist/component/schema.d.ts +11 -7
  23. package/dist/component/schema.d.ts.map +1 -1
  24. package/dist/component/schema.js +4 -1
  25. package/dist/component/schema.js.map +1 -1
  26. package/dist/providers/anonymous.d.ts +3 -0
  27. package/dist/providers/anonymous.d.ts.map +1 -1
  28. package/dist/providers/anonymous.js +3 -0
  29. package/dist/providers/anonymous.js.map +1 -1
  30. package/dist/providers/credentials.d.ts +3 -0
  31. package/dist/providers/credentials.d.ts.map +1 -1
  32. package/dist/providers/credentials.js +3 -0
  33. package/dist/providers/credentials.js.map +1 -1
  34. package/dist/providers/email.d.ts +3 -0
  35. package/dist/providers/email.d.ts.map +1 -1
  36. package/dist/providers/email.js +3 -0
  37. package/dist/providers/email.js.map +1 -1
  38. package/dist/providers/passkey.d.ts +7 -1
  39. package/dist/providers/passkey.d.ts.map +1 -1
  40. package/dist/providers/passkey.js +7 -1
  41. package/dist/providers/passkey.js.map +1 -1
  42. package/dist/providers/password.d.ts +3 -0
  43. package/dist/providers/password.d.ts.map +1 -1
  44. package/dist/providers/password.js +3 -0
  45. package/dist/providers/password.js.map +1 -1
  46. package/dist/providers/phone.d.ts +3 -0
  47. package/dist/providers/phone.d.ts.map +1 -1
  48. package/dist/providers/phone.js +3 -0
  49. package/dist/providers/phone.js.map +1 -1
  50. package/dist/providers/totp.d.ts +8 -0
  51. package/dist/providers/totp.d.ts.map +1 -1
  52. package/dist/providers/totp.js +8 -0
  53. package/dist/providers/totp.js.map +1 -1
  54. package/dist/server/{convex-auth.d.ts → auth.d.ts} +226 -36
  55. package/dist/server/auth.d.ts.map +1 -0
  56. package/dist/server/{convex-auth.js → auth.js} +287 -111
  57. package/dist/server/auth.js.map +1 -0
  58. package/dist/server/errors.d.ts +148 -0
  59. package/dist/server/errors.d.ts.map +1 -0
  60. package/dist/server/errors.js +179 -0
  61. package/dist/server/errors.js.map +1 -0
  62. package/dist/server/implementation/index.d.ts +170 -48
  63. package/dist/server/implementation/index.d.ts.map +1 -1
  64. package/dist/server/implementation/index.js +383 -167
  65. package/dist/server/implementation/index.js.map +1 -1
  66. package/dist/server/implementation/{apiKey.d.ts → keys.d.ts} +1 -1
  67. package/dist/server/implementation/keys.d.ts.map +1 -0
  68. package/dist/server/implementation/{apiKey.js → keys.js} +4 -5
  69. package/dist/server/implementation/keys.js.map +1 -0
  70. package/dist/server/implementation/mutations/{modifyAccount.d.ts → account.d.ts} +3 -3
  71. package/dist/server/implementation/mutations/account.d.ts.map +1 -0
  72. package/dist/server/implementation/mutations/{modifyAccount.js → account.js} +4 -3
  73. package/dist/server/implementation/mutations/account.js.map +1 -0
  74. package/dist/server/implementation/mutations/{createVerificationCode.d.ts → code.d.ts} +1 -1
  75. package/dist/server/implementation/mutations/code.d.ts.map +1 -0
  76. package/dist/server/implementation/mutations/{createVerificationCode.js → code.js} +2 -2
  77. package/dist/server/implementation/mutations/code.js.map +1 -0
  78. package/dist/server/implementation/mutations/index.d.ts +33 -33
  79. package/dist/server/implementation/mutations/index.d.ts.map +1 -1
  80. package/dist/server/implementation/mutations/index.js +22 -22
  81. package/dist/server/implementation/mutations/index.js.map +1 -1
  82. package/dist/server/implementation/mutations/{invalidateSessions.d.ts → invalidate.d.ts} +1 -1
  83. package/dist/server/implementation/mutations/invalidate.d.ts.map +1 -0
  84. package/dist/server/implementation/mutations/{invalidateSessions.js → invalidate.js} +2 -2
  85. package/dist/server/implementation/mutations/invalidate.js.map +1 -0
  86. package/dist/server/implementation/mutations/{userOAuth.d.ts → oauth.d.ts} +3 -3
  87. package/dist/server/implementation/mutations/oauth.d.ts.map +1 -0
  88. package/dist/server/implementation/mutations/{userOAuth.js → oauth.js} +4 -3
  89. package/dist/server/implementation/mutations/oauth.js.map +1 -0
  90. package/dist/server/implementation/mutations/{refreshSession.d.ts → refresh.d.ts} +1 -1
  91. package/dist/server/implementation/mutations/refresh.d.ts.map +1 -0
  92. package/dist/server/implementation/mutations/{refreshSession.js → refresh.js} +3 -3
  93. package/dist/server/implementation/mutations/refresh.js.map +1 -0
  94. package/dist/server/implementation/mutations/{createAccountFromCredentials.d.ts → register.d.ts} +4 -4
  95. package/dist/server/implementation/mutations/register.d.ts.map +1 -0
  96. package/dist/server/implementation/mutations/{createAccountFromCredentials.js → register.js} +4 -3
  97. package/dist/server/implementation/mutations/register.js.map +1 -0
  98. package/dist/server/implementation/mutations/{retrieveAccountWithCredentials.d.ts → retrieve.d.ts} +3 -3
  99. package/dist/server/implementation/mutations/retrieve.d.ts.map +1 -0
  100. package/dist/server/implementation/mutations/{retrieveAccountWithCredentials.js → retrieve.js} +3 -3
  101. package/dist/server/implementation/mutations/retrieve.js.map +1 -0
  102. package/dist/server/implementation/mutations/{verifierSignature.d.ts → signature.d.ts} +1 -1
  103. package/dist/server/implementation/mutations/signature.d.ts.map +1 -0
  104. package/dist/server/implementation/mutations/{verifierSignature.js → signature.js} +4 -3
  105. package/dist/server/implementation/mutations/signature.js.map +1 -0
  106. package/dist/server/implementation/mutations/{signIn.d.ts → signin.d.ts} +1 -1
  107. package/dist/server/implementation/mutations/{signIn.d.ts.map → signin.d.ts.map} +1 -1
  108. package/dist/server/implementation/mutations/{signIn.js → signin.js} +2 -2
  109. package/dist/server/implementation/mutations/{signIn.js.map → signin.js.map} +1 -1
  110. package/dist/server/implementation/mutations/{signOut.d.ts → signout.d.ts} +1 -1
  111. package/dist/server/implementation/mutations/{signOut.d.ts.map → signout.d.ts.map} +1 -1
  112. package/dist/server/implementation/mutations/{signOut.js → signout.js} +2 -2
  113. package/dist/server/implementation/mutations/{signOut.js.map → signout.js.map} +1 -1
  114. package/dist/server/implementation/mutations/{storeRef.d.ts → store.d.ts} +1 -1
  115. package/dist/server/implementation/mutations/store.d.ts.map +1 -0
  116. package/dist/server/implementation/mutations/{storeRef.js → store.js} +1 -1
  117. package/dist/server/implementation/mutations/store.js.map +1 -0
  118. package/dist/server/implementation/mutations/verifier.js +1 -1
  119. package/dist/server/implementation/mutations/verifier.js.map +1 -1
  120. package/dist/server/implementation/mutations/{verifyCodeAndSignIn.d.ts → verify.d.ts} +1 -1
  121. package/dist/server/implementation/mutations/verify.d.ts.map +1 -0
  122. package/dist/server/implementation/mutations/{verifyCodeAndSignIn.js → verify.js} +3 -3
  123. package/dist/server/implementation/mutations/verify.js.map +1 -0
  124. package/dist/server/implementation/passkey.d.ts.map +1 -1
  125. package/dist/server/implementation/passkey.js +47 -55
  126. package/dist/server/implementation/passkey.js.map +1 -1
  127. package/dist/server/implementation/provider.d.ts.map +1 -1
  128. package/dist/server/implementation/provider.js +5 -4
  129. package/dist/server/implementation/provider.js.map +1 -1
  130. package/dist/server/implementation/{rateLimit.d.ts → ratelimit.d.ts} +1 -1
  131. package/dist/server/implementation/{rateLimit.d.ts.map → ratelimit.d.ts.map} +1 -1
  132. package/dist/server/implementation/{rateLimit.js → ratelimit.js} +1 -1
  133. package/dist/server/implementation/{rateLimit.js.map → ratelimit.js.map} +1 -1
  134. package/dist/server/implementation/redirects.d.ts.map +1 -1
  135. package/dist/server/implementation/redirects.js +2 -1
  136. package/dist/server/implementation/redirects.js.map +1 -1
  137. package/dist/server/implementation/{refreshTokens.d.ts → refresh.d.ts} +1 -1
  138. package/dist/server/implementation/refresh.d.ts.map +1 -0
  139. package/dist/server/implementation/{refreshTokens.js → refresh.js} +3 -2
  140. package/dist/server/implementation/refresh.js.map +1 -0
  141. package/dist/server/implementation/sessions.js +1 -1
  142. package/dist/server/implementation/sessions.js.map +1 -1
  143. package/dist/server/implementation/{signIn.d.ts → signin.d.ts} +1 -1
  144. package/dist/server/implementation/{signIn.d.ts.map → signin.d.ts.map} +1 -1
  145. package/dist/server/implementation/{signIn.js → signin.js} +12 -8
  146. package/dist/server/implementation/signin.js.map +1 -0
  147. package/dist/server/implementation/totp.d.ts.map +1 -1
  148. package/dist/server/implementation/totp.js +29 -29
  149. package/dist/server/implementation/totp.js.map +1 -1
  150. package/dist/server/implementation/types.d.ts +131 -1
  151. package/dist/server/implementation/types.d.ts.map +1 -1
  152. package/dist/server/implementation/types.js +65 -1
  153. package/dist/server/implementation/types.js.map +1 -1
  154. package/dist/server/implementation/users.d.ts.map +1 -1
  155. package/dist/server/implementation/users.js +3 -2
  156. package/dist/server/implementation/users.js.map +1 -1
  157. package/dist/server/index.d.ts +131 -1
  158. package/dist/server/index.d.ts.map +1 -1
  159. package/dist/server/index.js +117 -1
  160. package/dist/server/index.js.map +1 -1
  161. package/dist/server/oauth/{authorizationUrl.d.ts → authorization.d.ts} +1 -1
  162. package/dist/server/oauth/authorization.d.ts.map +1 -0
  163. package/dist/server/oauth/{authorizationUrl.js → authorization.js} +4 -3
  164. package/dist/server/oauth/authorization.js.map +1 -0
  165. package/dist/server/oauth/callback.d.ts.map +1 -1
  166. package/dist/server/oauth/callback.js +7 -6
  167. package/dist/server/oauth/callback.js.map +1 -1
  168. package/dist/server/oauth/checks.d.ts.map +1 -1
  169. package/dist/server/oauth/checks.js +2 -1
  170. package/dist/server/oauth/checks.js.map +1 -1
  171. package/dist/server/oauth/{convexAuth.d.ts → helpers.d.ts} +1 -1
  172. package/dist/server/oauth/helpers.d.ts.map +1 -0
  173. package/dist/server/oauth/{convexAuth.js → helpers.js} +6 -5
  174. package/dist/server/oauth/helpers.js.map +1 -0
  175. package/dist/server/oauth/lib/utils/{customFetch.d.ts → fetch.d.ts} +1 -1
  176. package/dist/server/oauth/lib/utils/fetch.d.ts.map +1 -0
  177. package/dist/server/oauth/lib/utils/{customFetch.js → fetch.js} +1 -1
  178. package/dist/server/oauth/lib/utils/fetch.js.map +1 -0
  179. package/dist/server/{provider_utils.d.ts → providers.d.ts} +1 -1
  180. package/dist/server/providers.d.ts.map +1 -0
  181. package/dist/server/{provider_utils.js → providers.js} +1 -1
  182. package/dist/server/providers.js.map +1 -0
  183. package/dist/server/{email-templates.d.ts → templates.d.ts} +8 -1
  184. package/dist/server/templates.d.ts.map +1 -0
  185. package/dist/server/{portal-email.js → templates.js} +74 -3
  186. package/dist/server/templates.js.map +1 -0
  187. package/dist/server/types.d.ts +88 -5
  188. package/dist/server/types.d.ts.map +1 -1
  189. package/dist/server/utils.d.ts.map +1 -1
  190. package/dist/server/utils.js +2 -1
  191. package/dist/server/utils.js.map +1 -1
  192. package/dist/server/version.d.ts +1 -1
  193. package/dist/server/version.d.ts.map +1 -1
  194. package/dist/server/version.js +1 -1
  195. package/dist/server/version.js.map +1 -1
  196. package/package.json +5 -1
  197. package/src/cli/index.ts +5 -5
  198. package/src/cli/{portal-link.ts → link.ts} +1 -1
  199. package/src/cli/utils.ts +1 -1
  200. package/src/client/index.ts +102 -17
  201. package/src/component/_generated/api.ts +2 -2
  202. package/src/component/_generated/component.ts +1 -1
  203. package/src/component/{portalBridge.ts → bridge.ts} +2 -2
  204. package/src/component/index.ts +10 -2
  205. package/src/component/public.ts +25 -4
  206. package/src/component/schema.ts +4 -1
  207. package/src/providers/anonymous.ts +3 -0
  208. package/src/providers/credentials.ts +3 -0
  209. package/src/providers/email.ts +3 -0
  210. package/src/providers/passkey.ts +8 -1
  211. package/src/providers/password.ts +3 -0
  212. package/src/providers/phone.ts +3 -0
  213. package/src/providers/totp.ts +9 -0
  214. package/src/server/auth.ts +969 -0
  215. package/src/server/errors.ts +275 -0
  216. package/src/server/implementation/index.ts +370 -88
  217. package/src/server/implementation/{apiKey.ts → keys.ts} +7 -6
  218. package/src/server/implementation/mutations/{modifyAccount.ts → account.ts} +3 -4
  219. package/src/server/implementation/mutations/{createVerificationCode.ts → code.ts} +1 -1
  220. package/src/server/implementation/mutations/index.ts +22 -22
  221. package/src/server/implementation/mutations/{invalidateSessions.ts → invalidate.ts} +1 -1
  222. package/src/server/implementation/mutations/{userOAuth.ts → oauth.ts} +3 -2
  223. package/src/server/implementation/mutations/{refreshSession.ts → refresh.ts} +2 -2
  224. package/src/server/implementation/mutations/{createAccountFromCredentials.ts → register.ts} +3 -2
  225. package/src/server/implementation/mutations/{retrieveAccountWithCredentials.ts → retrieve.ts} +2 -2
  226. package/src/server/implementation/mutations/{verifierSignature.ts → signature.ts} +3 -2
  227. package/src/server/implementation/mutations/{signIn.ts → signin.ts} +1 -1
  228. package/src/server/implementation/mutations/{signOut.ts → signout.ts} +1 -1
  229. package/src/server/implementation/mutations/verifier.ts +1 -1
  230. package/src/server/implementation/mutations/{verifyCodeAndSignIn.ts → verify.ts} +2 -2
  231. package/src/server/implementation/passkey.ts +86 -116
  232. package/src/server/implementation/provider.ts +5 -8
  233. package/src/server/implementation/redirects.ts +2 -3
  234. package/src/server/implementation/{refreshTokens.ts → refresh.ts} +2 -1
  235. package/src/server/implementation/sessions.ts +1 -1
  236. package/src/server/implementation/{signIn.ts → signin.ts} +13 -11
  237. package/src/server/implementation/totp.ts +60 -84
  238. package/src/server/implementation/types.ts +316 -1
  239. package/src/server/implementation/users.ts +4 -7
  240. package/src/server/index.ts +142 -3
  241. package/src/server/oauth/{authorizationUrl.ts → authorization.ts} +3 -2
  242. package/src/server/oauth/callback.ts +7 -6
  243. package/src/server/oauth/checks.ts +3 -1
  244. package/src/server/oauth/{convexAuth.ts → helpers.ts} +8 -5
  245. package/src/server/{portal-email.ts → templates.ts} +78 -2
  246. package/src/server/types.ts +133 -4
  247. package/src/server/utils.ts +3 -1
  248. package/src/server/version.ts +1 -1
  249. package/dist/component/portalBridge.d.ts.map +0 -1
  250. package/dist/component/portalBridge.js.map +0 -1
  251. package/dist/server/convex-auth.d.ts.map +0 -1
  252. package/dist/server/convex-auth.js.map +0 -1
  253. package/dist/server/convex_types.d.ts +0 -17
  254. package/dist/server/convex_types.d.ts.map +0 -1
  255. package/dist/server/convex_types.js +0 -2
  256. package/dist/server/convex_types.js.map +0 -1
  257. package/dist/server/email-templates.d.ts.map +0 -1
  258. package/dist/server/email-templates.js +0 -74
  259. package/dist/server/email-templates.js.map +0 -1
  260. package/dist/server/implementation/apiKey.d.ts.map +0 -1
  261. package/dist/server/implementation/apiKey.js.map +0 -1
  262. package/dist/server/implementation/mutations/createAccountFromCredentials.d.ts.map +0 -1
  263. package/dist/server/implementation/mutations/createAccountFromCredentials.js.map +0 -1
  264. package/dist/server/implementation/mutations/createVerificationCode.d.ts.map +0 -1
  265. package/dist/server/implementation/mutations/createVerificationCode.js.map +0 -1
  266. package/dist/server/implementation/mutations/invalidateSessions.d.ts.map +0 -1
  267. package/dist/server/implementation/mutations/invalidateSessions.js.map +0 -1
  268. package/dist/server/implementation/mutations/modifyAccount.d.ts.map +0 -1
  269. package/dist/server/implementation/mutations/modifyAccount.js.map +0 -1
  270. package/dist/server/implementation/mutations/refreshSession.d.ts.map +0 -1
  271. package/dist/server/implementation/mutations/refreshSession.js.map +0 -1
  272. package/dist/server/implementation/mutations/retrieveAccountWithCredentials.d.ts.map +0 -1
  273. package/dist/server/implementation/mutations/retrieveAccountWithCredentials.js.map +0 -1
  274. package/dist/server/implementation/mutations/storeRef.d.ts.map +0 -1
  275. package/dist/server/implementation/mutations/storeRef.js.map +0 -1
  276. package/dist/server/implementation/mutations/userOAuth.d.ts.map +0 -1
  277. package/dist/server/implementation/mutations/userOAuth.js.map +0 -1
  278. package/dist/server/implementation/mutations/verifierSignature.d.ts.map +0 -1
  279. package/dist/server/implementation/mutations/verifierSignature.js.map +0 -1
  280. package/dist/server/implementation/mutations/verifyCodeAndSignIn.d.ts.map +0 -1
  281. package/dist/server/implementation/mutations/verifyCodeAndSignIn.js.map +0 -1
  282. package/dist/server/implementation/refreshTokens.d.ts.map +0 -1
  283. package/dist/server/implementation/refreshTokens.js.map +0 -1
  284. package/dist/server/implementation/signIn.js.map +0 -1
  285. package/dist/server/oauth/authorizationUrl.d.ts.map +0 -1
  286. package/dist/server/oauth/authorizationUrl.js.map +0 -1
  287. package/dist/server/oauth/convexAuth.d.ts.map +0 -1
  288. package/dist/server/oauth/convexAuth.js.map +0 -1
  289. package/dist/server/oauth/lib/utils/customFetch.d.ts.map +0 -1
  290. package/dist/server/oauth/lib/utils/customFetch.js.map +0 -1
  291. package/dist/server/portal-email.d.ts +0 -19
  292. package/dist/server/portal-email.d.ts.map +0 -1
  293. package/dist/server/portal-email.js.map +0 -1
  294. package/dist/server/provider_utils.d.ts.map +0 -1
  295. package/dist/server/provider_utils.js.map +0 -1
  296. package/src/server/convex-auth.ts +0 -602
  297. package/src/server/convex_types.ts +0 -55
  298. package/src/server/email-templates.ts +0 -77
  299. /package/src/cli/{generateKeys.ts → keys.ts} +0 -0
  300. /package/src/cli/{portal-upload.ts → upload.ts} +0 -0
  301. /package/src/server/implementation/mutations/{storeRef.ts → store.ts} +0 -0
  302. /package/src/server/implementation/{rateLimit.ts → ratelimit.ts} +0 -0
  303. /package/src/server/oauth/lib/utils/{customFetch.ts → fetch.ts} +0 -0
  304. /package/src/server/{provider_utils.ts → providers.ts} +0 -0
@@ -0,0 +1,969 @@
1
+ /**
2
+ * The `Auth` class — the main entry point for Convex Auth.
3
+ *
4
+ * Combines authentication and portal admin functionality:
5
+ *
6
+ * ```ts
7
+ * // convex/auth.ts
8
+ * import { Auth, Portal } from "@robelest/convex-auth/component";
9
+ * import google from "@auth/core/providers/google";
10
+ * import { components } from "./_generated/api";
11
+ *
12
+ * export const auth = new Auth(components.auth, {
13
+ * providers: [google],
14
+ * email: {
15
+ * from: "My App <noreply@example.com>",
16
+ * send: async (_ctx, { from, to, subject, html }) => {
17
+ * await fetch("https://api.resend.com/emails", {
18
+ * method: "POST",
19
+ * headers: {
20
+ * Authorization: `Bearer ${process.env.AUTH_RESEND_KEY}`,
21
+ * "Content-Type": "application/json",
22
+ * },
23
+ * body: JSON.stringify({ from, to, subject, html }),
24
+ * });
25
+ * },
26
+ * },
27
+ * });
28
+ * export const { signIn, signOut, store } = auth;
29
+ * export const { portalQuery, portalMutation, portalInternal } = Portal(auth);
30
+ * ```
31
+ *
32
+ * @module
33
+ */
34
+
35
+ import {
36
+ queryGeneric,
37
+ mutationGeneric,
38
+ internalMutationGeneric,
39
+ httpActionGeneric,
40
+ } from "convex/server";
41
+ import type { HttpRouter, UserIdentity } from "convex/server";
42
+ import { v } from "convex/values";
43
+ import type { GenericId } from "convex/values";
44
+ import type { Doc } from "./implementation/types.js";
45
+ import type { ComponentApi as AuthComponentApi } from "../component/_generated/component.js";
46
+ import { Auth as AuthFactory } from "./implementation/index.js";
47
+ import type { ConvexAuthConfig } from "./types.js";
48
+ import { registerStaticRoutes } from "@convex-dev/self-hosting";
49
+ import { portalMagicLinkEmail } from "./templates.js";
50
+ import { defaultMagicLinkEmail } from "./templates.js";
51
+ import emailProvider from "../providers/email.js";
52
+ import { AUTH_VERSION } from "./version.js";
53
+ import { throwAuthError } from "./errors.js";
54
+
55
+ // ============================================================================
56
+ // Types
57
+ // ============================================================================
58
+
59
+ /**
60
+ * Config for the Auth class. Extends the standard auth config
61
+ * minus `component` (which is passed as the first constructor argument).
62
+ *
63
+ * When `email` is configured, the library auto-registers:
64
+ * - A magic link provider (`id: "email"`) for user-facing sign-in
65
+ * - A portal provider (`id: "portal"`) for admin dashboard sign-in
66
+ *
67
+ * Portal functionality is always available — no configuration flag
68
+ * needed. The portal UI works when you export `portalQuery`,
69
+ * `portalMutation`, `portalInternal` from your `convex/auth.ts`
70
+ * and upload the portal static files via CLI.
71
+ */
72
+ export type AuthClassConfig = Omit<ConvexAuthConfig, "component">;
73
+
74
+ // ============================================================================
75
+ // Helpers
76
+ // ============================================================================
77
+
78
+ /**
79
+ * Check if the authenticated user is a portal admin.
80
+ * Uses the new index `roleAndStatusAndAcceptedByUserId` for efficient lookup.
81
+ */
82
+ async function requirePortalAdmin(
83
+ ctx: any,
84
+ authComponent: AuthComponentApi,
85
+ userId: string,
86
+ ): Promise<void> {
87
+ // Use inviteList with status filter, then check role + userId in-memory.
88
+ // The new index makes the status filter efficient.
89
+ const invites = await ctx.runQuery(authComponent.public.inviteList, {
90
+ status: "accepted",
91
+ });
92
+ const isAdmin = invites.some(
93
+ (invite: any) =>
94
+ invite.role === "portalAdmin" && invite.acceptedByUserId === userId,
95
+ );
96
+ if (!isAdmin) {
97
+ throwAuthError("PORTAL_NOT_AUTHORIZED");
98
+ }
99
+ }
100
+
101
+ // ============================================================================
102
+ // Auth class
103
+ // ============================================================================
104
+
105
+ /**
106
+ * Main entry point for Convex Auth. Instantiate with your component
107
+ * reference and config to get all the exports you need.
108
+ *
109
+ * ```ts
110
+ * export const auth = new Auth(components.auth, {
111
+ * providers: [google, password],
112
+ * email: {
113
+ * from: "My App <noreply@example.com>",
114
+ * send: (ctx, params) => resend.sendEmail(ctx, params),
115
+ * },
116
+ * });
117
+ * export const { signIn, signOut, store } = auth;
118
+ * export const { portalQuery, portalMutation, portalInternal } = Portal(auth);
119
+ * ```
120
+ */
121
+ export class Auth {
122
+ /** The inner `auth` helper object from AuthFactory() */
123
+ private readonly _auth: ReturnType<typeof AuthFactory>["auth"];
124
+ /** The signIn action — export this from your convex/auth.ts */
125
+ public readonly signIn: ReturnType<typeof AuthFactory>["signIn"];
126
+ /** The signOut action — export this from your convex/auth.ts */
127
+ public readonly signOut: ReturnType<typeof AuthFactory>["signOut"];
128
+ /** The store internal mutation — export this from your convex/auth.ts */
129
+ public readonly store: ReturnType<typeof AuthFactory>["store"];
130
+
131
+ /** @internal */
132
+ readonly component: AuthComponentApi;
133
+ /** @internal */
134
+ readonly portalUrl: string;
135
+
136
+ // ---- Proxied auth helper sub-objects ----
137
+ /** User helpers: `.current(ctx)`, `.require(ctx)`, `.get(ctx, userId)`, `.patch(ctx, userId, data)`, `.viewer(ctx)`, `.group.list(ctx, ...)`, `.group.get(ctx, ...)` */
138
+ get user() { return this._auth.user; }
139
+ /** Session helpers: `.current(ctx)`, `.invalidate(ctx, { userId, except? })` */
140
+ get session() { return this._auth.session; }
141
+ /** Provider helpers: `.signIn(ctx, provider, args)` */
142
+ get provider() { return this._auth.provider; }
143
+ /** Account helpers: `.create(ctx, args)`, `.get(ctx, args)`, `.updateCredentials(ctx, args)` */
144
+ get account() { return this._auth.account; }
145
+ /** Group helpers: `.create(ctx, ...)`, `.get(ctx, id)`, `.list(ctx, ...)`, `.update(ctx, ...)`, `.delete(ctx, id)`, `.member.*` */
146
+ get group() { return this._auth.group; }
147
+ /** Invite helpers: `.create(ctx, ...)`, `.get(ctx, id)`, `.getByTokenHash(ctx, hash)`, `.list(ctx, ...)`, `.accept(ctx, ...)`, `.revoke(ctx, id)` */
148
+ get invite() { return this._auth.invite; }
149
+ /** Passkey helpers: `.list(ctx, { userId })`, `.rename(ctx, id, name)`, `.remove(ctx, id)` */
150
+ get passkey() { return this._auth.passkey; }
151
+ /** TOTP helpers: `.list(ctx, { userId })`, `.remove(ctx, id)` */
152
+ get totp() { return this._auth.totp; }
153
+ /** API key helpers: `.create(ctx, ...)`, `.verify(ctx, rawKey)`, `.list(ctx, ...)`, `.get(ctx, id)`, `.update(ctx, ...)`, `.revoke(ctx, id)`, `.remove(ctx, id)` */
154
+ get key() { return this._auth.key; }
155
+
156
+ /**
157
+ * @param component - The auth component reference from `components.auth`.
158
+ * @param config - Auth configuration (providers, email transport, session, JWT, callbacks).
159
+ */
160
+ constructor(component: AuthComponentApi, config: AuthClassConfig) {
161
+ this.component = component;
162
+
163
+ // Derive portal URL from CONVEX_SITE_URL
164
+ this.portalUrl = process.env.CONVEX_SITE_URL
165
+ ? `${process.env.CONVEX_SITE_URL.replace(/\/$/, "")}/auth`
166
+ : "/auth";
167
+
168
+ const emailTransport = config.email;
169
+ const providers = [...config.providers];
170
+
171
+ // Auto-register user-facing magic link provider when email is configured.
172
+ // Skipped if the user already registered their own provider with id "email".
173
+ const hasUserEmailProvider = providers.some(
174
+ (p) => typeof p === "object" && "id" in p && p.id === "email",
175
+ );
176
+ if (emailTransport && !hasUserEmailProvider) {
177
+ providers.push(
178
+ emailProvider({
179
+ id: "email",
180
+ maxAge: 60 * 60 * 24, // 24 hours
181
+ authorize: undefined, // Magic link — no OTP email check needed
182
+ async sendVerificationRequest({ identifier, url }, ctx) {
183
+ if (!ctx) {
184
+ throwAuthError("MISSING_ACTION_CONTEXT");
185
+ }
186
+ const { host } = new URL(url);
187
+ await emailTransport.send(ctx, {
188
+ from: emailTransport.from,
189
+ to: identifier,
190
+ subject: `Sign in to ${host}`,
191
+ html: defaultMagicLinkEmail(url, host),
192
+ });
193
+ },
194
+ }),
195
+ );
196
+ }
197
+
198
+ // Auto-register portal admin magic link provider.
199
+ // Uses its own styled dark-theme email template.
200
+ providers.push(
201
+ emailProvider({
202
+ id: "portal",
203
+ maxAge: 60 * 60 * 24, // 24 hours
204
+ authorize: undefined, // Magic link — no OTP email check needed
205
+ async sendVerificationRequest({ identifier, url, expires }, ctx) {
206
+ if (!emailTransport) {
207
+ throwAuthError("EMAIL_CONFIG_REQUIRED");
208
+ }
209
+ if (!ctx) {
210
+ throwAuthError("MISSING_ACTION_CONTEXT");
211
+ }
212
+
213
+ // Check authorization BEFORE sending — only portal-authorized emails
214
+ const invites = await ctx.runQuery(component.public.inviteList, {
215
+ status: "accepted",
216
+ });
217
+ const hasAccess = invites.some(
218
+ (invite: any) => invite.role === "portalAdmin" && invite.email === identifier,
219
+ );
220
+ if (!hasAccess) {
221
+ throwAuthError("PORTAL_NOT_AUTHORIZED");
222
+ }
223
+
224
+ const hours = Math.max(
225
+ 1,
226
+ Math.floor((+expires - Date.now()) / (60 * 60 * 1000)),
227
+ );
228
+ try {
229
+ await emailTransport.send(ctx, {
230
+ from: emailTransport.from,
231
+ to: identifier,
232
+ subject: "Sign in to Auth Portal",
233
+ html: portalMagicLinkEmail(url, hours),
234
+ });
235
+ } catch (e: unknown) {
236
+ throwAuthError(
237
+ "EMAIL_SEND_FAILED",
238
+ "Failed to send portal sign-in email.",
239
+ { detail: e instanceof Error ? e.message : String(e) },
240
+ );
241
+ }
242
+ },
243
+ }),
244
+ );
245
+
246
+ // Initialize the core AuthFactory()
247
+ const authResult = AuthFactory({
248
+ ...config,
249
+ component,
250
+ providers,
251
+ });
252
+
253
+ this._auth = authResult.auth;
254
+ this.signIn = authResult.signIn;
255
+ this.signOut = authResult.signOut;
256
+ this.store = authResult.store;
257
+
258
+ }
259
+
260
+ /**
261
+ * HTTP namespace — route registration, Bearer-authenticated endpoints,
262
+ * and portal static file serving.
263
+ *
264
+ * ```ts
265
+ * // convex/http.ts
266
+ * import { httpRouter } from "convex/server";
267
+ * import { auth } from "./auth";
268
+ *
269
+ * const http = httpRouter();
270
+ * auth.http.add(http);
271
+ * export default http;
272
+ * ```
273
+ */
274
+ get http() {
275
+ // Cache the object so repeated access returns the same reference
276
+ const inner = this._auth.http;
277
+ const component = this.component;
278
+
279
+ return {
280
+ ...inner,
281
+
282
+ /**
283
+ * Register core HTTP routes (OAuth, JWKS) **and** portal static file
284
+ * serving in one call.
285
+ *
286
+ * @param http - The Convex HTTP router to register routes on.
287
+ * @param opts.pathPrefix - URL prefix for portal static files. Defaults to `"/auth"`.
288
+ * @param opts.spaFallback - Serve `index.html` for unmatched sub-paths. Defaults to `true`.
289
+ */
290
+ add(
291
+ http: HttpRouter,
292
+ opts?: { pathPrefix?: string; spaFallback?: boolean },
293
+ ): void {
294
+ // Core auth routes (OAuth, JWKS, etc.)
295
+ inner.add(http);
296
+
297
+ const prefix = opts?.pathPrefix ?? "/auth";
298
+
299
+ // Portal configuration endpoint — serves Convex URLs + version info.
300
+ // The portal SPA fetches this at startup to discover its Convex backend,
301
+ // which is critical for custom domain deployments where the hostname
302
+ // alone doesn't reveal the Convex cloud URL.
303
+ // Registered as an exact path match before the static file prefix catch-all.
304
+ http.route({
305
+ path: `${prefix}/.well-known/portal-config`,
306
+ method: "GET",
307
+ handler: httpActionGeneric(async () => {
308
+ return new Response(
309
+ JSON.stringify({
310
+ convexUrl: process.env.CONVEX_CLOUD_URL,
311
+ siteUrl: process.env.CONVEX_SITE_URL,
312
+ version: AUTH_VERSION,
313
+ }),
314
+ {
315
+ status: 200,
316
+ headers: {
317
+ "Content-Type": "application/json",
318
+ "Cache-Control":
319
+ "public, max-age=60, stale-while-revalidate=60",
320
+ "Access-Control-Allow-Origin": "*",
321
+ },
322
+ },
323
+ );
324
+ }),
325
+ });
326
+
327
+ // Create a shim that maps the self-hosting ComponentApi shape
328
+ // to the auth component's bridge functions
329
+ const selfHostingShim = {
330
+ lib: {
331
+ getByPath: component.bridge.getByPath,
332
+ getCurrentDeployment: component.bridge.getCurrentDeployment,
333
+ listAssets: component.bridge.listAssets,
334
+ recordAsset: component.bridge.recordAsset,
335
+ gcOldAssets: component.bridge.gcOldAssets,
336
+ setCurrentDeployment: component.bridge.setCurrentDeployment,
337
+ // generateUploadUrl is not needed — we use app storage directly
338
+ generateUploadUrl: undefined as any,
339
+ },
340
+ };
341
+
342
+ registerStaticRoutes(http, selfHostingShim as any, {
343
+ pathPrefix: prefix,
344
+ spaFallback: opts?.spaFallback ?? true,
345
+ });
346
+ },
347
+ };
348
+ }
349
+ }
350
+
351
+ // ============================================================================
352
+ // Portal exports (standalone function)
353
+ // ============================================================================
354
+
355
+ /**
356
+ * Create portal function definitions from an `Auth` instance.
357
+ *
358
+ * Standalone function (not a class method) because Convex's bundler
359
+ * can trace `export const { x } = fn(instance)` but not `instance.method()`.
360
+ *
361
+ * ```ts
362
+ * export const { portalQuery, portalMutation, portalInternal } = Portal(auth);
363
+ * ```
364
+ *
365
+ * @param auth - The `Auth` class instance from your `convex/auth.ts`.
366
+ * @returns `{ portalQuery, portalMutation, portalInternal }` — export all three.
367
+ */
368
+ export function Portal(auth: Auth) {
369
+ const authComponent = auth.component;
370
+ const authHelper = (auth as any)._auth;
371
+ const portalUrl = auth.portalUrl;
372
+
373
+ const portalQuery = queryGeneric({
374
+ args: {
375
+ action: v.string(),
376
+ userId: v.optional(v.string()),
377
+ // Group query params
378
+ groupId: v.optional(v.string()),
379
+ groupType: v.optional(v.string()),
380
+ groupParentId: v.optional(v.string()),
381
+ },
382
+ handler: async (
383
+ ctx: any,
384
+ args: {
385
+ action: string;
386
+ userId?: string;
387
+ groupId?: string;
388
+ groupType?: string;
389
+ groupParentId?: string;
390
+ },
391
+ ) => {
392
+ const { action, userId } = args;
393
+ const currentUserId = await authHelper.user.require(ctx);
394
+
395
+ // Allow isAdmin check without admin requirement
396
+ if (action === "isAdmin") {
397
+ try {
398
+ await requirePortalAdmin(ctx, authComponent, currentUserId);
399
+ return true;
400
+ } catch {
401
+ return false;
402
+ }
403
+ }
404
+
405
+ await requirePortalAdmin(ctx, authComponent, currentUserId);
406
+
407
+ switch (action) {
408
+ // ---- Admin-only bulk listing (no public API) ----
409
+ case "listUsers":
410
+ return await ctx.runQuery(authComponent.public.userList);
411
+
412
+ case "listSessions":
413
+ return await ctx.runQuery(authComponent.public.sessionList);
414
+
415
+ case "listKeys":
416
+ return await ctx.runQuery(authComponent.public.keyList);
417
+
418
+ case "getCurrentDeployment":
419
+ return await ctx.runQuery(
420
+ authComponent.bridge.getCurrentDeployment,
421
+ );
422
+
423
+ // ---- User queries (public auth API) ----
424
+ case "getUser":
425
+ return await authHelper.user.get(ctx, userId!);
426
+
427
+ case "getUserSessions":
428
+ return await ctx.runQuery(authComponent.public.sessionListByUser, {
429
+ userId: userId!,
430
+ });
431
+
432
+ case "getUserAccounts": {
433
+ const accounts = await ctx.runQuery(
434
+ authComponent.public.accountListByUser,
435
+ { userId: userId! },
436
+ );
437
+ // Strip secrets — never send password hashes to the frontend
438
+ return accounts.map(({ secret: _, ...rest }: any) => rest);
439
+ }
440
+
441
+ case "getUserKeys":
442
+ return await authHelper.key.list(ctx, { userId: userId! });
443
+
444
+ case "getUserGroups": {
445
+ const memberships = await authHelper.user.group.list(ctx, { userId: userId! });
446
+ // Resolve group details for each membership
447
+ const groups = await Promise.all(
448
+ memberships.map(async (m: any) => {
449
+ const group = await authHelper.group.get(ctx, m.groupId);
450
+ return { ...m, group: group ?? null };
451
+ }),
452
+ );
453
+ return groups;
454
+ }
455
+
456
+ // ---- Key queries (public auth API) ----
457
+ case "getKey":
458
+ return await authHelper.key.get(ctx, userId!); // userId param repurposed as keyId
459
+
460
+ // ---- Group queries (public auth API) ----
461
+ case "listGroups":
462
+ return await authHelper.group.list(ctx, {
463
+ type: args.groupType,
464
+ parentGroupId: args.groupParentId,
465
+ });
466
+
467
+ case "getGroup":
468
+ return await authHelper.group.get(ctx, args.groupId!);
469
+
470
+ case "getGroupMembers":
471
+ return await authHelper.group.member.list(ctx, { groupId: args.groupId! });
472
+
473
+ case "getGroupInvites":
474
+ return await authHelper.invite.list(ctx, { groupId: args.groupId! });
475
+
476
+ // ---- Invite validation (portal context) ----
477
+ case "validateInvite": {
478
+ // userId param repurposed as tokenHash for this action
479
+ const tokenHash = userId;
480
+ if (!tokenHash) throwAuthError("INVITE_TOKEN_REQUIRED");
481
+ const invite = await authHelper.invite.getByTokenHash(ctx, tokenHash);
482
+ if (!invite || invite.status !== "pending") {
483
+ return null;
484
+ }
485
+ if (invite.expiresTime && invite.expiresTime < Date.now()) {
486
+ return null;
487
+ }
488
+ return { _id: invite._id, role: invite.role };
489
+ }
490
+
491
+ default:
492
+ throwAuthError("PORTAL_UNKNOWN_ACTION", `Unknown portal query action: ${action}`);
493
+ }
494
+ },
495
+ });
496
+
497
+ const portalMutation = mutationGeneric({
498
+ args: {
499
+ action: v.string(),
500
+ sessionId: v.optional(v.string()),
501
+ tokenHash: v.optional(v.string()),
502
+ // API key fields
503
+ keyId: v.optional(v.string()),
504
+ keyUserId: v.optional(v.string()),
505
+ keyName: v.optional(v.string()),
506
+ keyScopes: v.optional(
507
+ v.array(
508
+ v.object({
509
+ resource: v.string(),
510
+ actions: v.array(v.string()),
511
+ }),
512
+ ),
513
+ ),
514
+ keyRateLimit: v.optional(
515
+ v.object({
516
+ maxRequests: v.number(),
517
+ windowMs: v.number(),
518
+ }),
519
+ ),
520
+ keyExpiresAt: v.optional(v.number()),
521
+ // Group mutation fields
522
+ groupId: v.optional(v.string()),
523
+ groupName: v.optional(v.string()),
524
+ groupSlug: v.optional(v.string()),
525
+ groupType: v.optional(v.string()),
526
+ groupParentId: v.optional(v.string()),
527
+ groupExtend: v.optional(v.any()),
528
+ memberId: v.optional(v.string()),
529
+ memberUserId: v.optional(v.string()),
530
+ memberRole: v.optional(v.string()),
531
+ memberStatus: v.optional(v.string()),
532
+ },
533
+ handler: async (ctx: any, args: any) => {
534
+ const currentUserId = await authHelper.user.require(ctx);
535
+
536
+ switch (args.action) {
537
+ case "acceptInvite": {
538
+ if (!args.tokenHash) throwAuthError("INVITE_TOKEN_REQUIRED");
539
+ const invite = await authHelper.invite.getByTokenHash(ctx, args.tokenHash);
540
+ if (!invite) throwAuthError("INVALID_INVITE");
541
+ if (invite.status !== "pending") {
542
+ throwAuthError("INVITE_ALREADY_USED", `Invite already ${invite.status}`);
543
+ }
544
+ if (invite.expiresTime && invite.expiresTime < Date.now()) {
545
+ throwAuthError("INVITE_EXPIRED");
546
+ }
547
+ await authHelper.invite.accept(ctx, invite._id, currentUserId);
548
+ return;
549
+ }
550
+
551
+ // ---- Admin-only (no public API for session delete) ----
552
+ case "revokeSession": {
553
+ await requirePortalAdmin(ctx, authComponent, currentUserId);
554
+ await ctx.runMutation(authComponent.public.sessionDelete, {
555
+ sessionId: args.sessionId!,
556
+ });
557
+ return;
558
+ }
559
+
560
+ // ---- API Keys (public auth API) ----
561
+ case "createKey": {
562
+ await requirePortalAdmin(ctx, authComponent, currentUserId);
563
+ return await authHelper.key.create(ctx, {
564
+ userId: args.keyUserId!,
565
+ name: args.keyName!,
566
+ scopes: args.keyScopes ?? [],
567
+ rateLimit: args.keyRateLimit,
568
+ expiresAt: args.keyExpiresAt,
569
+ });
570
+ }
571
+
572
+ case "revokeKey": {
573
+ await requirePortalAdmin(ctx, authComponent, currentUserId);
574
+ await authHelper.key.revoke(ctx, args.keyId!);
575
+ return;
576
+ }
577
+
578
+ case "deleteKey": {
579
+ await requirePortalAdmin(ctx, authComponent, currentUserId);
580
+ await authHelper.key.remove(ctx, args.keyId!);
581
+ return;
582
+ }
583
+
584
+ case "updateKey": {
585
+ await requirePortalAdmin(ctx, authComponent, currentUserId);
586
+ const data: Record<string, any> = {};
587
+ if (args.keyName) data.name = args.keyName;
588
+ if (args.keyScopes) data.scopes = args.keyScopes;
589
+ if (args.keyRateLimit) data.rateLimit = args.keyRateLimit;
590
+ await authHelper.key.update(ctx, args.keyId!, data);
591
+ return;
592
+ }
593
+
594
+ // ---- Groups (public auth API) ----
595
+ case "createGroup": {
596
+ await requirePortalAdmin(ctx, authComponent, currentUserId);
597
+ const groupData: Record<string, any> = { name: args.groupName! };
598
+ if (args.groupSlug) groupData.slug = args.groupSlug;
599
+ if (args.groupType) groupData.type = args.groupType;
600
+ if (args.groupParentId) groupData.parentGroupId = args.groupParentId;
601
+ if (args.groupExtend) groupData.extend = args.groupExtend;
602
+ return await authHelper.group.create(ctx, groupData);
603
+ }
604
+
605
+ case "updateGroup": {
606
+ await requirePortalAdmin(ctx, authComponent, currentUserId);
607
+ const updateData: Record<string, any> = {};
608
+ if (args.groupName) updateData.name = args.groupName;
609
+ if (args.groupSlug !== undefined) updateData.slug = args.groupSlug;
610
+ if (args.groupType !== undefined) updateData.type = args.groupType;
611
+ await authHelper.group.update(ctx, args.groupId!, updateData);
612
+ return;
613
+ }
614
+
615
+ case "deleteGroup": {
616
+ await requirePortalAdmin(ctx, authComponent, currentUserId);
617
+ await authHelper.group.delete(ctx, args.groupId!);
618
+ return;
619
+ }
620
+
621
+ case "addGroupMember": {
622
+ await requirePortalAdmin(ctx, authComponent, currentUserId);
623
+ return await authHelper.group.member.add(ctx, {
624
+ groupId: args.groupId!,
625
+ userId: args.memberUserId!,
626
+ role: args.memberRole,
627
+ status: args.memberStatus,
628
+ });
629
+ }
630
+
631
+ case "removeGroupMember": {
632
+ await requirePortalAdmin(ctx, authComponent, currentUserId);
633
+ await authHelper.group.member.remove(ctx, args.memberId!);
634
+ return;
635
+ }
636
+
637
+ case "updateGroupMember": {
638
+ await requirePortalAdmin(ctx, authComponent, currentUserId);
639
+ const memberData: Record<string, any> = {};
640
+ if (args.memberRole !== undefined) memberData.role = args.memberRole;
641
+ if (args.memberStatus !== undefined) memberData.status = args.memberStatus;
642
+ await authHelper.group.member.update(ctx, args.memberId!, memberData);
643
+ return;
644
+ }
645
+
646
+ default:
647
+ throwAuthError("PORTAL_UNKNOWN_ACTION", `Unknown portal mutation action: ${args.action}`);
648
+ }
649
+ },
650
+ });
651
+
652
+ const portalInternal = internalMutationGeneric({
653
+ args: {
654
+ action: v.string(),
655
+ tokenHash: v.optional(v.string()),
656
+ path: v.optional(v.string()),
657
+ storageId: v.optional(v.string()),
658
+ blobId: v.optional(v.string()),
659
+ contentType: v.optional(v.string()),
660
+ deploymentId: v.optional(v.string()),
661
+ currentDeploymentId: v.optional(v.string()),
662
+ limit: v.optional(v.number()),
663
+ },
664
+ handler: async (ctx: any, args: any) => {
665
+ switch (args.action) {
666
+ // ---- Invite management (CLI) ----
667
+ case "createPortalInvite": {
668
+ await ctx.runMutation(authComponent.public.inviteCreate, {
669
+ tokenHash: args.tokenHash,
670
+ role: "portalAdmin",
671
+ status: "pending" as const,
672
+ });
673
+ return { portalUrl };
674
+ }
675
+
676
+ // ---- Static hosting (CLI upload) ----
677
+ case "generateUploadUrl": {
678
+ return await ctx.storage.generateUploadUrl();
679
+ }
680
+
681
+ case "recordAsset": {
682
+ const { oldStorageId, oldBlobId } = await ctx.runMutation(
683
+ authComponent.bridge.recordAsset,
684
+ {
685
+ path: args.path,
686
+ ...(args.storageId ? { storageId: args.storageId } : {}),
687
+ ...(args.blobId ? { blobId: args.blobId } : {}),
688
+ contentType: args.contentType,
689
+ deploymentId: args.deploymentId,
690
+ },
691
+ );
692
+ if (oldStorageId) {
693
+ try {
694
+ await ctx.storage.delete(oldStorageId);
695
+ } catch {
696
+ // Ignore — old file may have been in different storage
697
+ }
698
+ }
699
+ return oldBlobId ?? null;
700
+ }
701
+
702
+ case "gcOldAssets": {
703
+ const { storageIds, blobIds } = await ctx.runMutation(
704
+ authComponent.bridge.gcOldAssets,
705
+ { currentDeploymentId: args.currentDeploymentId },
706
+ );
707
+ for (const storageId of storageIds) {
708
+ try {
709
+ await ctx.storage.delete(storageId);
710
+ } catch {
711
+ // Ignore
712
+ }
713
+ }
714
+ await ctx.runMutation(
715
+ authComponent.bridge.setCurrentDeployment,
716
+ { deploymentId: args.currentDeploymentId },
717
+ );
718
+ return { deleted: storageIds.length, blobIds };
719
+ }
720
+
721
+ case "listAssets": {
722
+ return await ctx.runQuery(authComponent.bridge.listAssets, {
723
+ limit: args.limit,
724
+ });
725
+ }
726
+
727
+ default:
728
+ throwAuthError("PORTAL_UNKNOWN_ACTION", `Unknown portalInternal action: ${args.action}`);
729
+ }
730
+ },
731
+ });
732
+
733
+ return { portalQuery, portalMutation, portalInternal };
734
+ }
735
+
736
+ // ============================================================================
737
+ // AuthCtx — ctx enrichment for customQuery / customMutation
738
+ // ============================================================================
739
+
740
+ /**
741
+ * The shape of a user document from the auth component's `user` table.
742
+ *
743
+ * Includes system fields (`_id`, `_creationTime`) plus the schema fields
744
+ * (`name`, `email`, `image`, `extend`, etc.).
745
+ */
746
+ export type UserDoc = Doc<"user">;
747
+
748
+ /**
749
+ * Configuration for auth context enrichment.
750
+ *
751
+ * @typeParam TResolve - The shape returned by the `resolve` callback.
752
+ * Inferred automatically — you usually don't need to supply this manually.
753
+ */
754
+ export type AuthCtxConfig<
755
+ TResolve extends Record<string, unknown> = Record<string, never>,
756
+ > = {
757
+ /**
758
+ * When `true`, unauthenticated requests set `ctx.auth.userId` and
759
+ * `ctx.auth.user` to `null` instead of throwing.
760
+ *
761
+ * @default false
762
+ */
763
+ optional?: boolean;
764
+ /**
765
+ * Resolve additional context after authentication succeeds (e.g.
766
+ * group/role for multi-tenant apps). The returned object is spread
767
+ * into `ctx.auth`.
768
+ */
769
+ resolve?: (
770
+ ctx: any,
771
+ user: UserDoc,
772
+ ) => Promise<TResolve> | TResolve;
773
+ };
774
+
775
+ /**
776
+ * Create a `convex-helpers`–compatible customization object that
777
+ * enriches `ctx.auth` with the authenticated user's data.
778
+ *
779
+ * Standalone function (not a class method) because Convex's bundler
780
+ * can trace `export const x = fn(instance)` but not `instance.method()`.
781
+ *
782
+ * ### Basic usage (with `convex-helpers`)
783
+ *
784
+ * ```ts
785
+ * // convex/functions.ts
786
+ * import { customQuery, customMutation } from "convex-helpers/server/customFunctions";
787
+ * import { query as rawQuery, mutation as rawMutation } from "./_generated/server";
788
+ * import { AuthCtx } from "\@robelest/convex-auth/component";
789
+ * import { auth } from "./auth";
790
+ *
791
+ * const authCtx = AuthCtx(auth);
792
+ *
793
+ * export const query = customQuery(rawQuery, authCtx);
794
+ * export const mutation = customMutation(rawMutation, authCtx);
795
+ * ```
796
+ *
797
+ * Then in any function file:
798
+ *
799
+ * ```ts
800
+ * // convex/messages.ts
801
+ * import { query, mutation } from "./functions";
802
+ *
803
+ * export const list = query({
804
+ * args: {},
805
+ * handler: async (ctx) => {
806
+ * // ctx.auth.userId and ctx.auth.user are already resolved
807
+ * return ctx.db.query("messages").collect();
808
+ * },
809
+ * });
810
+ * ```
811
+ *
812
+ * ### Optional auth (public routes)
813
+ *
814
+ * ```ts
815
+ * export const publicQuery = customQuery(rawQuery, AuthCtx(auth, { optional: true }));
816
+ * // ctx.auth.userId is null when unauthenticated
817
+ * ```
818
+ *
819
+ * ### Multi-tenant with group resolution
820
+ *
821
+ * ```ts
822
+ * const authCtx = AuthCtx(auth, {
823
+ * resolve: async (ctx, user) => {
824
+ * const groupId = user?.extend?.lastActiveGroup;
825
+ * const membership = await auth.user.group.get(ctx, {
826
+ * userId: user._id,
827
+ * groupId,
828
+ * });
829
+ * return { groupId, role: membership?.role ?? "member" };
830
+ * },
831
+ * });
832
+ * // ctx.auth.groupId and ctx.auth.role available in handlers
833
+ * ```
834
+ *
835
+ * @param auth - The `Auth` class instance from your `convex/auth.ts`.
836
+ * @param config - Optional configuration for optional auth and group resolution.
837
+ * @returns A `{ args, input }` customization object compatible with
838
+ * `customQuery` / `customMutation` from `convex-helpers`.
839
+ */
840
+ /**
841
+ * Overload: optional auth — `userId` and `user` may be `null`.
842
+ */
843
+ export function AuthCtx<
844
+ TResolve extends Record<string, unknown> = Record<string, never>,
845
+ >(
846
+ auth: Auth,
847
+ config: AuthCtxConfig<TResolve> & { optional: true },
848
+ ): {
849
+ args: {};
850
+ input: (
851
+ ctx: any,
852
+ _args: any,
853
+ _extra?: any,
854
+ ) => Promise<{
855
+ ctx: {
856
+ auth: {
857
+ getUserIdentity: () => Promise<UserIdentity | null>;
858
+ userId: GenericId<"user"> | null;
859
+ user: UserDoc | null;
860
+ } & TResolve;
861
+ };
862
+ args: {};
863
+ }>;
864
+ };
865
+ /**
866
+ * Overload: required auth (default) — `userId` and `user` are never `null`.
867
+ */
868
+ export function AuthCtx<
869
+ TResolve extends Record<string, unknown> = Record<string, never>,
870
+ >(
871
+ auth: Auth,
872
+ config?: AuthCtxConfig<TResolve>,
873
+ ): {
874
+ args: {};
875
+ input: (
876
+ ctx: any,
877
+ _args: any,
878
+ _extra?: any,
879
+ ) => Promise<{
880
+ ctx: {
881
+ auth: {
882
+ getUserIdentity: () => Promise<UserIdentity | null>;
883
+ userId: GenericId<"user">;
884
+ user: UserDoc;
885
+ } & TResolve;
886
+ };
887
+ args: {};
888
+ }>;
889
+ };
890
+ // Implementation
891
+ export function AuthCtx(auth: Auth, config?: AuthCtxConfig<any>) {
892
+ const authHelper = (auth as any)._auth;
893
+
894
+ return {
895
+ args: {},
896
+ input: async (ctx: any, _args: any, _extra?: any) => {
897
+ const nativeAuth = ctx.auth;
898
+
899
+ if (config?.optional) {
900
+ const userId = await authHelper.user.current(ctx);
901
+ if (!userId) {
902
+ return {
903
+ ctx: {
904
+ auth: {
905
+ getUserIdentity: nativeAuth.getUserIdentity.bind(nativeAuth),
906
+ userId: null,
907
+ user: null,
908
+ },
909
+ },
910
+ args: {},
911
+ };
912
+ }
913
+ const user = await authHelper.user.get(ctx, userId);
914
+ const extra = config.resolve
915
+ ? await config.resolve(ctx, user)
916
+ : {};
917
+ return {
918
+ ctx: {
919
+ auth: {
920
+ getUserIdentity: nativeAuth.getUserIdentity.bind(nativeAuth),
921
+ userId,
922
+ user,
923
+ ...extra,
924
+ },
925
+ },
926
+ args: {},
927
+ };
928
+ }
929
+
930
+ // Required mode (default): throws NOT_SIGNED_IN
931
+ const userId = await authHelper.user.require(ctx);
932
+ const user = await authHelper.user.get(ctx, userId);
933
+ const extra = config?.resolve
934
+ ? await config.resolve(ctx, user)
935
+ : {};
936
+
937
+ return {
938
+ ctx: {
939
+ auth: {
940
+ getUserIdentity: nativeAuth.getUserIdentity.bind(nativeAuth),
941
+ userId,
942
+ user,
943
+ ...extra,
944
+ },
945
+ },
946
+ args: {},
947
+ };
948
+ },
949
+ };
950
+ }
951
+
952
+ /**
953
+ * Extract the `ctx.auth` shape from an {@link AuthCtx} result.
954
+ *
955
+ * Follows the same pattern as `Infer<typeof validator>` in Convex
956
+ * and `z.infer<typeof schema>` in Zod.
957
+ *
958
+ * @example
959
+ * ```ts
960
+ * const authCtx = AuthCtx(auth, {
961
+ * resolve: async (ctx, user) => ({ groupId: "abc", role: "admin" }),
962
+ * });
963
+ * type MyAuth = InferAuth<typeof authCtx>;
964
+ * // { getUserIdentity, userId, user, groupId: string, role: string }
965
+ * ```
966
+ */
967
+ export type InferAuth<
968
+ T extends { input: (...args: any[]) => Promise<{ ctx: { auth: any } }> },
969
+ > = Awaited<ReturnType<T["input"]>>["ctx"]["auth"];