@robelest/convex-auth 0.0.4-preview.32 → 0.0.4-preview.34

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 (89) hide show
  1. package/dist/component/_generated/component.d.ts +1611 -2039
  2. package/dist/component/account.js +3 -0
  3. package/dist/component/convex.config.d.ts +2 -2
  4. package/dist/component/factor/device.js +3 -0
  5. package/dist/component/factor/passkey.js +3 -0
  6. package/dist/component/factor/totp.js +3 -0
  7. package/dist/component/group/invite.js +3 -0
  8. package/dist/component/group/member.js +3 -0
  9. package/dist/component/group.js +3 -0
  10. package/dist/component/model.d.ts +342 -30
  11. package/dist/component/model.js +22 -4
  12. package/dist/component/modules.js +24 -21
  13. package/dist/component/public/factors/devices.js +37 -106
  14. package/dist/component/public/factors/passkeys.js +29 -149
  15. package/dist/component/public/factors/totp.js +32 -159
  16. package/dist/component/public/groups/core.js +19 -82
  17. package/dist/component/public/groups/invites.js +15 -104
  18. package/dist/component/public/groups/members.js +26 -149
  19. package/dist/component/public/identity/accounts.js +12 -94
  20. package/dist/component/public/identity/codes.js +13 -73
  21. package/dist/component/public/identity/sessions.js +5 -107
  22. package/dist/component/public/identity/tokens.js +13 -103
  23. package/dist/component/public/identity/users.js +188 -185
  24. package/dist/component/public/identity/verifiers.js +17 -80
  25. package/dist/component/public/security/keys.js +13 -120
  26. package/dist/component/public/security/limits.js +0 -43
  27. package/dist/component/public/sso/audit.js +0 -28
  28. package/dist/component/public/sso/core.js +31 -104
  29. package/dist/component/public/sso/domains.js +0 -71
  30. package/dist/component/public/sso/scim.js +63 -239
  31. package/dist/component/public/sso/secrets.js +0 -30
  32. package/dist/component/public/sso/webhooks.js +23 -128
  33. package/dist/component/rateLimit.js +3 -0
  34. package/dist/component/schema.d.ts +378 -342
  35. package/dist/component/schema.js +11 -1
  36. package/dist/component/session.js +3 -0
  37. package/dist/component/sso/audit.js +3 -0
  38. package/dist/component/sso/connection/domain/verification.js +3 -0
  39. package/dist/component/sso/connection/domain.js +3 -0
  40. package/dist/component/sso/connection/scim/config.js +3 -0
  41. package/dist/component/sso/connection/scim/identity.js +3 -0
  42. package/dist/component/sso/connection/secret.js +3 -0
  43. package/dist/component/sso/connection.js +3 -0
  44. package/dist/component/sso/webhook/delivery.js +3 -0
  45. package/dist/component/sso/webhook/endpoint.js +3 -0
  46. package/dist/component/token/pkce.js +3 -0
  47. package/dist/component/token/refresh.js +3 -0
  48. package/dist/component/token/verification.js +3 -0
  49. package/dist/component/user/email.js +3 -0
  50. package/dist/component/user/key.js +3 -0
  51. package/dist/component/user.js +62 -0
  52. package/dist/core/index.d.ts +131 -28
  53. package/dist/core/index.js +2 -0
  54. package/dist/model.js +391 -0
  55. package/dist/providers/credentials.d.ts +1 -1
  56. package/dist/providers/github.js +6 -0
  57. package/dist/providers/password.js +1 -2
  58. package/dist/server/auth.d.ts +73 -7
  59. package/dist/server/auth.js +4 -1
  60. package/dist/server/context.js +30 -3
  61. package/dist/server/contract.js +42 -42
  62. package/dist/server/core.js +224 -86
  63. package/dist/server/db.js +45 -37
  64. package/dist/server/facade.d.ts +39 -0
  65. package/dist/server/facade.js +16 -0
  66. package/dist/server/index.d.ts +5 -3
  67. package/dist/server/index.js +3 -1
  68. package/dist/server/mounts.d.ts +101 -101
  69. package/dist/server/mutations/credentials/signin.js +3 -7
  70. package/dist/server/mutations/oauth.js +9 -6
  71. package/dist/server/runtime.d.ts +147 -46
  72. package/dist/server/runtime.js +10 -8
  73. package/dist/server/services/group.js +9 -9
  74. package/dist/server/sso/domain.d.ts +1 -1
  75. package/dist/server/sso/domain.js +40 -40
  76. package/dist/server/sso/http.js +18 -18
  77. package/dist/server/sso/oidc.js +1 -1
  78. package/dist/server/sso/policies.js +3 -3
  79. package/dist/server/sso/policy.js +12 -4
  80. package/dist/server/sso/provision.js +9 -9
  81. package/dist/server/sso/validators.js +2 -2
  82. package/dist/server/sso/webhook.js +8 -8
  83. package/dist/server/types.d.ts +185 -124
  84. package/dist/server/types.js +29 -24
  85. package/dist/server/users.js +49 -2
  86. package/dist/server/validators.d.ts +745 -0
  87. package/dist/server/validators.js +60 -0
  88. package/package.json +1 -1
  89. package/dist/component/public.js +0 -22
@@ -1,4 +1,4 @@
1
- import { vPaginated, vUserDoc } from "../../model.js";
1
+ import { vPaginated, vUserDoc, vUserEmailDoc, vUserEmailSource } from "../../model.js";
2
2
  import { mutation, query } from "../../functions.js";
3
3
  import { ConvexError, v } from "convex/values";
4
4
 
@@ -24,23 +24,6 @@ import { ConvexError, v } from "convex/values";
24
24
  * @returns An object with `items` (array of user documents) and `nextCursor`
25
25
  * (`string | null`) for fetching subsequent pages.
26
26
  *
27
- * @example
28
- * ```ts
29
- * // Fetch the first page of non-anonymous users
30
- * const page1 = await ctx.runQuery(
31
- * component.identity.users.userList,
32
- * { where: { isAnonymous: false }, limit: 20 },
33
- * );
34
- * console.log(page1.items);
35
- *
36
- * // Fetch the next page
37
- * if (page1.nextCursor !== null) {
38
- * const page2 = await ctx.runQuery(
39
- * component.identity.users.userList,
40
- * { where: { isAnonymous: false }, limit: 20, cursor: page1.nextCursor },
41
- * );
42
- * }
43
- * ```
44
27
  */
45
28
  const userList = query({
46
29
  args: {
@@ -84,131 +67,6 @@ const userList = query({
84
67
  }
85
68
  });
86
69
  /**
87
- * Retrieve a single user by their Convex document ID.
88
- *
89
- * Performs a direct point lookup on the `User` table. Returns `null` if the
90
- * user has been deleted or never existed.
91
- *
92
- * @param args.userId - The Convex document ID (`Id<"User">`) of the user to retrieve.
93
- * @returns The user document if it exists, or `null` otherwise.
94
- *
95
- * @example
96
- * ```ts
97
- * const user = await ctx.runQuery(
98
- * component.identity.users.userGetById,
99
- * { userId: session.userId },
100
- * );
101
- * if (user !== null) {
102
- * console.log(`Name: ${user.name}, Email: ${user.email}`);
103
- * }
104
- * ```
105
- */
106
- const userGetById = query({
107
- args: { userId: v.id("User") },
108
- returns: v.union(vUserDoc, v.null()),
109
- handler: async (ctx, { userId }) => {
110
- return await ctx.db.get("User", userId);
111
- }
112
- });
113
- /**
114
- * Fetch many user documents by ID in a single component round-trip.
115
- *
116
- * Equivalent to calling {@link userGetById} for each ID in parallel from the
117
- * app side, but collapses what would be `N` cross-component RPCs into one.
118
- * Returns the documents in the same order as the input IDs; missing users
119
- * appear as `null`. Input is de-duplicated internally so passing the same
120
- * ID twice costs exactly one `ctx.db.get`.
121
- *
122
- * Hot paths like `groups:getDashboard` (member summaries) and
123
- * `issues:projectIssues` (assignee/creator lookups) previously fanned out
124
- * N `userGetById` calls — this helper is the batched replacement.
125
- *
126
- * @param args.userIds - Array of user document IDs (order preserved, duplicates tolerated).
127
- * @returns Array of user documents or `null` entries, in the same order as `args.userIds`.
128
- *
129
- * @example
130
- * ```ts
131
- * const users = await ctx.runQuery(
132
- * component.identity.users.userGetMany,
133
- * { userIds: memberIds },
134
- * );
135
- * const byId = new Map(users.filter(u => u !== null).map(u => [u!._id, u!]));
136
- * ```
137
- */
138
- const userGetMany = query({
139
- args: { userIds: v.array(v.id("User")) },
140
- returns: v.array(v.union(vUserDoc, v.null())),
141
- handler: async (ctx, { userIds }) => {
142
- if (userIds.length === 0) return [];
143
- const unique = Array.from(new Set(userIds));
144
- const docs = await Promise.all(unique.map((id) => ctx.db.get("User", id)));
145
- const byId = new Map(unique.map((id, i) => [id, docs[i] ?? null]));
146
- return userIds.map((id) => byId.get(id) ?? null);
147
- }
148
- });
149
- /**
150
- * Find a user by their verified email address.
151
- *
152
- * Queries the `User` table using the `email_verified` index to locate users
153
- * whose `email` matches and whose `emailVerificationTime` is set. If exactly
154
- * one user is found, that document is returned. Returns `null` if no user has
155
- * this email verified or if multiple users share the same verified email
156
- * (an ambiguous state that should not occur in normal operation).
157
- *
158
- * @param args.email - The verified email address to search for (case-sensitive, exact match).
159
- * @returns The matching user document if exactly one verified user is found, or `null` otherwise.
160
- *
161
- * @example
162
- * ```ts
163
- * const user = await ctx.runQuery(
164
- * component.identity.users.userFindByVerifiedEmail,
165
- * { email: "alice@example.com" },
166
- * );
167
- * if (user !== null) {
168
- * console.log(`Found verified user: ${user._id}`);
169
- * }
170
- * ```
171
- */
172
- const userFindByVerifiedEmail = query({
173
- args: { email: v.string() },
174
- returns: v.union(vUserDoc, v.null()),
175
- handler: async (ctx, { email }) => {
176
- const users = await ctx.db.query("User").withIndex("email_verified", (q) => q.eq("email", email).gt("emailVerificationTime", void 0)).take(2);
177
- return users.length === 1 ? users[0] : null;
178
- }
179
- });
180
- /**
181
- * Find a user by their verified phone number.
182
- *
183
- * Queries the `User` table using the `phone_verified` index to locate users
184
- * whose `phone` matches and whose `phoneVerificationTime` is set. If exactly
185
- * one user is found, that document is returned. Returns `null` if no user has
186
- * this phone verified or if multiple users share the same verified phone
187
- * (an ambiguous state that should not occur in normal operation).
188
- *
189
- * @param args.phone - The verified phone number to search for (exact match, e.g. `"+15551234567"`).
190
- * @returns The matching user document if exactly one verified user is found, or `null` otherwise.
191
- *
192
- * @example
193
- * ```ts
194
- * const user = await ctx.runQuery(
195
- * component.identity.users.userFindByVerifiedPhone,
196
- * { phone: "+15551234567" },
197
- * );
198
- * if (user !== null) {
199
- * console.log(`Found verified user: ${user._id}`);
200
- * }
201
- * ```
202
- */
203
- const userFindByVerifiedPhone = query({
204
- args: { phone: v.string() },
205
- returns: v.union(vUserDoc, v.null()),
206
- handler: async (ctx, { phone }) => {
207
- const users = await ctx.db.query("User").withIndex("phone_verified", (q) => q.eq("phone", phone).gt("phoneVerificationTime", void 0)).take(2);
208
- return users.length === 1 ? users[0] : null;
209
- }
210
- });
211
- /**
212
70
  * Insert a new user document into the `User` table.
213
71
  *
214
72
  * Creates a brand-new user record. The `data` argument should conform to the
@@ -219,19 +77,6 @@ const userFindByVerifiedPhone = query({
219
77
  * `email`, `isAnonymous`, and any custom fields under `extend`.
220
78
  * @returns The document ID of the newly created user.
221
79
  *
222
- * @example
223
- * ```ts
224
- * const userId = await ctx.runMutation(
225
- * component.identity.users.userInsert,
226
- * {
227
- * data: {
228
- * name: "Alice",
229
- * email: "alice@example.com",
230
- * isAnonymous: false,
231
- * },
232
- * },
233
- * );
234
- * ```
235
80
  */
236
81
  const userInsert = mutation({
237
82
  args: { data: v.any() },
@@ -255,17 +100,6 @@ const userInsert = mutation({
255
100
  * shape as the User table schema.
256
101
  * @returns The document ID of the created or updated user.
257
102
  *
258
- * @example
259
- * ```ts
260
- * // Create a new user if none exists, or update the existing one
261
- * const userId = await ctx.runMutation(
262
- * component.identity.users.userUpsert,
263
- * {
264
- * userId: existingUserId ?? undefined,
265
- * data: { name: "Alice", email: "alice@example.com" },
266
- * },
267
- * );
268
- * ```
269
103
  */
270
104
  const userUpsert = mutation({
271
105
  args: {
@@ -293,16 +127,6 @@ const userUpsert = mutation({
293
127
  * @param args.data - A partial object containing the fields to merge into the user document.
294
128
  * @returns `null` on success.
295
129
  *
296
- * @example
297
- * ```ts
298
- * await ctx.runMutation(
299
- * component.identity.users.userPatch,
300
- * {
301
- * userId: user._id,
302
- * data: { name: "Alice Smith", image: "https://example.com/avatar.png" },
303
- * },
304
- * );
305
- * ```
306
130
  */
307
131
  const userPatch = mutation({
308
132
  args: {
@@ -325,13 +149,6 @@ const userPatch = mutation({
325
149
  * @param args.userId - The document ID of the user to delete.
326
150
  * @returns `null` on success (including when the user was already absent).
327
151
  *
328
- * @example
329
- * ```ts
330
- * await ctx.runMutation(
331
- * component.identity.users.userDelete,
332
- * { userId: user._id },
333
- * );
334
- * ```
335
152
  */
336
153
  const userDelete = mutation({
337
154
  args: {
@@ -375,11 +192,197 @@ const userDelete = mutation({
375
192
  ...totps.map((t) => ctx.db.delete("TotpFactor", t._id))
376
193
  ]);
377
194
  }
195
+ const ownedEmails = await ctx.db.query("UserEmail").withIndex("user_id", (q) => q.eq("userId", userId)).collect();
196
+ await Promise.all(ownedEmails.map((e) => ctx.db.delete("UserEmail", e._id)));
378
197
  await ctx.db.delete("User", userId);
379
198
  return null;
380
199
  }
381
200
  });
201
+ /**
202
+ * List every email a user owns (across providers/SSO connections).
203
+ *
204
+ * @param args.userId - The user whose emails to list.
205
+ * @returns The user's `UserEmail` documents (may be empty).
206
+ *
207
+ */
208
+ const userEmailListByUser = query({
209
+ args: { userId: v.id("User") },
210
+ returns: v.array(vUserEmailDoc),
211
+ handler: async (ctx, { userId }) => {
212
+ return await ctx.db.query("UserEmail").withIndex("user_id", (q) => q.eq("userId", userId)).collect();
213
+ }
214
+ });
215
+ /**
216
+ * Find a verified-email owner, optionally scoped to a single SSO
217
+ * connection. Returns the matching user document if exactly one verified
218
+ * `UserEmail` matches (preserving the "one-or-null" linking contract).
219
+ *
220
+ * Pass `connectionId` for SSO logins so a verified email only matches a
221
+ * row asserted by that same connection — never across IdPs.
222
+ *
223
+ * @param args.email - Email address (exact match).
224
+ * @param args.connectionId - Restrict to this connection's emails (SSO).
225
+ * @returns The owning user document, or `null` when zero or 2+ match.
226
+ *
227
+ */
228
+ const userEmailOwner = query({
229
+ args: {
230
+ email: v.string(),
231
+ connectionId: v.optional(v.id("GroupConnection"))
232
+ },
233
+ returns: v.union(vUserDoc, v.null()),
234
+ handler: async (ctx, { email, connectionId }) => {
235
+ const rows = connectionId === void 0 ? await ctx.db.query("UserEmail").withIndex("email_verified", (q) => q.eq("email", email).gt("verificationTime", void 0)).take(2) : (await ctx.db.query("UserEmail").withIndex("connection_id_email", (q) => q.eq("connectionId", connectionId).eq("email", email)).take(2)).filter((r) => typeof r.verificationTime === "number");
236
+ if (rows.length !== 1) return null;
237
+ return await ctx.db.get("User", rows[0].userId);
238
+ }
239
+ });
240
+ /**
241
+ * Record (insert or update) an email a user owns. When `isPrimary` is
242
+ * `true`, any existing primary for the user is demoted and the
243
+ * denormalized `User.email` / `emailVerificationTime` pointer is synced.
244
+ *
245
+ * Keyed by `(userId, email)`. Provenance (`source`, `connectionId`,
246
+ * `accountId`, `provider`) is recorded so SSO linking can stay
247
+ * connection-scoped.
248
+ *
249
+ * @param args.userId - Owner of the email.
250
+ * @param args.email - The email address (store lowercased).
251
+ * @param args.verified - Mark verified (sets `verificationTime`).
252
+ * @param args.isPrimary - Promote to the user's primary email.
253
+ * @param args.source - Which mechanism asserted it (`oauth`, `saml`, …).
254
+ * @param args.accountId - Originating account, when applicable.
255
+ * @param args.provider - Originating provider id, when applicable.
256
+ * @param args.connectionId - Originating SSO connection, when applicable.
257
+ * @returns The `UserEmail` document ID.
258
+ *
259
+ */
260
+ const userEmailUpsert = mutation({
261
+ args: {
262
+ userId: v.id("User"),
263
+ email: v.string(),
264
+ verified: v.optional(v.boolean()),
265
+ isPrimary: v.optional(v.boolean()),
266
+ source: vUserEmailSource,
267
+ accountId: v.optional(v.id("Account")),
268
+ provider: v.optional(v.string()),
269
+ connectionId: v.optional(v.id("GroupConnection"))
270
+ },
271
+ returns: v.id("UserEmail"),
272
+ handler: async (ctx, args) => {
273
+ const owned = await ctx.db.query("UserEmail").withIndex("user_id", (q) => q.eq("userId", args.userId)).collect();
274
+ const existing = owned.find((e) => e.email === args.email) ?? null;
275
+ const makePrimary = args.isPrimary === true || owned.length === 0;
276
+ const verificationTime = args.verified === true ? existing?.verificationTime ?? Date.now() : existing?.verificationTime;
277
+ if (makePrimary) await Promise.all(owned.filter((e) => e.isPrimary && e._id !== existing?._id).map((e) => ctx.db.patch("UserEmail", e._id, { isPrimary: false })));
278
+ let id;
279
+ if (existing !== null) {
280
+ await ctx.db.patch("UserEmail", existing._id, {
281
+ verificationTime,
282
+ isPrimary: makePrimary ? true : existing.isPrimary,
283
+ source: args.source,
284
+ accountId: args.accountId ?? existing.accountId,
285
+ provider: args.provider ?? existing.provider,
286
+ connectionId: args.connectionId ?? existing.connectionId
287
+ });
288
+ id = existing._id;
289
+ } else id = await ctx.db.insert("UserEmail", {
290
+ userId: args.userId,
291
+ email: args.email,
292
+ verificationTime,
293
+ isPrimary: makePrimary,
294
+ source: args.source,
295
+ accountId: args.accountId,
296
+ provider: args.provider,
297
+ connectionId: args.connectionId
298
+ });
299
+ if (makePrimary) await ctx.db.patch("User", args.userId, {
300
+ email: args.email,
301
+ ...verificationTime !== void 0 ? { emailVerificationTime: verificationTime } : {}
302
+ });
303
+ return id;
304
+ }
305
+ });
306
+ /**
307
+ * Promote one of the user's emails to primary, syncing the denormalized
308
+ * `User.email` / `emailVerificationTime` pointer. The target must exist
309
+ * and be verified.
310
+ *
311
+ * @param args.userId - Owner of the email.
312
+ * @param args.email - The address to promote (must be owned + verified).
313
+ * @returns `null`.
314
+ * @throws `INVALID_PARAMETERS` if the email is not owned or not verified.
315
+ *
316
+ */
317
+ const userEmailSetPrimary = mutation({
318
+ args: {
319
+ userId: v.id("User"),
320
+ email: v.string()
321
+ },
322
+ returns: v.null(),
323
+ handler: async (ctx, { userId, email }) => {
324
+ const owned = await ctx.db.query("UserEmail").withIndex("user_id", (q) => q.eq("userId", userId)).collect();
325
+ const target = owned.find((e) => e.email === email);
326
+ if (target === void 0) throw new ConvexError({
327
+ code: "INVALID_PARAMETERS",
328
+ message: "Email is not owned by this user."
329
+ });
330
+ if (target.verificationTime === void 0) throw new ConvexError({
331
+ code: "INVALID_PARAMETERS",
332
+ message: "Cannot make an unverified email primary."
333
+ });
334
+ await Promise.all(owned.filter((e) => e.isPrimary && e._id !== target._id).map((e) => ctx.db.patch("UserEmail", e._id, { isPrimary: false })));
335
+ await ctx.db.patch("UserEmail", target._id, { isPrimary: true });
336
+ await ctx.db.patch("User", userId, {
337
+ email: target.email,
338
+ emailVerificationTime: target.verificationTime
339
+ });
340
+ return null;
341
+ }
342
+ });
343
+ /**
344
+ * Remove an email a user owns. Guards: cannot remove the primary, the
345
+ * last verified email, or a connection-managed row (`saml`/`oidc`/`scim`
346
+ * with a `connectionId` — owned by the IdP/SCIM, not the user).
347
+ *
348
+ * @param args.userId - Owner of the email.
349
+ * @param args.email - The address to remove (must be owned).
350
+ * @returns `null`.
351
+ * @throws `INVALID_PARAMETERS` if not owned, primary, the only verified
352
+ * email, or connection-managed.
353
+ *
354
+ */
355
+ const userEmailRemove = mutation({
356
+ args: {
357
+ userId: v.id("User"),
358
+ email: v.string()
359
+ },
360
+ returns: v.null(),
361
+ handler: async (ctx, { userId, email }) => {
362
+ const owned = await ctx.db.query("UserEmail").withIndex("user_id", (q) => q.eq("userId", userId)).collect();
363
+ const target = owned.find((e) => e.email === email);
364
+ if (target === void 0) throw new ConvexError({
365
+ code: "INVALID_PARAMETERS",
366
+ message: "Email is not owned by this user."
367
+ });
368
+ if (target.isPrimary) throw new ConvexError({
369
+ code: "INVALID_PARAMETERS",
370
+ message: "Cannot remove the primary email; set another primary first."
371
+ });
372
+ if (target.connectionId !== void 0 && (target.source === "saml" || target.source === "oidc" || target.source === "scim")) throw new ConvexError({
373
+ code: "INVALID_PARAMETERS",
374
+ message: "This email is managed by an SSO/SCIM connection."
375
+ });
376
+ const verifiedCount = owned.filter((e) => e.verificationTime !== void 0).length;
377
+ if (target.verificationTime !== void 0 && verifiedCount <= 1) throw new ConvexError({
378
+ code: "INVALID_PARAMETERS",
379
+ message: "Cannot remove the only verified email."
380
+ });
381
+ await ctx.db.delete("UserEmail", target._id);
382
+ return null;
383
+ }
384
+ });
382
385
 
383
386
  //#endregion
384
- export { userDelete, userFindByVerifiedEmail, userFindByVerifiedPhone, userGetById, userGetMany, userInsert, userList, userPatch, userUpsert };
387
+ export { userDelete, userEmailListByUser, userEmailOwner, userEmailRemove, userEmailSetPrimary, userEmailUpsert, userInsert, userList, userPatch, userUpsert };
385
388
  //# sourceMappingURL=users.js.map
@@ -21,13 +21,6 @@ async function getUnexpiredVerifier(ctx, verifierId) {
21
21
  * When provided, the verifier is scoped to the given session.
22
22
  * @returns The document ID of the newly created verifier.
23
23
  *
24
- * @example
25
- * ```ts
26
- * const verifierId = await ctx.runMutation(
27
- * component.identity.verifiers.verifierCreate,
28
- * { sessionId: session._id },
29
- * );
30
- * ```
31
24
  */
32
25
  const verifierCreate = mutation({
33
26
  args: {
@@ -45,61 +38,24 @@ const verifierCreate = mutation({
45
38
  }
46
39
  });
47
40
  /**
48
- * Retrieve a single verifier by its Convex document ID.
49
- *
50
- * Performs a direct point lookup on the `AuthVerifier` table. Returns `null` if
51
- * the verifier has been deleted or never existed.
52
- *
53
- * @param args.verifierId - The Convex document ID (`Id<"AuthVerifier">`) of the verifier to retrieve.
54
- * @returns The verifier document if it exists, or `null` otherwise.
55
- *
56
- * @example
57
- * ```ts
58
- * const verifier = await ctx.runQuery(
59
- * component.identity.verifiers.verifierGetById,
60
- * { verifierId: storedVerifierId },
61
- * );
62
- * if (verifier !== null) {
63
- * console.log(`Verifier signature: ${verifier.signature}`);
64
- * }
65
- * ```
41
+ * Read a verifier by identity one function, all-optional args, unioned
42
+ * return: `{ id }` (point lookup) or `{ signature }` (unique index).
43
+ * Expiry enforced for both.
66
44
  */
67
- const verifierGetById = query({
68
- args: { verifierId: v.id("AuthVerifier") },
69
- returns: v.union(vAuthVerifierDoc, v.null()),
70
- handler: async (ctx, { verifierId }) => {
71
- return await getUnexpiredVerifier(ctx, verifierId);
72
- }
73
- });
74
- /**
75
- * Look up a verifier by its cryptographic signature.
76
- *
77
- * Queries the `AuthVerifier` table using the `signature` index to find the
78
- * unique verifier matching the given signature string. This is the primary
79
- * lookup used during the OAuth callback phase to correlate the incoming
80
- * authorization response with the original PKCE challenge.
81
- *
82
- * @param args.signature - The cryptographic signature string to search for (exact match).
83
- * @returns The matching verifier document, or `null` if no verifier has the given signature.
84
- *
85
- * @example
86
- * ```ts
87
- * const verifier = await ctx.runQuery(
88
- * component.identity.verifiers.verifierGetBySignature,
89
- * { signature: incomingStateParam },
90
- * );
91
- * if (verifier === null) {
92
- * throw new Error("Invalid or expired OAuth state");
93
- * }
94
- * ```
95
- */
96
- const verifierGetBySignature = query({
97
- args: { signature: v.string() },
45
+ const verifierGet = query({
46
+ args: {
47
+ id: v.optional(v.id("AuthVerifier")),
48
+ signature: v.optional(v.string())
49
+ },
98
50
  returns: v.union(vAuthVerifierDoc, v.null()),
99
- handler: async (ctx, { signature }) => {
100
- const verifier = await ctx.db.query("AuthVerifier").withIndex("signature", (q) => q.eq("signature", signature)).unique();
101
- if (verifier?.expirationTime !== void 0 && verifier.expirationTime < Date.now()) return null;
102
- return verifier;
51
+ handler: async (ctx, args) => {
52
+ if (args.signature !== void 0) {
53
+ const verifier = await ctx.db.query("AuthVerifier").withIndex("signature", (q) => q.eq("signature", args.signature)).unique();
54
+ if (verifier?.expirationTime !== void 0 && verifier.expirationTime < Date.now()) return null;
55
+ return verifier;
56
+ }
57
+ if (args.id === void 0) return null;
58
+ return await getUnexpiredVerifier(ctx, args.id);
103
59
  }
104
60
  });
105
61
  /**
@@ -114,17 +70,6 @@ const verifierGetBySignature = query({
114
70
  * (e.g. `{ signature: string }` or `{ sessionId: Id<"Session"> }`).
115
71
  * @returns `null` on success.
116
72
  *
117
- * @example
118
- * ```ts
119
- * // Set the PKCE signature on the verifier
120
- * await ctx.runMutation(
121
- * component.identity.verifiers.verifierPatch,
122
- * {
123
- * verifierId: verifier._id,
124
- * data: { signature: generatedSignature },
125
- * },
126
- * );
127
- * ```
128
73
  */
129
74
  const verifierPatch = mutation({
130
75
  args: {
@@ -147,14 +92,6 @@ const verifierPatch = mutation({
147
92
  * @param args.verifierId - The document ID of the verifier to delete.
148
93
  * @returns `null` on success.
149
94
  *
150
- * @example
151
- * ```ts
152
- * // Clean up the verifier after a successful OAuth exchange
153
- * await ctx.runMutation(
154
- * component.identity.verifiers.verifierDelete,
155
- * { verifierId: verifier._id },
156
- * );
157
- * ```
158
95
  */
159
96
  const verifierDelete = mutation({
160
97
  args: { verifierId: v.id("AuthVerifier") },
@@ -166,5 +103,5 @@ const verifierDelete = mutation({
166
103
  });
167
104
 
168
105
  //#endregion
169
- export { verifierCreate, verifierDelete, verifierGetById, verifierGetBySignature, verifierPatch };
106
+ export { verifierCreate, verifierDelete, verifierGet, verifierPatch };
170
107
  //# sourceMappingURL=verifiers.js.map