convex-zen 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/dist/cli/generate.d.ts +14 -0
  2. package/dist/cli/generate.d.ts.map +1 -0
  3. package/dist/cli/generate.js +297 -0
  4. package/dist/cli/generate.js.map +1 -0
  5. package/dist/cli/index.d.ts +3 -0
  6. package/dist/cli/index.d.ts.map +1 -0
  7. package/dist/cli/index.js +111 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/client/index.d.ts +300 -0
  10. package/dist/client/index.d.ts.map +1 -0
  11. package/dist/client/index.js +434 -0
  12. package/dist/client/index.js.map +1 -0
  13. package/dist/client/plugins/admin.d.ts +92 -0
  14. package/dist/client/plugins/admin.d.ts.map +1 -0
  15. package/dist/client/plugins/admin.js +165 -0
  16. package/dist/client/plugins/admin.js.map +1 -0
  17. package/dist/client/primitives.d.ts +57 -0
  18. package/dist/client/primitives.d.ts.map +1 -0
  19. package/dist/client/primitives.js +64 -0
  20. package/dist/client/primitives.js.map +1 -0
  21. package/dist/client/providers.d.ts +14 -0
  22. package/dist/client/providers.d.ts.map +1 -0
  23. package/dist/client/providers.js +25 -0
  24. package/dist/client/providers.js.map +1 -0
  25. package/dist/client/react.d.ts +23 -0
  26. package/dist/client/react.d.ts.map +1 -0
  27. package/dist/client/react.js +48 -0
  28. package/dist/client/react.js.map +1 -0
  29. package/dist/client/tanstack-start-client-plugins.d.ts +34 -0
  30. package/dist/client/tanstack-start-client-plugins.d.ts.map +1 -0
  31. package/dist/client/tanstack-start-client-plugins.js +32 -0
  32. package/dist/client/tanstack-start-client-plugins.js.map +1 -0
  33. package/dist/client/tanstack-start-client.d.ts +52 -0
  34. package/dist/client/tanstack-start-client.d.ts.map +1 -0
  35. package/dist/client/tanstack-start-client.js +130 -0
  36. package/dist/client/tanstack-start-client.js.map +1 -0
  37. package/dist/client/tanstack-start-plugins.d.ts +27 -0
  38. package/dist/client/tanstack-start-plugins.d.ts.map +1 -0
  39. package/dist/client/tanstack-start-plugins.js +145 -0
  40. package/dist/client/tanstack-start-plugins.js.map +1 -0
  41. package/dist/client/tanstack-start.d.ts +130 -0
  42. package/dist/client/tanstack-start.d.ts.map +1 -0
  43. package/dist/client/tanstack-start.js +331 -0
  44. package/dist/client/tanstack-start.js.map +1 -0
  45. package/dist/component/_generated/api.d.ts +50 -0
  46. package/dist/component/_generated/api.d.ts.map +1 -0
  47. package/dist/component/_generated/api.js +31 -0
  48. package/dist/component/_generated/api.js.map +1 -0
  49. package/dist/component/_generated/component.d.ts +92 -0
  50. package/dist/component/_generated/component.d.ts.map +1 -0
  51. package/dist/component/_generated/component.js +11 -0
  52. package/dist/component/_generated/component.js.map +1 -0
  53. package/dist/component/_generated/dataModel.d.ts +46 -0
  54. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  55. package/dist/component/_generated/dataModel.js +11 -0
  56. package/dist/component/_generated/dataModel.js.map +1 -0
  57. package/dist/component/_generated/server.d.ts +121 -0
  58. package/dist/component/_generated/server.d.ts.map +1 -0
  59. package/dist/component/_generated/server.js +78 -0
  60. package/dist/component/_generated/server.js.map +1 -0
  61. package/dist/component/convex.config.d.ts +3 -0
  62. package/dist/component/convex.config.d.ts.map +1 -0
  63. package/dist/component/convex.config.js +4 -0
  64. package/dist/component/convex.config.js.map +1 -0
  65. package/dist/component/core/sessions.d.ts +33 -0
  66. package/dist/component/core/sessions.d.ts.map +1 -0
  67. package/dist/component/core/sessions.js +186 -0
  68. package/dist/component/core/sessions.js.map +1 -0
  69. package/dist/component/core/users.d.ts +19 -0
  70. package/dist/component/core/users.d.ts.map +1 -0
  71. package/dist/component/core/users.js +154 -0
  72. package/dist/component/core/users.js.map +1 -0
  73. package/dist/component/core/verifications.d.ts +34 -0
  74. package/dist/component/core/verifications.d.ts.map +1 -0
  75. package/dist/component/core/verifications.js +135 -0
  76. package/dist/component/core/verifications.js.map +1 -0
  77. package/dist/component/gateway.d.ts +16 -0
  78. package/dist/component/gateway.d.ts.map +1 -0
  79. package/dist/component/gateway.js +229 -0
  80. package/dist/component/gateway.js.map +1 -0
  81. package/dist/component/lib/crypto.d.ts +24 -0
  82. package/dist/component/lib/crypto.d.ts.map +1 -0
  83. package/dist/component/lib/crypto.js +57 -0
  84. package/dist/component/lib/crypto.js.map +1 -0
  85. package/dist/component/lib/rateLimit.d.ts +26 -0
  86. package/dist/component/lib/rateLimit.d.ts.map +1 -0
  87. package/dist/component/lib/rateLimit.js +96 -0
  88. package/dist/component/lib/rateLimit.js.map +1 -0
  89. package/dist/component/lib/validators.d.ts +19 -0
  90. package/dist/component/lib/validators.d.ts.map +1 -0
  91. package/dist/component/lib/validators.js +12 -0
  92. package/dist/component/lib/validators.js.map +1 -0
  93. package/dist/component/plugins/admin.d.ts +72 -0
  94. package/dist/component/plugins/admin.d.ts.map +1 -0
  95. package/dist/component/plugins/admin.js +152 -0
  96. package/dist/component/plugins/admin.js.map +1 -0
  97. package/dist/component/providers/emailPassword.d.ts +49 -0
  98. package/dist/component/providers/emailPassword.d.ts.map +1 -0
  99. package/dist/component/providers/emailPassword.js +316 -0
  100. package/dist/component/providers/emailPassword.js.map +1 -0
  101. package/dist/component/providers/oauth.d.ts +33 -0
  102. package/dist/component/providers/oauth.d.ts.map +1 -0
  103. package/dist/component/providers/oauth.js +256 -0
  104. package/dist/component/providers/oauth.js.map +1 -0
  105. package/dist/component/schema.d.ts +132 -0
  106. package/dist/component/schema.d.ts.map +1 -0
  107. package/dist/component/schema.js +82 -0
  108. package/dist/component/schema.js.map +1 -0
  109. package/dist/types.d.ts +67 -0
  110. package/dist/types.d.ts.map +1 -0
  111. package/dist/types.js +5 -0
  112. package/dist/types.js.map +1 -0
  113. package/package.json +121 -0
  114. package/src/cli/generate.ts +360 -0
  115. package/src/cli/index.ts +133 -0
  116. package/src/client/index.ts +707 -0
  117. package/src/client/plugins/admin.ts +205 -0
  118. package/src/client/primitives.ts +100 -0
  119. package/src/client/providers.ts +35 -0
  120. package/src/client/react.ts +97 -0
  121. package/src/client/tanstack-start-client-plugins.ts +113 -0
  122. package/src/client/tanstack-start-client.ts +259 -0
  123. package/src/client/tanstack-start-plugins.ts +203 -0
  124. package/src/client/tanstack-start.ts +535 -0
  125. package/src/component/_generated/api.ts +70 -0
  126. package/src/component/_generated/component.ts +184 -0
  127. package/src/component/_generated/dataModel.ts +60 -0
  128. package/src/component/_generated/server.ts +156 -0
  129. package/src/component/convex.config.ts +5 -0
  130. package/src/component/core/sessions.ts +228 -0
  131. package/src/component/core/users.ts +199 -0
  132. package/src/component/core/verifications.ts +173 -0
  133. package/src/component/gateway.ts +321 -0
  134. package/src/component/lib/crypto.ts +63 -0
  135. package/src/component/lib/internalApi.ts +66 -0
  136. package/src/component/lib/rateLimit.ts +111 -0
  137. package/src/component/lib/validators.ts +12 -0
  138. package/src/component/plugins/admin.ts +178 -0
  139. package/src/component/providers/emailPassword.ts +374 -0
  140. package/src/component/providers/oauth.ts +324 -0
  141. package/src/component/schema.ts +88 -0
  142. package/src/types.ts +68 -0
@@ -0,0 +1,707 @@
1
+ import type {
2
+ EmailProvider,
3
+ ConvexAuthPlugin,
4
+ OAuthProviderConfig,
5
+ AdminPluginConfig,
6
+ } from "../types";
7
+ import { AdminPlugin } from "./plugins/admin";
8
+
9
+ /**
10
+ * Minimal Convex context interfaces. Parameters use `any` so that Convex's
11
+ * actual generic context types (which have typed FunctionReference params)
12
+ * satisfy these interfaces without triggering contravariant type errors.
13
+ */
14
+ interface RunsQueries {
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ runQuery: (fn: any, args: any) => Promise<any>;
17
+ }
18
+ interface RunsMutations {
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ runMutation: (fn: any, args: any) => Promise<any>;
21
+ }
22
+ interface RunsActions {
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ runAction: (fn: any, args: any) => Promise<any>;
25
+ }
26
+ type ConvexCtx = RunsQueries & RunsMutations & RunsActions;
27
+ type PluginList = readonly ConvexAuthPlugin[];
28
+ type AdminPluginFor<TPlugins extends PluginList> =
29
+ Extract<TPlugins[number], { id: "admin" }> extends never
30
+ ? null
31
+ : AdminPlugin;
32
+ type MaybePromise<T> = T | Promise<T>;
33
+ type AuthIdentity = {
34
+ subject?: string | null;
35
+ [key: string]: unknown;
36
+ };
37
+
38
+ interface AdminFacade {
39
+ listUsers: (
40
+ ctx: RunsActions,
41
+ args?: { token?: string; limit?: number; cursor?: string }
42
+ ) => Promise<unknown>;
43
+ banUser: (
44
+ ctx: RunsActions,
45
+ args: { userId: string; reason?: string; expiresAt?: number; token?: string }
46
+ ) => Promise<unknown>;
47
+ unbanUser: (
48
+ ctx: RunsActions,
49
+ args: { userId: string; token?: string }
50
+ ) => Promise<unknown>;
51
+ setRole: (
52
+ ctx: RunsActions,
53
+ args: { userId: string; role: string; token?: string }
54
+ ) => Promise<unknown>;
55
+ deleteUser: (
56
+ ctx: RunsActions,
57
+ args: { userId: string; token?: string }
58
+ ) => Promise<unknown>;
59
+ }
60
+ type AdminFacadeFor<TPlugins extends PluginList> =
61
+ Extract<TPlugins[number], { id: "admin" }> extends never
62
+ ? null
63
+ : AdminFacade;
64
+
65
+ export interface AuthUser {
66
+ _id: string;
67
+ email: string;
68
+ emailVerified: boolean;
69
+ name?: string;
70
+ image?: string;
71
+ role?: string;
72
+ banned?: boolean;
73
+ banReason?: string;
74
+ banExpires?: number;
75
+ createdAt?: number;
76
+ updatedAt?: number;
77
+ }
78
+
79
+ /** ConvexAuth constructor options. */
80
+ export interface ConvexAuthOptions<TPlugins extends PluginList = PluginList> {
81
+ providers?: OAuthProviderConfig[];
82
+ emailProvider?: EmailProvider;
83
+ plugins?: TPlugins;
84
+ /** Require email verification before sign-in. Default: true. */
85
+ requireEmailVerified?: boolean;
86
+ /**
87
+ * Optional global token resolver so callers can do auth APIs with only `ctx`.
88
+ * Commonly reads from framework/identity context.
89
+ */
90
+ resolveSessionToken?: (ctx: unknown) => MaybePromise<string | null>;
91
+ /**
92
+ * Optional global userId resolver when using identity-based auth.
93
+ * Defaults to `ctx.auth.getUserIdentity()?.subject` when available.
94
+ */
95
+ resolveUserId?: (ctx: unknown) => MaybePromise<string | null>;
96
+ }
97
+
98
+ /**
99
+ * ConvexAuth — the main integration class for convex-zen.
100
+ *
101
+ * Instantiate once in the host app with the component reference and config.
102
+ * All auth operations go through this class.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * // convex/auth.ts
107
+ * import { ConvexAuth, googleProvider } from "convex-zen";
108
+ * import { adminPlugin } from "convex-zen/plugins/admin";
109
+ * import { components } from "./_generated/api";
110
+ *
111
+ * export const auth = new ConvexAuth(components.convexAuth, {
112
+ * providers: [googleProvider({ clientId: "...", clientSecret: "..." })],
113
+ * emailProvider: {
114
+ * sendVerificationEmail: async (to, code) => { ... },
115
+ * sendPasswordResetEmail: async (to, code) => { ... },
116
+ * },
117
+ * plugins: [adminPlugin({ defaultRole: "user" })],
118
+ * });
119
+ * ```
120
+ */
121
+ export class ConvexAuth<TPlugins extends PluginList = PluginList> {
122
+ private readonly component: Record<string, unknown>;
123
+ private readonly options: ConvexAuthOptions<TPlugins>;
124
+ private readonly _adminPlugin: AdminPlugin | null;
125
+ private readonly providerMap: Map<string, OAuthProviderConfig>;
126
+ private readonly adminConfig: AdminPluginConfig | null;
127
+
128
+ constructor(
129
+ component: Record<string, unknown>,
130
+ options: ConvexAuthOptions<TPlugins> = {}
131
+ ) {
132
+ this.component = component;
133
+ this.options = options;
134
+
135
+ this.providerMap = new Map(
136
+ (options.providers ?? []).map((p) => [p.id, p])
137
+ );
138
+
139
+ this.adminConfig =
140
+ options.plugins?.find(
141
+ (p): p is AdminPluginConfig => p.id === "admin"
142
+ ) ?? null;
143
+
144
+ this._adminPlugin = this.adminConfig
145
+ ? new AdminPlugin(component, this.adminConfig)
146
+ : null;
147
+ }
148
+
149
+ /** Access plugin instances after initialization. */
150
+ get plugins() {
151
+ return {
152
+ admin: this._adminPlugin as AdminPluginFor<TPlugins>,
153
+ };
154
+ }
155
+
156
+ /** Alias for plugins to support auth.plugin.admin style access. */
157
+ get plugin() {
158
+ return this.plugins;
159
+ }
160
+
161
+ /** Admin facade on the auth object (no separate authSystem object required). */
162
+ get admin(): AdminFacadeFor<TPlugins> {
163
+ if (!this._adminPlugin) {
164
+ return null as AdminFacadeFor<TPlugins>;
165
+ }
166
+ return {
167
+ listUsers: async (ctx, args = {}) => {
168
+ const adminToken = await this.requireResolvedToken(ctx, args.token);
169
+ const payload: {
170
+ adminToken: string;
171
+ limit?: number;
172
+ cursor?: string;
173
+ } = { adminToken };
174
+ if (args.limit !== undefined) {
175
+ payload.limit = args.limit;
176
+ }
177
+ if (args.cursor !== undefined) {
178
+ payload.cursor = args.cursor;
179
+ }
180
+ return this._adminPlugin!.listUsers(ctx, payload);
181
+ },
182
+ banUser: async (ctx, args) => {
183
+ const adminToken = await this.requireResolvedToken(ctx, args.token);
184
+ const payload: {
185
+ adminToken: string;
186
+ userId: string;
187
+ reason?: string;
188
+ expiresAt?: number;
189
+ } = {
190
+ adminToken,
191
+ userId: args.userId,
192
+ };
193
+ if (args.reason !== undefined) {
194
+ payload.reason = args.reason;
195
+ }
196
+ if (args.expiresAt !== undefined) {
197
+ payload.expiresAt = args.expiresAt;
198
+ }
199
+ return this._adminPlugin!.banUser(ctx, payload);
200
+ },
201
+ unbanUser: async (ctx, args) => {
202
+ const adminToken = await this.requireResolvedToken(ctx, args.token);
203
+ return this._adminPlugin!.unbanUser(ctx, {
204
+ adminToken,
205
+ userId: args.userId,
206
+ });
207
+ },
208
+ setRole: async (ctx, args) => {
209
+ const adminToken = await this.requireResolvedToken(ctx, args.token);
210
+ return this._adminPlugin!.setRole(ctx, {
211
+ adminToken,
212
+ userId: args.userId,
213
+ role: args.role,
214
+ });
215
+ },
216
+ deleteUser: async (ctx, args) => {
217
+ const adminToken = await this.requireResolvedToken(ctx, args.token);
218
+ return this._adminPlugin!.deleteUser(ctx, {
219
+ adminToken,
220
+ userId: args.userId,
221
+ });
222
+ },
223
+ } as AdminFacadeFor<TPlugins>;
224
+ }
225
+
226
+ /** Session helpers available directly on auth object. */
227
+ get session() {
228
+ return {
229
+ validate: async (ctx: ConvexCtx, token?: string) => {
230
+ return this.safeValidateSession(ctx, token);
231
+ },
232
+ require: async (ctx: ConvexCtx, token?: string) => {
233
+ return this.requireAuthSession(ctx, token);
234
+ },
235
+ };
236
+ }
237
+
238
+ /** Authenticated user helpers available directly on auth object. */
239
+ get user() {
240
+ return {
241
+ safeGet: async (ctx: ConvexCtx, token?: string) => {
242
+ return this.safeGetAuthUser(ctx, token);
243
+ },
244
+ require: async (ctx: ConvexCtx, token?: string) => {
245
+ return this.requireAuthUser(ctx, token);
246
+ },
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Register OAuth callback HTTP routes on the host's router.
252
+ * Mounts GET `/auth/callback/:provider` for each configured OAuth provider.
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * // convex/http.ts
257
+ * import { httpRouter } from "convex/server";
258
+ * import { auth } from "./auth";
259
+ * const http = httpRouter();
260
+ * auth.registerRoutes(http);
261
+ * export default http;
262
+ * ```
263
+ */
264
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
265
+ registerRoutes(http: any, options?: { callbackBaseUrl?: string }): void {
266
+ for (const provider of this.providerMap.values()) {
267
+ const path = `/auth/callback/${provider.id}`;
268
+
269
+ http.route({
270
+ path,
271
+ method: "GET",
272
+ handler: async (req: Request) => {
273
+ const url = new URL(req.url);
274
+ const code = url.searchParams.get("code");
275
+ const state = url.searchParams.get("state");
276
+ const error = url.searchParams.get("error");
277
+
278
+ if (error) {
279
+ return new Response(JSON.stringify({ error }), {
280
+ status: 400,
281
+ headers: { "Content-Type": "application/json" },
282
+ });
283
+ }
284
+
285
+ if (!code || !state) {
286
+ return new Response(
287
+ JSON.stringify({ error: "Missing code or state" }),
288
+ { status: 400, headers: { "Content-Type": "application/json" } }
289
+ );
290
+ }
291
+
292
+ // Note: HTTP handlers can't use ctx directly.
293
+ // Host app should use a Convex HTTP action that calls auth.handleCallback.
294
+ // This default handler returns the raw data for the host to process.
295
+ return new Response(
296
+ JSON.stringify({
297
+ code,
298
+ state,
299
+ provider: provider.id,
300
+ callbackUrl: options?.callbackBaseUrl
301
+ ? `${options.callbackBaseUrl}${path}`
302
+ : undefined,
303
+ }),
304
+ {
305
+ status: 200,
306
+ headers: { "Content-Type": "application/json" },
307
+ }
308
+ );
309
+ },
310
+ });
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Sign up a new user with email and password.
316
+ * Generates a verification code and sends it via the configured emailProvider.
317
+ * Call from a Convex action in the host app.
318
+ */
319
+ async signUp(
320
+ ctx: ConvexCtx,
321
+ args: {
322
+ email: string;
323
+ password: string;
324
+ name?: string;
325
+ ipAddress?: string;
326
+ }
327
+ ): Promise<{ status: "verification_required" }> {
328
+ if (!this.options.emailProvider) {
329
+ throw new Error("emailProvider is required for email/password auth");
330
+ }
331
+
332
+ const result = (await ctx.runAction(
333
+ this.fn("gateway:signUp"),
334
+ args
335
+ )) as { status: "verification_required"; verificationCode: string };
336
+
337
+ // Send email from host app context (functions can't be Convex args)
338
+ await this.options.emailProvider.sendVerificationEmail(
339
+ args.email,
340
+ result.verificationCode
341
+ );
342
+
343
+ return { status: "verification_required" };
344
+ }
345
+
346
+ /**
347
+ * Sign in with email and password.
348
+ * Call from a Convex action in the host app.
349
+ */
350
+ async signIn(
351
+ ctx: ConvexCtx,
352
+ args: {
353
+ email: string;
354
+ password: string;
355
+ ipAddress?: string;
356
+ userAgent?: string;
357
+ }
358
+ ): Promise<{ sessionToken: string; userId: string }> {
359
+ return ctx.runAction(this.fn("gateway:signIn"), {
360
+ ...args,
361
+ requireEmailVerified: this.options.requireEmailVerified ?? true,
362
+ }) as Promise<{ sessionToken: string; userId: string }>;
363
+ }
364
+
365
+ /**
366
+ * Verify email with a verification code.
367
+ */
368
+ async verifyEmail(
369
+ ctx: ConvexCtx,
370
+ args: { email: string; code: string }
371
+ ): Promise<{ status: string }> {
372
+ return ctx.runAction(
373
+ this.fn("gateway:verifyEmail"),
374
+ args
375
+ ) as Promise<{ status: string }>;
376
+ }
377
+
378
+ /**
379
+ * Request a password reset. Sends the reset code via emailProvider.
380
+ */
381
+ async requestPasswordReset(
382
+ ctx: ConvexCtx,
383
+ args: { email: string; ipAddress?: string }
384
+ ): Promise<{ status: "sent" }> {
385
+ if (!this.options.emailProvider) {
386
+ throw new Error("emailProvider is required for password reset");
387
+ }
388
+
389
+ const result = (await ctx.runAction(
390
+ this.fn("gateway:requestPasswordReset"),
391
+ args
392
+ )) as { status: "sent"; resetCode: string | null };
393
+
394
+ if (result.resetCode) {
395
+ await this.options.emailProvider.sendPasswordResetEmail(
396
+ args.email,
397
+ result.resetCode
398
+ );
399
+ }
400
+
401
+ return { status: "sent" };
402
+ }
403
+
404
+ /**
405
+ * Reset password using a verification code.
406
+ */
407
+ async resetPassword(
408
+ ctx: ConvexCtx,
409
+ args: { email: string; code: string; newPassword: string }
410
+ ): Promise<{ status: string }> {
411
+ return ctx.runAction(
412
+ this.fn("gateway:resetPassword"),
413
+ args
414
+ ) as Promise<{ status: string }>;
415
+ }
416
+
417
+ /**
418
+ * Get an OAuth authorization URL for the given provider.
419
+ * Call from a Convex action.
420
+ */
421
+ async getOAuthUrl(
422
+ ctx: ConvexCtx,
423
+ providerId: string,
424
+ redirectUrl?: string
425
+ ): Promise<{ authorizationUrl: string }> {
426
+ const provider = this.providerMap.get(providerId);
427
+ if (!provider) {
428
+ throw new Error(`OAuth provider "${providerId}" not configured`);
429
+ }
430
+ return ctx.runAction(this.fn("gateway:getAuthorizationUrl"), {
431
+ provider,
432
+ redirectUrl,
433
+ }) as Promise<{ authorizationUrl: string }>;
434
+ }
435
+
436
+ /**
437
+ * Handle an OAuth callback. Call from a Convex HTTP action.
438
+ * Validates state, exchanges code for tokens, upserts user, creates session.
439
+ */
440
+ async handleCallback(
441
+ ctx: ConvexCtx,
442
+ args: {
443
+ code: string;
444
+ state: string;
445
+ providerId: string;
446
+ ipAddress?: string;
447
+ userAgent?: string;
448
+ redirectUrl?: string;
449
+ }
450
+ ): Promise<{ sessionToken: string; userId: string; redirectUrl?: string }> {
451
+ const provider = this.providerMap.get(args.providerId);
452
+ if (!provider) {
453
+ throw new Error(`OAuth provider "${args.providerId}" not configured`);
454
+ }
455
+ return ctx.runAction(this.fn("gateway:handleCallback"), {
456
+ provider,
457
+ code: args.code,
458
+ state: args.state,
459
+ redirectUrl: args.redirectUrl,
460
+ ipAddress: args.ipAddress,
461
+ userAgent: args.userAgent,
462
+ }) as Promise<{
463
+ sessionToken: string;
464
+ userId: string;
465
+ redirectUrl?: string;
466
+ }>;
467
+ }
468
+
469
+ /**
470
+ * Validate a session token. Returns user/session IDs or null.
471
+ * Can be called from queries, mutations, or actions.
472
+ */
473
+ async validateSession(
474
+ ctx: ConvexCtx,
475
+ token: string
476
+ ): Promise<{ userId: string; sessionId: string } | null> {
477
+ return ctx.runAction(this.fn("gateway:validateSession"), {
478
+ token,
479
+ checkBanned: this.adminConfig !== null,
480
+ }) as Promise<{ userId: string; sessionId: string } | null>;
481
+ }
482
+
483
+ /**
484
+ * Validate session, resolving token from context when one is not provided.
485
+ */
486
+ async safeValidateSession(
487
+ ctx: ConvexCtx,
488
+ token?: string
489
+ ): Promise<{ userId: string; sessionId: string } | null> {
490
+ const resolvedToken = await this.resolveToken(ctx, token);
491
+ if (!resolvedToken) {
492
+ return null;
493
+ }
494
+ return this.validateSession(ctx, resolvedToken);
495
+ }
496
+
497
+ /**
498
+ * Validate session and throw when missing/invalid.
499
+ */
500
+ async requireAuthSession(
501
+ ctx: ConvexCtx,
502
+ token?: string
503
+ ): Promise<{ userId: string; sessionId: string }> {
504
+ const session = await this.safeValidateSession(ctx, token);
505
+ if (!session) {
506
+ throw new Error("Unauthorized");
507
+ }
508
+ return session;
509
+ }
510
+
511
+ /**
512
+ * Resolve the current signed-in user from a session token.
513
+ * Returns null when token is invalid/expired/banned.
514
+ */
515
+ async getAuthUserFromToken(
516
+ ctx: ConvexCtx,
517
+ token: string
518
+ ): Promise<AuthUser | null> {
519
+ return ctx.runAction(this.fn("gateway:getCurrentUser"), {
520
+ token,
521
+ checkBanned: this.adminConfig !== null,
522
+ }) as Promise<AuthUser | null>;
523
+ }
524
+
525
+ /**
526
+ * Resolve authenticated user from `ctx` if possible.
527
+ * Resolution order:
528
+ * 1. explicit token argument
529
+ * 2. resolveSessionToken option
530
+ * 3. identity claims (session token-like fields)
531
+ * 4. resolveUserId option
532
+ * 5. `ctx.auth.getUserIdentity()?.subject`
533
+ */
534
+ async safeGetAuthUser(
535
+ ctx: ConvexCtx,
536
+ token?: string
537
+ ): Promise<AuthUser | null> {
538
+ const resolvedToken = await this.resolveToken(ctx, token);
539
+ if (resolvedToken) {
540
+ return this.getAuthUserFromToken(ctx, resolvedToken);
541
+ }
542
+
543
+ const userId = await this.resolveUserId(ctx);
544
+ if (!userId) {
545
+ return null;
546
+ }
547
+
548
+ return ctx.runAction(this.fn("gateway:getUserById"), {
549
+ userId,
550
+ checkBanned: this.adminConfig !== null,
551
+ }) as Promise<AuthUser | null>;
552
+ }
553
+
554
+ /**
555
+ * Resolve current user and throw if unauthenticated.
556
+ */
557
+ async requireAuthUser(
558
+ ctx: ConvexCtx,
559
+ token?: string
560
+ ): Promise<AuthUser> {
561
+ const user = await this.safeGetAuthUser(ctx, token);
562
+ if (!user) {
563
+ throw new Error("Unauthorized");
564
+ }
565
+ return user;
566
+ }
567
+
568
+ /**
569
+ * Sign out by invalidating a session token.
570
+ * Can be called from a mutation or action context.
571
+ */
572
+ async signOut(ctx: RunsActions, token: string): Promise<void> {
573
+ await ctx.runAction(this.fn("gateway:invalidateSession"), { token });
574
+ }
575
+
576
+ /**
577
+ * Sign out all sessions for a user.
578
+ */
579
+ async signOutAll(ctx: RunsActions, userId: string): Promise<void> {
580
+ await ctx.runAction(this.fn("gateway:invalidateAllSessions"), { userId });
581
+ }
582
+
583
+ private async getIdentity(ctx: unknown): Promise<AuthIdentity | null> {
584
+ const auth = (ctx as { auth?: { getUserIdentity?: () => Promise<unknown> } })
585
+ .auth;
586
+ if (!auth?.getUserIdentity) {
587
+ return null;
588
+ }
589
+ const identity = await auth.getUserIdentity();
590
+ if (!identity || typeof identity !== "object") {
591
+ return null;
592
+ }
593
+ return identity as AuthIdentity;
594
+ }
595
+
596
+ private getStringClaim(
597
+ identity: AuthIdentity | null,
598
+ claim: string
599
+ ): string | null {
600
+ if (!identity) {
601
+ return null;
602
+ }
603
+ const value = identity[claim];
604
+ return typeof value === "string" && value.length > 0 ? value : null;
605
+ }
606
+
607
+ private async resolveToken(
608
+ ctx: unknown,
609
+ explicitToken?: string
610
+ ): Promise<string | null> {
611
+ if (explicitToken) {
612
+ return explicitToken;
613
+ }
614
+
615
+ const tokenFromResolver = await this.options.resolveSessionToken?.(ctx);
616
+ if (tokenFromResolver) {
617
+ return tokenFromResolver;
618
+ }
619
+
620
+ const identity = await this.getIdentity(ctx);
621
+ return (
622
+ this.getStringClaim(identity, "sessionToken") ??
623
+ this.getStringClaim(identity, "token") ??
624
+ this.getStringClaim(identity, "https://convex-zen.dev/sessionToken") ??
625
+ null
626
+ );
627
+ }
628
+
629
+ private async requireResolvedToken(
630
+ ctx: unknown,
631
+ explicitToken?: string
632
+ ): Promise<string> {
633
+ const token = await this.resolveToken(ctx, explicitToken);
634
+ if (!token) {
635
+ throw new Error("Unauthorized");
636
+ }
637
+ return token;
638
+ }
639
+
640
+ private async resolveUserId(ctx: unknown): Promise<string | null> {
641
+ const userIdFromResolver = await this.options.resolveUserId?.(ctx);
642
+ if (userIdFromResolver) {
643
+ return userIdFromResolver;
644
+ }
645
+
646
+ const identity = await this.getIdentity(ctx);
647
+ if (!identity) {
648
+ return null;
649
+ }
650
+
651
+ if (typeof identity.subject === "string" && identity.subject.length > 0) {
652
+ return identity.subject;
653
+ }
654
+ return this.getStringClaim(identity, "userId");
655
+ }
656
+
657
+ /**
658
+ * Resolve a component function reference by path string.
659
+ * Converts "providers/emailPassword:signUp" into
660
+ * component.providers.emailPassword.signUp via nested property traversal.
661
+ */
662
+ private fn(path: string): unknown {
663
+ const [modulePath, funcName] = path.split(":");
664
+ if (!modulePath || !funcName) {
665
+ throw new Error(`Invalid function path: ${path}`);
666
+ }
667
+ const parts = modulePath.split("/");
668
+ let ref: Record<string, unknown> = this.component;
669
+ for (const part of parts) {
670
+ const next = ref[part];
671
+ if (
672
+ !next ||
673
+ typeof next !== "object" ||
674
+ Array.isArray(next)
675
+ ) {
676
+ throw new Error(`Invalid function path segment: ${part}`);
677
+ }
678
+ ref = next as Record<string, unknown>;
679
+ }
680
+ const resolved = ref[funcName];
681
+ if (!resolved) {
682
+ throw new Error(`Function not found: ${path}`);
683
+ }
684
+ return resolved;
685
+ }
686
+ }
687
+
688
+ // Named exports for convenience
689
+ export { googleProvider, githubProvider } from "./providers";
690
+ export { adminPlugin } from "./plugins/admin";
691
+ export {
692
+ SessionPrimitives,
693
+ createSessionPrimitives,
694
+ } from "./primitives";
695
+ export type {
696
+ ConvexAuthPlugin,
697
+ EmailProvider,
698
+ OAuthProviderConfig,
699
+ AdminPluginConfig,
700
+ } from "../types";
701
+ export type {
702
+ SessionInfo,
703
+ SignInInput,
704
+ SignInOutput,
705
+ SessionTransport,
706
+ EstablishedSession,
707
+ } from "./primitives";