@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.
- package/dist/bin.js +125 -36
- package/dist/browser/index.d.ts +3 -13
- package/dist/browser/index.js +47 -12
- package/dist/browser/navigation.js +1 -1
- package/dist/browser/passkey.js +7 -7
- package/dist/browser/runtime.js +13 -15
- package/dist/client/core/types.d.ts +179 -63
- package/dist/client/core/types.js +6 -0
- package/dist/client/factors/totp.js +1 -1
- package/dist/client/index.d.ts +5 -4
- package/dist/client/index.js +115 -56
- package/dist/client/runtime/mutex.js +3 -2
- package/dist/component/_generated/component.d.ts +40 -0
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/http.js +9 -0
- package/dist/component/index.d.ts +1 -1
- package/dist/component/model.d.ts +25 -25
- package/dist/component/model.js +2 -1
- package/dist/component/modules.js +1 -0
- package/dist/component/public/factors/passkeys.js +31 -1
- package/dist/component/public/identity/codes.js +1 -1
- package/dist/component/public/identity/tokens.js +2 -1
- package/dist/component/public/identity/verifiers.js +15 -5
- package/dist/component/public.js +2 -2
- package/dist/component/schema.d.ts +292 -290
- package/dist/component/schema.js +2 -1
- package/dist/core/index.d.ts +8 -3
- package/dist/core/index.js +7 -2
- package/dist/expo/index.d.ts +21 -0
- package/dist/expo/index.js +148 -0
- package/dist/expo/passkey.js +174 -0
- package/dist/providers/apple.d.ts +1 -1
- package/dist/providers/apple.js +6 -8
- package/dist/providers/custom.d.ts +1 -1
- package/dist/providers/custom.js +4 -7
- package/dist/providers/github.d.ts +1 -1
- package/dist/providers/github.js +5 -8
- package/dist/providers/google.d.ts +1 -1
- package/dist/providers/google.js +5 -8
- package/dist/providers/microsoft.d.ts +1 -1
- package/dist/providers/microsoft.js +5 -9
- package/dist/providers/password.d.ts +18 -37
- package/dist/providers/password.js +170 -115
- package/dist/providers/redirect.d.ts +1 -0
- package/dist/providers/redirect.js +20 -0
- package/dist/server/auth.d.ts +6 -7
- package/dist/server/auth.js +3 -2
- package/dist/server/{ctxCache.js → cache/context.js} +2 -2
- package/dist/server/{componentContext.d.ts → component/context.d.ts} +2 -2
- package/dist/server/context.js +3 -10
- package/dist/server/contract.d.ts +2 -87
- package/dist/server/contract.js +1 -1
- package/dist/server/cookies.js +25 -1
- package/dist/server/core.js +1 -1
- package/dist/server/errors.js +24 -1
- package/dist/server/{auth-context.d.ts → facade.d.ts} +3 -45
- package/dist/server/{auth-context.js → facade.js} +11 -12
- package/dist/server/http.d.ts +7 -7
- package/dist/server/http.js +36 -7
- package/dist/server/{convexIdentity.d.ts → identity/convex.d.ts} +3 -3
- package/dist/server/index.d.ts +5 -3
- package/dist/server/index.js +3 -2
- package/dist/server/mounts.d.ts +175 -22
- package/dist/server/mutations/code.js +7 -1
- package/dist/server/mutations/{credentialsSignIn.js → credentials/signin.js} +10 -10
- package/dist/server/mutations/index.js +1 -1
- package/dist/server/mutations/invalidate.js +11 -1
- package/dist/server/mutations/oauth.js +25 -27
- package/dist/server/mutations/signin.js +6 -0
- package/dist/server/mutations/signout.js +5 -0
- package/dist/server/mutations/store.js +1 -1
- package/dist/server/oauth/factory.js +11 -3
- package/dist/server/passkey.js +126 -110
- package/dist/server/prefetch.js +8 -1
- package/dist/server/redirects.js +11 -3
- package/dist/server/refresh.js +6 -1
- package/dist/server/runtime.d.ts +58 -36
- package/dist/server/runtime.js +333 -36
- package/dist/server/services/group.js +4 -0
- package/dist/server/sessions.js +1 -0
- package/dist/server/signin.js +8 -6
- package/dist/server/sso/domain.d.ts +159 -16
- package/dist/server/sso/domain.js +1 -1
- package/dist/server/sso/http.js +144 -60
- package/dist/server/sso/oidc.js +28 -12
- package/dist/server/sso/policy.js +30 -14
- package/dist/server/sso/provision.js +1 -1
- package/dist/server/sso/saml.js +18 -9
- package/dist/server/sso/scim.js +12 -4
- package/dist/server/sso/shared.js +5 -5
- package/dist/server/telemetry.js +3 -0
- package/dist/server/tokens.js +10 -2
- package/dist/server/totp.js +127 -100
- package/dist/server/types.d.ts +224 -151
- package/dist/server/url.js +1 -1
- package/dist/server/users.js +93 -53
- package/dist/server/wellknown.d.ts +130 -0
- package/dist/server/wellknown.js +195 -0
- package/dist/shared/errors.js +0 -1
- package/package.json +36 -4
- package/dist/server/oauth/index.js +0 -12
- package/dist/server/utils/dispatch.js +0 -36
- package/dist/shared/authResults.d.ts +0 -16
- /package/dist/server/{componentContext.js → component/context.js} +0 -0
- /package/dist/server/{convexIdentity.js → identity/convex.js} +0 -0
package/dist/server/types.d.ts
CHANGED
|
@@ -195,155 +195,15 @@ type ConvexAuthConfig = {
|
|
|
195
195
|
maxFailedAttemptsPerHour?: number;
|
|
196
196
|
};
|
|
197
197
|
/**
|
|
198
|
-
* Lifecycle callbacks
|
|
198
|
+
* Lifecycle callbacks. Two functions, both discriminated by `event.kind`:
|
|
199
199
|
*
|
|
200
|
-
*
|
|
201
|
-
* `
|
|
202
|
-
*
|
|
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
|
-
/**
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
package/dist/server/url.js
CHANGED
package/dist/server/users.js
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
121
|
-
if (
|
|
122
|
-
log(LOG_LEVELS.DEBUG, "Calling
|
|
123
|
-
await
|
|
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
|
-
|
|
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
|
-
}
|
|
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;
|