@robelest/convex-auth 0.0.4-preview.30 → 0.0.4-preview.31

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 (105) hide show
  1. package/dist/bin.js +125 -36
  2. package/dist/browser/index.d.ts +3 -13
  3. package/dist/browser/index.js +47 -12
  4. package/dist/browser/navigation.js +1 -1
  5. package/dist/browser/passkey.js +7 -7
  6. package/dist/browser/runtime.js +13 -15
  7. package/dist/client/core/types.d.ts +179 -63
  8. package/dist/client/core/types.js +6 -0
  9. package/dist/client/factors/totp.js +1 -1
  10. package/dist/client/index.d.ts +5 -4
  11. package/dist/client/index.js +115 -56
  12. package/dist/client/runtime/mutex.js +3 -2
  13. package/dist/component/_generated/component.d.ts +40 -0
  14. package/dist/component/convex.config.d.ts +2 -2
  15. package/dist/component/http.js +9 -0
  16. package/dist/component/index.d.ts +1 -1
  17. package/dist/component/model.d.ts +25 -25
  18. package/dist/component/model.js +2 -1
  19. package/dist/component/modules.js +1 -0
  20. package/dist/component/public/factors/passkeys.js +31 -1
  21. package/dist/component/public/identity/codes.js +1 -1
  22. package/dist/component/public/identity/tokens.js +2 -1
  23. package/dist/component/public/identity/verifiers.js +15 -5
  24. package/dist/component/public.js +2 -2
  25. package/dist/component/schema.d.ts +292 -290
  26. package/dist/component/schema.js +2 -1
  27. package/dist/core/index.d.ts +8 -3
  28. package/dist/core/index.js +7 -2
  29. package/dist/expo/index.d.ts +21 -0
  30. package/dist/expo/index.js +148 -0
  31. package/dist/expo/passkey.js +174 -0
  32. package/dist/providers/apple.d.ts +1 -1
  33. package/dist/providers/apple.js +6 -8
  34. package/dist/providers/custom.d.ts +1 -1
  35. package/dist/providers/custom.js +4 -7
  36. package/dist/providers/github.d.ts +1 -1
  37. package/dist/providers/github.js +5 -8
  38. package/dist/providers/google.d.ts +1 -1
  39. package/dist/providers/google.js +5 -8
  40. package/dist/providers/microsoft.d.ts +1 -1
  41. package/dist/providers/microsoft.js +5 -9
  42. package/dist/providers/password.d.ts +18 -37
  43. package/dist/providers/password.js +170 -115
  44. package/dist/providers/redirect.d.ts +1 -0
  45. package/dist/providers/redirect.js +20 -0
  46. package/dist/server/auth.d.ts +6 -7
  47. package/dist/server/auth.js +3 -2
  48. package/dist/server/{ctxCache.js → cache/context.js} +2 -2
  49. package/dist/server/{componentContext.d.ts → component/context.d.ts} +2 -2
  50. package/dist/server/context.js +3 -10
  51. package/dist/server/contract.d.ts +2 -87
  52. package/dist/server/contract.js +1 -1
  53. package/dist/server/cookies.js +25 -1
  54. package/dist/server/core.js +1 -1
  55. package/dist/server/errors.js +24 -1
  56. package/dist/server/{auth-context.d.ts → facade.d.ts} +3 -45
  57. package/dist/server/{auth-context.js → facade.js} +11 -12
  58. package/dist/server/http.d.ts +7 -7
  59. package/dist/server/http.js +36 -7
  60. package/dist/server/{convexIdentity.d.ts → identity/convex.d.ts} +3 -3
  61. package/dist/server/index.d.ts +5 -3
  62. package/dist/server/index.js +3 -2
  63. package/dist/server/mounts.d.ts +175 -22
  64. package/dist/server/mutations/code.js +7 -1
  65. package/dist/server/mutations/{credentialsSignIn.js → credentials/signin.js} +10 -10
  66. package/dist/server/mutations/index.js +1 -1
  67. package/dist/server/mutations/invalidate.js +11 -1
  68. package/dist/server/mutations/oauth.js +25 -27
  69. package/dist/server/mutations/signin.js +6 -0
  70. package/dist/server/mutations/signout.js +5 -0
  71. package/dist/server/mutations/store.js +1 -1
  72. package/dist/server/oauth/factory.js +11 -3
  73. package/dist/server/passkey.js +126 -110
  74. package/dist/server/prefetch.js +8 -1
  75. package/dist/server/redirects.js +11 -3
  76. package/dist/server/refresh.js +6 -1
  77. package/dist/server/runtime.d.ts +58 -36
  78. package/dist/server/runtime.js +333 -36
  79. package/dist/server/services/group.js +4 -0
  80. package/dist/server/sessions.js +1 -0
  81. package/dist/server/signin.js +8 -6
  82. package/dist/server/sso/domain.d.ts +159 -16
  83. package/dist/server/sso/domain.js +1 -1
  84. package/dist/server/sso/http.js +144 -60
  85. package/dist/server/sso/oidc.js +28 -12
  86. package/dist/server/sso/policy.js +30 -14
  87. package/dist/server/sso/provision.js +1 -1
  88. package/dist/server/sso/saml.js +18 -9
  89. package/dist/server/sso/scim.js +12 -4
  90. package/dist/server/sso/shared.js +5 -5
  91. package/dist/server/telemetry.js +3 -0
  92. package/dist/server/tokens.js +10 -2
  93. package/dist/server/totp.js +127 -100
  94. package/dist/server/types.d.ts +224 -151
  95. package/dist/server/url.js +1 -1
  96. package/dist/server/users.js +93 -53
  97. package/dist/server/wellknown.d.ts +130 -0
  98. package/dist/server/wellknown.js +195 -0
  99. package/dist/shared/errors.js +0 -1
  100. package/package.json +36 -4
  101. package/dist/server/oauth/index.js +0 -12
  102. package/dist/server/utils/dispatch.js +0 -36
  103. package/dist/shared/authResults.d.ts +0 -16
  104. /package/dist/server/{componentContext.js → component/context.js} +0 -0
  105. /package/dist/server/{convexIdentity.js → identity/convex.js} +0 -0
@@ -195,155 +195,15 @@ type ConvexAuthConfig = {
195
195
  maxFailedAttemptsPerHour?: number;
196
196
  };
197
197
  /**
198
- * Lifecycle callbacks for customizing sign-in behavior.
198
+ * Lifecycle callbacks. Two functions, both discriminated by `event.kind`:
199
199
  *
200
- * Use `redirect` to control post-OAuth redirect URLs, and
201
- * `createOrUpdateUser` or `afterUserCreatedOrUpdated` to
202
- * customize account linking and user document creation.
200
+ * - {@link AuthCallbacks.before before} intercept + customize specific
201
+ * operations. Returning `undefined` falls back to the default.
202
+ * - {@link AuthCallbacks.after after} observe lifecycle events. Events
203
+ * fired from mutation context are transactional; events fired from action
204
+ * context run after the orchestration mutation commits.
203
205
  */
204
- callbacks?: {
205
- /**
206
- * Control which URLs are allowed as a destination after OAuth sign-in
207
- * and for magic links:
208
- *
209
- * ```ts
210
- * import { createAuth } from "@robelest/convex-auth/component";
211
- * import { components } from "./_generated/api";
212
- *
213
- * const auth = createAuth(components.auth, {
214
- * providers: [google],
215
- * callbacks: {
216
- * async redirect({ redirectTo }) {
217
- * // Check that redirectTo is valid
218
- * // and return the relative or absolute URL
219
- * // to redirect to.
220
- * },
221
- * },
222
- * });
223
- * ```
224
- *
225
- * Convex Auth performs redirect only during OAuth sign-in. By default,
226
- * it redirects back to the URL specified via the `SITE_URL` environment
227
- * variable. Similarly magic links link to `SITE_URL`. Additional frontend
228
- * origins can be listed in `SECONDARY_URL` for flows like passkeys.
229
- *
230
- * You can customize that behavior by providing a `redirectTo` param
231
- * to the `signIn` function:
232
- *
233
- * ```ts
234
- * signIn("google", { redirectTo: "/dashboard" })
235
- * ```
236
- *
237
- * You can even redirect to a different site.
238
- *
239
- * This callback, if specified, is then called with the provided
240
- * `redirectTo` param. Otherwise, only query params, relative paths
241
- * and URLs starting with `SITE_URL` are allowed.
242
- */
243
- redirect?: (params: {
244
- /**
245
- * The param value passed to the `signIn` function.
246
- */
247
- redirectTo: string;
248
- }) => Promise<string>;
249
- /**
250
- * Completely control account linking via this callback.
251
- *
252
- * This callback is called during the sign-in process,
253
- * before account creation and token generation.
254
- * If specified, this callback is responsible for creating
255
- * or updating the user document.
256
- *
257
- * For "credentials" providers, the callback is only called
258
- * when `createAccount` is called.
259
- */
260
- createOrUpdateUser?: (ctx: GenericMutationCtx<AnyDataModel>, args: {
261
- /**
262
- * If this is a sign-in to an existing account,
263
- * this is the existing user ID linked to that account.
264
- */
265
- existingUserId: GenericId<"User"> | null;
266
- /**
267
- * The provider type or "verification" if this callback is called
268
- * after an email or phone token verification.
269
- */
270
- type: "oauth" | "credentials" | "email" | "phone" | "verification";
271
- /**
272
- * The provider used for the sign-in, or the provider
273
- * tied to the account which is having the email or phone verified.
274
- */
275
- provider: AuthProviderMaterializedConfig;
276
- /**
277
- * - The profile returned by the OAuth provider's `profile` method.
278
- * - The profile passed to `createAccount` from a ConvexCredentials
279
- * config.
280
- * - The email address to which an email will be sent.
281
- * - The phone number to which a text will be sent.
282
- */
283
- profile: Record<string, unknown> & {
284
- email?: string;
285
- phone?: string;
286
- emailVerified?: boolean;
287
- phoneVerified?: boolean;
288
- };
289
- /**
290
- * The `shouldLink` argument passed to `createAccount`.
291
- */
292
- shouldLink?: boolean;
293
- }) => Promise<GenericId<"User">>;
294
- /**
295
- * Perform additional writes after a user is created.
296
- *
297
- * This callback is called during the sign-in process,
298
- * after the user is created or updated,
299
- * before account creation and token generation.
300
- *
301
- * **This callback is only called if `createOrUpdateUser`
302
- * is not specified.** If `createOrUpdateUser` is specified,
303
- * you can perform any additional writes in that callback.
304
- *
305
- * For "credentials" providers, the callback is only called
306
- * when `createAccount` is called.
307
- */
308
- afterUserCreatedOrUpdated?: (ctx: GenericMutationCtx<AnyDataModel>, args: {
309
- /**
310
- * The ID of the user that is being signed in.
311
- */
312
- userId: GenericId<"User">;
313
- /**
314
- * If this is a sign-in to an existing account,
315
- * this is the existing user ID linked to that account.
316
- */
317
- existingUserId: GenericId<"User"> | null;
318
- /**
319
- * The provider type or "verification" if this callback is called
320
- * after an email or phone token verification.
321
- */
322
- type: "oauth" | "credentials" | "email" | "phone" | "verification";
323
- /**
324
- * The provider used for the sign-in, or the provider
325
- * tied to the account which is having the email or phone verified.
326
- */
327
- provider: AuthProviderMaterializedConfig;
328
- /**
329
- * - The profile returned by the OAuth provider's `profile` method.
330
- * - The profile passed to `createAccount` from a ConvexCredentials
331
- * config.
332
- * - The email address to which an email will be sent.
333
- * - The phone number to which a text will be sent.
334
- */
335
- profile: Record<string, unknown> & {
336
- email?: string;
337
- phone?: string;
338
- emailVerified?: boolean;
339
- phoneVerified?: boolean;
340
- };
341
- /**
342
- * The `shouldLink` argument passed to `createAccount`.
343
- */
344
- shouldLink?: boolean;
345
- }) => Promise<void>;
346
- };
206
+ callbacks?: AuthCallbacks;
347
207
  /**
348
208
  * Application-defined role and grant model used by membership access checks.
349
209
  */
@@ -361,6 +221,170 @@ type ConvexAuthConfig = {
361
221
  */
362
222
  telemetry?: AuthTelemetryConfig;
363
223
  };
224
+ /** The provider context that triggered a `link` / `userCreated` / `userUpdated` event. */
225
+ type AuthCallbackContext = "oauth" | "credentials" | "email" | "phone" | "verification";
226
+ /** Profile shape passed to `link` / `userCreated` / `userUpdated` callbacks. */
227
+ type AuthCallbackProfile = Record<string, unknown> & {
228
+ email?: string;
229
+ phone?: string;
230
+ emailVerified?: boolean;
231
+ phoneVerified?: boolean;
232
+ };
233
+ /**
234
+ * Discriminated union of intercept events. Each variant runs *before* the
235
+ * default behavior for that operation. Returning a value replaces the
236
+ * default; returning `undefined` keeps it.
237
+ */
238
+ type BeforeEvent = {
239
+ kind: "redirect"; /** The `redirectTo` param passed to `signIn(provider, { redirectTo })`. */
240
+ redirectTo: string;
241
+ } | {
242
+ kind: "link"; /** Existing user ID if this account is already linked, else `null`. */
243
+ existingUserId: GenericId<"User"> | null; /** Where the link request originated. */
244
+ type: AuthCallbackContext; /** The provider whose account is being linked. */
245
+ provider: AuthProviderMaterializedConfig; /** Profile data harvested from the provider. */
246
+ profile: AuthCallbackProfile; /** The `shouldLink` argument passed to `createAccount`. */
247
+ shouldLink?: boolean;
248
+ };
249
+ /**
250
+ * Convex context type passed to `before` callbacks.
251
+ *
252
+ * `redirect` events arrive in action context; `link` events arrive
253
+ * in mutation context. Use `if ("db" in ctx)` to narrow when you need
254
+ * mutation-only methods.
255
+ */
256
+ type BeforeCtx = GenericMutationCtx<any> | GenericActionCtx<any>;
257
+ /**
258
+ * Return value contract for a `before` callback:
259
+ *
260
+ * - For `kind: "redirect"` — return a `string` (final URL) or `undefined`
261
+ * to fall back to the default URL resolver.
262
+ * - For `kind: "link"` — return a `GenericId<"User">` or `undefined`
263
+ * to fall back to the default user create/link logic.
264
+ */
265
+ type BeforeResult = string | GenericId<"User"> | undefined;
266
+ /**
267
+ * Discriminated union of post-operation lifecycle events. Events fired from
268
+ * mutation context run inside the same Convex transaction, so throwing rolls
269
+ * the operation back. Events fired from action context run after their
270
+ * orchestration mutation commits and cannot roll that mutation back.
271
+ */
272
+ type AuthEvent = {
273
+ kind: "userCreated";
274
+ userId: GenericId<"User">;
275
+ type: AuthCallbackContext;
276
+ provider: AuthProviderMaterializedConfig;
277
+ profile: AuthCallbackProfile;
278
+ } | {
279
+ kind: "userUpdated";
280
+ userId: GenericId<"User">;
281
+ existingUserId: GenericId<"User">;
282
+ type: AuthCallbackContext;
283
+ provider: AuthProviderMaterializedConfig;
284
+ profile: AuthCallbackProfile;
285
+ } | {
286
+ kind: "signedIn";
287
+ userId: GenericId<"User">;
288
+ sessionId: GenericId<"Session">;
289
+ provider: string;
290
+ flow?: string;
291
+ } | {
292
+ kind: "signedOut";
293
+ userId: GenericId<"User">;
294
+ sessionId: GenericId<"Session">;
295
+ } | {
296
+ kind: "passwordChanged";
297
+ userId: GenericId<"User">;
298
+ flow: "reset" | "change";
299
+ } | {
300
+ kind: "passkeyAdded";
301
+ userId: GenericId<"User">;
302
+ passkeyId: GenericId<"Passkey">;
303
+ credentialId: string;
304
+ } | {
305
+ kind: "passkeyRemoved";
306
+ userId: GenericId<"User">;
307
+ passkeyId: GenericId<"Passkey">;
308
+ } | {
309
+ kind: "totpEnrolled";
310
+ userId: GenericId<"User">;
311
+ totpId: GenericId<"TotpFactor">;
312
+ } | {
313
+ kind: "totpRemoved";
314
+ userId: GenericId<"User">;
315
+ totpId: GenericId<"TotpFactor">;
316
+ } | {
317
+ kind: "emailVerified";
318
+ userId: GenericId<"User">;
319
+ email: string;
320
+ } | {
321
+ kind: "phoneVerified";
322
+ userId: GenericId<"User">;
323
+ phone: string;
324
+ } | {
325
+ kind: "accountLinked";
326
+ userId: GenericId<"User">;
327
+ provider: string;
328
+ providerAccountId: string;
329
+ } | {
330
+ kind: "accountUnlinked";
331
+ userId: GenericId<"User">;
332
+ accountId: GenericId<"Account">;
333
+ provider: string;
334
+ } | {
335
+ kind: "sessionsInvalidated";
336
+ userId: GenericId<"User">;
337
+ sessionIds: GenericId<"Session">[];
338
+ };
339
+ /**
340
+ * Convex context type passed to `after` callbacks.
341
+ *
342
+ * Different events fire from different runtime contexts (some from
343
+ * mutations, some from actions). The union type exposes only methods both
344
+ * share — `runQuery`, `runMutation`, `scheduler`, `auth`. To use ctx-specific
345
+ * features (`db`, `runAction`), narrow with `if ("db" in ctx)`.
346
+ */
347
+ type AfterCtx = GenericMutationCtx<any> | GenericActionCtx<any>;
348
+ /**
349
+ * Lifecycle callback set. Two callbacks, both discriminated by `event.kind`.
350
+ *
351
+ * @example
352
+ * ```ts
353
+ * createAuth(components.auth, {
354
+ * providers: [google()],
355
+ * callbacks: {
356
+ * async before(ctx, event) {
357
+ * if (event.kind === "redirect") return safeRedirect(event.redirectTo);
358
+ * // returning undefined falls back to default for other kinds
359
+ * },
360
+ * async after(ctx, event) {
361
+ * if (event.kind === "userCreated") {
362
+ * await ctx.scheduler.runAfter(0, internal.workflows.onboard, {
363
+ * userId: event.userId,
364
+ * });
365
+ * }
366
+ * },
367
+ * },
368
+ * });
369
+ * ```
370
+ */
371
+ interface AuthCallbacks {
372
+ /**
373
+ * Intercept and customize specific operations before they commit.
374
+ * Returning a value replaces the default; returning `undefined` keeps it.
375
+ *
376
+ * Match on `event.kind` to narrow the event shape. See {@link BeforeResult}
377
+ * for what to return per kind.
378
+ */
379
+ before?: (ctx: BeforeCtx, event: BeforeEvent) => Promise<BeforeResult>;
380
+ /**
381
+ * Observe lifecycle events. The `ctx` type is a union of mutation and
382
+ * action ctx — see {@link AfterCtx}. Most events fire from mutation
383
+ * context (atomic with the operation, throwing rolls back); a few fire
384
+ * from action context after the orchestration mutation commits.
385
+ */
386
+ after?: (ctx: AfterCtx, event: AuthEvent) => Promise<void>;
387
+ }
364
388
  /**
365
389
  * Union of all supported auth provider config types.
366
390
  *
@@ -558,6 +582,7 @@ interface EmailConfig<DataModel extends GenericDataModel = GenericDataModel> {
558
582
  /**
559
583
  * The values passed to the `signIn` function.
560
584
  */
585
+
561
586
  params: Record<string, Value | undefined>, account: GenericDoc<DataModel, "Account">) => Promise<void>;
562
587
  /** Raw user options before merging with defaults. */
563
588
  options: EmailUserConfig<DataModel>;
@@ -624,6 +649,7 @@ interface PhoneConfig<DataModel extends GenericDataModel = GenericDataModel> {
624
649
  /**
625
650
  * The values passed to the `signIn` function.
626
651
  */
652
+
627
653
  params: Record<string, Value | undefined>, account: GenericDoc<DataModel, "Account">) => Promise<void>;
628
654
  options: PhoneUserConfig<DataModel>;
629
655
  }
@@ -790,6 +816,18 @@ type AuthInvalidateSessionsArgs = {
790
816
  userId: GenericId<"User">;
791
817
  except?: GenericId<"Session">[];
792
818
  };
819
+ /** Arguments for `auth.account.unlink()`. */
820
+ type AuthUnlinkAccountArgs = {
821
+ accountId: GenericId<"Account">;
822
+ };
823
+ /** Arguments for `auth.passkey.delete()`. */
824
+ type AuthDeletePasskeyArgs = {
825
+ passkeyId: GenericId<"Passkey">;
826
+ };
827
+ /** Arguments for `auth.totp.delete()`. */
828
+ type AuthDeleteTotpArgs = {
829
+ totpId: GenericId<"TotpFactor">;
830
+ };
793
831
  /** Arguments for `auth.provider.signIn()`. */
794
832
  type AuthProviderSignInArgs = {
795
833
  accountId?: GenericId<"Account">;
@@ -837,7 +875,11 @@ type AuthMemberRequireArgs = AuthMemberInspectArgs & {
837
875
  * ```
838
876
  */
839
877
  type AuthServerHelpers = {
840
- /** Account management: create, retrieve, and update provider-linked accounts. */account: {
878
+ /**
879
+ * Account management: create, retrieve, update, and unlink
880
+ * provider-linked accounts.
881
+ */
882
+ account: {
841
883
  create: (ctx: GenericActionCtx<GenericDataModel>, args: AuthCreateAccountArgs) => Promise<{
842
884
  account: GenericDoc<GenericDataModel, "Account">;
843
885
  user: GenericDoc<GenericDataModel, "User">;
@@ -849,6 +891,36 @@ type AuthServerHelpers = {
849
891
  update: (ctx: GenericActionCtx<GenericDataModel>, args: AuthUpdateAccountArgs) => Promise<{
850
892
  accountId: GenericId<"Account">;
851
893
  }>;
894
+ /**
895
+ * Unlink (delete) a provider-linked account by ID and fire the
896
+ * `accountUnlinked` lifecycle event with the captured `provider`.
897
+ */
898
+ unlink: (ctx: GenericActionCtx<GenericDataModel>, args: AuthUnlinkAccountArgs) => Promise<{
899
+ accountId: GenericId<"Account">;
900
+ userId: GenericId<"User">;
901
+ provider: string;
902
+ }>;
903
+ }; /** Passkey credential management exposed to provider authorize callbacks. */
904
+ passkey: {
905
+ /**
906
+ * Delete a passkey credential by ID and fire the `passkeyRemoved`
907
+ * lifecycle event with the owning `userId`.
908
+ */
909
+ delete: (ctx: GenericActionCtx<GenericDataModel>, args: AuthDeletePasskeyArgs) => Promise<{
910
+ passkeyId: GenericId<"Passkey">;
911
+ userId: GenericId<"User">;
912
+ }>;
913
+ }; /** TOTP factor management exposed to provider authorize callbacks. */
914
+ totp: {
915
+ /**
916
+ * Delete a TOTP factor by ID and fire the `totpRemoved` lifecycle
917
+ * event. If this was the user's last verified factor, the
918
+ * `User.hasTotp` flag is cleared as part of the same mutation.
919
+ */
920
+ delete: (ctx: GenericActionCtx<GenericDataModel>, args: AuthDeleteTotpArgs) => Promise<{
921
+ totpId: GenericId<"TotpFactor">;
922
+ userId: GenericId<"User">;
923
+ }>;
852
924
  };
853
925
  session: {
854
926
  current: (ctx: {
@@ -1029,12 +1101,12 @@ type UserWhere = {
1029
1101
  /** Sortable fields for `auth.user.list()`. */
1030
1102
  type UserOrderBy = "_creationTime" | "name" | "email" | "phone";
1031
1103
  /**
1032
- * Context injected into `auth.http.action()` and `auth.http.route()` handlers.
1104
+ * Context injected into `auth.request.action()` and `auth.request.route()` handlers.
1033
1105
  *
1034
1106
  * The handler's `ctx` receives these fields after Bearer token verification:
1035
1107
  *
1036
1108
  * ```ts
1037
- * auth.http.route(http, {
1109
+ * auth.request.route(http, {
1038
1110
  * path: "/api/data",
1039
1111
  * method: "GET",
1040
1112
  * handler: async (ctx, request) => {
@@ -1134,6 +1206,7 @@ type AuthComponentApi = {
1134
1206
  keyPatch: FunctionReference<"mutation", "internal">;
1135
1207
  keyDelete: FunctionReference<"mutation", "internal">;
1136
1208
  passkeyInsert: FunctionReference<"mutation", "internal">;
1209
+ passkeyGetById: FunctionReference<"query", "internal">;
1137
1210
  passkeyGetByCredentialId: FunctionReference<"query", "internal">;
1138
1211
  passkeyListByUserId: FunctionReference<"query", "internal">;
1139
1212
  passkeyUpdateCounter: FunctionReference<"mutation", "internal">;
@@ -1213,5 +1286,5 @@ type SessionTokenIdentityClaims = {
1213
1286
  };
1214
1287
  type KeyDoc = Infer<typeof vApiKeyDoc>;
1215
1288
  //#endregion
1216
- export { AuthAuthorizationConfig, AuthGrant, AuthProviderConfig, AuthRoleId, ConvexAuthConfig, ConvexAuthMaterializedConfig, ConvexCredentialsConfig, CorsConfig, DeviceProviderConfig, Doc, EmailConfig, EmailUserConfig, GenericActionCtxWithAuthConfig, GenericDoc, GroupConnectionDeprovisionMode, GroupConnectionPolicy, GroupConnectionPolicyPatch, HasDeviceProvider, HasPasskeyProvider, HasSSO, HasTotpProvider, HttpKeyContext, KeyDoc, KeyScope, OAuthMaterializedConfig, OAuthProfile, OAuthTokens, OIDCClaimMapping, PasskeyProviderConfig, PhoneConfig, PhoneUserConfig, SSOProviderConfig, ScopeChecker, SessionTokenIdentityClaims, TotpProviderConfig, UserOrderBy, UserWhere };
1289
+ export { AfterCtx, AuthAuthorizationConfig, AuthCallbackContext, AuthCallbackProfile, AuthCallbacks, AuthEvent, AuthGrant, AuthProviderConfig, AuthRoleId, BeforeCtx, BeforeEvent, BeforeResult, ConvexAuthConfig, ConvexAuthMaterializedConfig, ConvexCredentialsConfig, CorsConfig, DeviceProviderConfig, Doc, EmailConfig, EmailUserConfig, GenericActionCtxWithAuthConfig, GenericDoc, GroupConnectionPolicy, GroupConnectionPolicyPatch, HasDeviceProvider, HasPasskeyProvider, HasSSO, HasTotpProvider, HttpKeyContext, KeyDoc, KeyScope, OAuthMaterializedConfig, OAuthProfile, OAuthTokens, OIDCClaimMapping, PasskeyProviderConfig, PhoneConfig, PhoneUserConfig, SSOProviderConfig, ScopeChecker, SessionTokenIdentityClaims, TotpProviderConfig, UserOrderBy, UserWhere };
1217
1290
  //# sourceMappingURL=types.d.ts.map
@@ -28,5 +28,5 @@ function isLocalHost(host) {
28
28
  }
29
29
 
30
30
  //#endregion
31
- export { isLocalHost, siteUrlsFromEnv };
31
+ export { isLocalHost, normalizeUrl, siteUrlsFromEnv };
32
32
  //# sourceMappingURL=url.js.map
@@ -39,6 +39,52 @@ async function upsertUserAndAccount(ctx, sessionId, account, args, config, opts)
39
39
  accountId: await createOrUpdateAccount(ctx, userId, account, args, config)
40
40
  };
41
41
  }
42
+ async function resolveUserIdByLinking(ctx, args, profile, shouldLinkViaEmail, shouldLinkViaPhone, existingUserIdOverride, config) {
43
+ const [emailLookup, phoneLookup] = await Promise.all([typeof profile.email === "string" && shouldLinkViaEmail ? uniqueUserWithVerifiedEmail(ctx, profile.email, config) : Promise.resolve(null), typeof profile.phone === "string" && shouldLinkViaPhone ? uniqueUserWithVerifiedPhone(ctx, profile.phone, config) : Promise.resolve(null)]);
44
+ const emailUserId = emailLookup?._id ?? null;
45
+ const phoneUserId = phoneLookup?._id ?? null;
46
+ if (emailUserId !== null && phoneUserId !== null) {
47
+ log(LOG_LEVELS.DEBUG, `Found both email and phone verified users, not linking: email: ${emailUserId}, phone: ${phoneUserId}`);
48
+ return existingUserIdOverride;
49
+ }
50
+ if (emailUserId !== null) {
51
+ log(LOG_LEVELS.DEBUG, `Found existing email verified user, linking: ${emailUserId}`);
52
+ return emailUserId;
53
+ }
54
+ if (phoneUserId !== null) {
55
+ log(LOG_LEVELS.DEBUG, `Found existing phone verified user, linking: ${phoneUserId}`);
56
+ return phoneUserId;
57
+ }
58
+ log(LOG_LEVELS.DEBUG, "No existing verified users found, creating new user");
59
+ return existingUserIdOverride;
60
+ }
61
+ async function checkAllowLink(args, config, userId) {
62
+ if (config.sso?.hooks?.allowLink === void 0 || args.provider.type !== "oauth" && args.provider.type !== "sso") return true;
63
+ return await config.sso.hooks.allowLink({
64
+ protocol: args.provider.type === "oauth" && typeof args.accountExtend?.identity?.protocol === "string" ? args.accountExtend.identity.protocol : "oidc",
65
+ connectionId: typeof args.accountExtend?.identity?.connectionId === "string" ? args.accountExtend.identity.connectionId : void 0,
66
+ profile: args.profile,
67
+ userId
68
+ }) !== false;
69
+ }
70
+ async function updateExistingUser(db, userId, userData, source, provisioningUser) {
71
+ const currentUser = await db.users.getById(userId);
72
+ const mode = effectiveUserUpdateMode(source, provisioningUser);
73
+ const patchData = buildUserPatchData({
74
+ currentUser: currentUser ?? {},
75
+ nextUser: userData,
76
+ mode
77
+ });
78
+ if (Object.keys(patchData).length === 0) return;
79
+ try {
80
+ await db.users.patch(userId, patchData);
81
+ } catch (error) {
82
+ throw new ConvexError({
83
+ code: "USER_UPDATE_FAILED",
84
+ message: `Could not update user document with ID \`${userId}\`, either the user has been deleted but their account has not, or the profile data doesn't match the \`users\` table schema: ${error instanceof Error ? error.message : String(error)}`
85
+ });
86
+ }
87
+ }
42
88
  async function defaultCreateOrUpdateUser(ctx, existingSessionId, existingAccount, args, config, existingUserIdOverride, provisioningUser, source) {
43
89
  log(LOG_LEVELS.DEBUG, "defaultCreateOrUpdateUser args:", {
44
90
  existingAccountId: existingAccount?._id,
@@ -47,12 +93,18 @@ async function defaultCreateOrUpdateUser(ctx, existingSessionId, existingAccount
47
93
  });
48
94
  const existingUserId = existingAccount?.userId ?? null;
49
95
  const db = authDb(ctx, config);
50
- if (config.callbacks?.createOrUpdateUser !== void 0) {
51
- log(LOG_LEVELS.DEBUG, "Using custom createOrUpdateUser callback");
52
- return await config.callbacks.createOrUpdateUser(ctx, {
96
+ const before = config.callbacks?.before;
97
+ if (before !== void 0) {
98
+ log(LOG_LEVELS.DEBUG, "Calling `before({ kind: \"link\" })` callback");
99
+ const customUserId = await before(ctx, {
100
+ kind: "link",
53
101
  existingUserId,
54
- ...args
102
+ type: args.type,
103
+ provider: args.provider,
104
+ profile: args.profile,
105
+ shouldLink: args.shouldLinkViaEmail || args.shouldLinkViaPhone
55
106
  });
107
+ if (customUserId !== void 0) return customUserId;
56
108
  }
57
109
  const { provider, profile: { id: _profileId, emailVerified: profileEmailVerified, phoneVerified: profilePhoneVerified, ...profile } } = args;
58
110
  const emailVerified = profileEmailVerified ?? (provider.type === "oauth" && provider.accountLinking !== "none");
@@ -61,30 +113,8 @@ async function defaultCreateOrUpdateUser(ctx, existingSessionId, existingAccount
61
113
  const shouldLinkViaPhone = args.shouldLinkViaPhone || phoneVerified || provider.type === "phone";
62
114
  let userId = existingUserId ?? existingUserIdOverride;
63
115
  if (existingUserId === null) {
64
- const [emailLookup, phoneLookup] = await Promise.all([typeof profile.email === "string" && shouldLinkViaEmail ? uniqueUserWithVerifiedEmail(ctx, profile.email, config) : Promise.resolve(null), typeof profile.phone === "string" && shouldLinkViaPhone ? uniqueUserWithVerifiedPhone(ctx, profile.phone, config) : Promise.resolve(null)]);
65
- const existingUserWithVerifiedEmailId = emailLookup?._id ?? null;
66
- const existingUserWithVerifiedPhoneId = phoneLookup?._id ?? null;
67
- if (existingUserWithVerifiedEmailId !== null && existingUserWithVerifiedPhoneId !== null) {
68
- log(LOG_LEVELS.DEBUG, `Found existing email and phone verified users, so not linking: email: ${existingUserWithVerifiedEmailId}, phone: ${existingUserWithVerifiedPhoneId}`);
69
- userId = null;
70
- } else if (existingUserWithVerifiedEmailId !== null) {
71
- log(LOG_LEVELS.DEBUG, `Found existing email verified user, linking: ${existingUserWithVerifiedEmailId}`);
72
- userId = existingUserWithVerifiedEmailId;
73
- } else if (existingUserWithVerifiedPhoneId !== null) {
74
- log(LOG_LEVELS.DEBUG, `Found existing phone verified user, linking: ${existingUserWithVerifiedPhoneId}`);
75
- userId = existingUserWithVerifiedPhoneId;
76
- } else {
77
- log(LOG_LEVELS.DEBUG, "No existing verified users found, creating new user");
78
- userId = null;
79
- }
80
- if (userId !== null && config.sso?.hooks?.allowLink !== void 0 && (args.provider.type === "oauth" || args.provider.type === "sso")) {
81
- if (await config.sso.hooks.allowLink({
82
- protocol: args.provider.type === "oauth" && typeof args.accountExtend?.identity?.protocol === "string" ? args.accountExtend.identity.protocol : "oidc",
83
- connectionId: typeof args.accountExtend?.identity?.connectionId === "string" ? args.accountExtend.identity.connectionId : void 0,
84
- profile: args.profile,
85
- userId
86
- }) === false) userId = null;
87
- }
116
+ userId = await resolveUserIdByLinking(ctx, args, profile, shouldLinkViaEmail, shouldLinkViaPhone, existingUserIdOverride, config);
117
+ if (userId !== null && !await checkAllowLink(args, config, userId)) userId = null;
88
118
  }
89
119
  const userData = {
90
120
  ...emailVerified ? { emailVerificationTime: Date.now() } : null,
@@ -92,40 +122,43 @@ async function defaultCreateOrUpdateUser(ctx, existingSessionId, existingAccount
92
122
  ...profile
93
123
  };
94
124
  const existingOrLinkedUserId = userId;
95
- if (userId !== null) {
96
- const currentUserId = userId;
97
- const currentUser = await db.users.getById(currentUserId);
98
- const mode = effectiveUserUpdateMode(source, provisioningUser);
99
- const patchData = buildUserPatchData({
100
- currentUser: currentUser ?? {},
101
- nextUser: userData,
102
- mode
103
- });
104
- if (Object.keys(patchData).length === 0) return userId;
105
- try {
106
- await db.users.patch(currentUserId, patchData);
107
- } catch (error) {
108
- throw new ConvexError({
109
- code: "USER_UPDATE_FAILED",
110
- message: `Could not update user document with ID \`${currentUserId}\`, either the user has been deleted but their account has not, or the profile data doesn't match the \`users\` table schema: ${error instanceof Error ? error.message : String(error)}`
111
- });
112
- }
113
- } else {
125
+ if (userId !== null) await updateExistingUser(db, userId, userData, source, provisioningUser);
126
+ else {
114
127
  if (source === "login" && provisioningUser?.createOnSignIn === false) throw new ConvexError({
115
128
  code: "NOT_AUTHORIZED",
116
129
  message: "This SSO connection does not allow creating users on sign-in."
117
130
  });
118
131
  userId = await db.users.insert(userData);
119
132
  }
120
- const afterUserCreatedOrUpdated = config.callbacks?.afterUserCreatedOrUpdated;
121
- if (afterUserCreatedOrUpdated !== void 0) {
122
- log(LOG_LEVELS.DEBUG, "Calling custom afterUserCreatedOrUpdated callback");
123
- await afterUserCreatedOrUpdated(ctx, {
133
+ const after = config.callbacks?.after;
134
+ if (after !== void 0) {
135
+ log(LOG_LEVELS.DEBUG, "Calling `after` callback for user lifecycle event");
136
+ if (existingOrLinkedUserId === null) await after(ctx, {
137
+ kind: "userCreated",
138
+ userId,
139
+ type: args.type,
140
+ provider: args.provider,
141
+ profile: args.profile
142
+ });
143
+ else await after(ctx, {
144
+ kind: "userUpdated",
124
145
  userId,
125
146
  existingUserId: existingOrLinkedUserId,
126
- ...args
147
+ type: args.type,
148
+ provider: args.provider,
149
+ profile: args.profile
150
+ });
151
+ if (emailVerified && typeof args.profile.email === "string") await after(ctx, {
152
+ kind: "emailVerified",
153
+ userId,
154
+ email: args.profile.email
155
+ });
156
+ if (phoneVerified && typeof args.profile.phone === "string") await after(ctx, {
157
+ kind: "phoneVerified",
158
+ userId,
159
+ phone: args.profile.phone
127
160
  });
128
- } else log(LOG_LEVELS.DEBUG, "No custom afterUserCreatedOrUpdated callback, skipping");
161
+ }
129
162
  return userId;
130
163
  }
131
164
  async function uniqueUserWithVerifiedEmail(ctx, email, config) {
@@ -137,6 +170,7 @@ async function uniqueUserWithVerifiedPhone(ctx, phone, config) {
137
170
  async function createOrUpdateAccount(ctx, userId, account, args, config) {
138
171
  const db = authDb(ctx, config);
139
172
  const mergedExtend = "existingAccount" in account ? mergeExtend(account.existingAccount.extend, args.accountExtend) : args.accountExtend;
173
+ const isNewAccount = !("existingAccount" in account);
140
174
  const accountId = "existingAccount" in account ? account.existingAccount._id : await db.accounts.create({
141
175
  userId,
142
176
  provider: args.provider.id,
@@ -144,6 +178,12 @@ async function createOrUpdateAccount(ctx, userId, account, args, config) {
144
178
  secret: account.secret,
145
179
  extend: mergedExtend
146
180
  });
181
+ if (isNewAccount) await config.callbacks?.after?.(ctx, {
182
+ kind: "accountLinked",
183
+ userId,
184
+ provider: args.provider.id,
185
+ providerAccountId: account.providerAccountId
186
+ });
147
187
  if ("existingAccount" in account && account.existingAccount.userId !== userId) await db.accounts.patch(accountId, { userId });
148
188
  const accountPatchData = {};
149
189
  if (mergedExtend) accountPatchData.extend = mergedExtend;