@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
@@ -21,19 +21,6 @@ import { v } from "convex/values";
21
21
  * @param status - Initial status of the device authorization (e.g. `"pending"`).
22
22
  * @returns The `_id` of the newly created `DeviceCode` document.
23
23
  *
24
- * @example
25
- * ```ts
26
- * const deviceCodeId = await ctx.runMutation(
27
- * components.auth.factors.devices.deviceInsert,
28
- * {
29
- * deviceCodeHash: "a1b2c3d4e5f6...",
30
- * userCode: "ABCD-1234",
31
- * expiresAt: Date.now() + 10 * 60 * 1000,
32
- * interval: 5,
33
- * status: "pending",
34
- * },
35
- * );
36
- * ```
37
24
  */
38
25
  const deviceInsert = mutation({
39
26
  args: {
@@ -49,62 +36,36 @@ const deviceInsert = mutation({
49
36
  }
50
37
  });
51
38
  /**
52
- * Look up a device authorization record by its hashed device code.
39
+ * Read a device authorization record by identity.
40
+ *
41
+ * Accepts exactly one selector:
42
+ * - `id` — direct document lookup by `DeviceCode` `_id`.
43
+ * - `deviceCodeHash` — lookup via the `device_code_hash` index. This is
44
+ * the primary lookup used by the token endpoint when a device client
45
+ * polls for authorization status.
46
+ * - `userCode` — the first `"pending"` record matching the user-facing
47
+ * code, via the `user_code_status` compound index. Used when an
48
+ * authenticated user enters the code shown on the device to approve it.
49
+ *
50
+ * @param id - Optional `_id` of the `DeviceCode` document to retrieve.
51
+ * @param deviceCodeHash - Optional SHA-256 hash of the device code.
52
+ * @param userCode - Optional short, human-readable code the user typed
53
+ * in (e.g. `"ABCD-1234"`).
54
+ * @returns The matching `DeviceCode` document, or `null` if none matches.
53
55
  *
54
- * Queries the `DeviceCode` table using the `device_code_hash` index.
55
- * This is the primary lookup used by the token endpoint when a device
56
- * client polls for authorization status.
57
- *
58
- * @param deviceCodeHash - SHA-256 hash of the device code to look up.
59
- * @returns The matching `DeviceCode` document, or `null` if no record
60
- * exists for the given hash.
61
- *
62
- * @example
63
- * ```ts
64
- * const deviceCode = await ctx.runQuery(
65
- * components.auth.factors.devices.deviceGetByCodeHash,
66
- * { deviceCodeHash: "a1b2c3d4e5f6..." },
67
- * );
68
- * if (deviceCode && deviceCode.status === "authorized") {
69
- * // Exchange for tokens
70
- * }
71
- * ```
72
56
  */
73
- const deviceGetByCodeHash = query({
74
- args: { deviceCodeHash: v.string() },
75
- returns: v.union(vDeviceCodeDoc, v.null()),
76
- handler: async (ctx, { deviceCodeHash }) => {
77
- return await ctx.db.query("DeviceCode").withIndex("device_code_hash", (q) => q.eq("deviceCodeHash", deviceCodeHash)).first();
78
- }
79
- });
80
- /**
81
- * Look up a pending device authorization by its user-facing code.
82
- *
83
- * Queries the `DeviceCode` table using the `user_code_status` compound index,
84
- * filtering to only `"pending"` records. This is called when an authenticated
85
- * user enters the code shown on the device to approve the authorization.
86
- *
87
- * @param userCode - The short, human-readable code the user typed in
88
- * (e.g. `"ABCD-1234"`).
89
- * @returns The matching pending `DeviceCode` document, or `null` if no
90
- * pending authorization exists for the given user code.
91
- *
92
- * @example
93
- * ```ts
94
- * const pending = await ctx.runQuery(
95
- * components.auth.factors.devices.deviceGetByUserCode,
96
- * { userCode: "ABCD-1234" },
97
- * );
98
- * if (pending === null) {
99
- * throw new Error("Invalid or expired user code");
100
- * }
101
- * ```
102
- */
103
- const deviceGetByUserCode = query({
104
- args: { userCode: v.string() },
57
+ const deviceGet = query({
58
+ args: {
59
+ id: v.optional(v.id("DeviceCode")),
60
+ deviceCodeHash: v.optional(v.string()),
61
+ userCode: v.optional(v.string())
62
+ },
105
63
  returns: v.union(vDeviceCodeDoc, v.null()),
106
- handler: async (ctx, { userCode }) => {
107
- return await ctx.db.query("DeviceCode").withIndex("user_code_status", (q) => q.eq("userCode", userCode).eq("status", "pending")).first();
64
+ handler: async (ctx, args) => {
65
+ if (args.deviceCodeHash !== void 0) return await ctx.db.query("DeviceCode").withIndex("device_code_hash", (q) => q.eq("deviceCodeHash", args.deviceCodeHash)).first();
66
+ if (args.userCode !== void 0) return await ctx.db.query("DeviceCode").withIndex("user_code_status", (q) => q.eq("userCode", args.userCode).eq("status", "pending")).first();
67
+ if (args.id === void 0) return null;
68
+ return await ctx.db.get("DeviceCode", args.id);
108
69
  }
109
70
  });
110
71
  /**
@@ -121,17 +82,6 @@ const deviceGetByUserCode = query({
121
82
  * approving user's current login.
122
83
  * @returns `null` on success.
123
84
  *
124
- * @example
125
- * ```ts
126
- * await ctx.runMutation(
127
- * components.auth.factors.devices.deviceAuthorize,
128
- * {
129
- * deviceId: pending._id,
130
- * userId: currentUser._id,
131
- * sessionId: currentSession._id,
132
- * },
133
- * );
134
- * ```
135
85
  */
136
86
  const deviceAuthorize = mutation({
137
87
  args: {
@@ -150,36 +100,25 @@ const deviceAuthorize = mutation({
150
100
  }
151
101
  });
152
102
  /**
153
- * Update the last-polled timestamp on a device authorization record.
103
+ * Partially update a device authorization record.
154
104
  *
155
- * Called each time the device client polls the token endpoint. The
156
- * timestamp is used to enforce the minimum polling interval and to
157
- * detect slow-polling violations per RFC 8628.
105
+ * Performs a partial patch on the `DeviceCode` document e.g. bumping
106
+ * `lastPolledAt` on each poll to enforce the minimum polling interval
107
+ * and detect slow-polling violations per RFC 8628.
158
108
  *
159
109
  * @param deviceId - The `_id` of the `DeviceCode` document to update.
160
- * @param lastPolledAt - Unix timestamp (in milliseconds) of the most
161
- * recent poll request from the device client.
110
+ * @param data - An object containing the fields to patch.
162
111
  * @returns `null` on success.
163
112
  *
164
- * @example
165
- * ```ts
166
- * await ctx.runMutation(
167
- * components.auth.factors.devices.deviceUpdateLastPolled,
168
- * {
169
- * deviceId: deviceCode._id,
170
- * lastPolledAt: Date.now(),
171
- * },
172
- * );
173
- * ```
174
113
  */
175
- const deviceUpdateLastPolled = mutation({
114
+ const deviceUpdate = mutation({
176
115
  args: {
177
116
  deviceId: v.id("DeviceCode"),
178
- lastPolledAt: v.number()
117
+ data: v.any()
179
118
  },
180
119
  returns: v.null(),
181
- handler: async (ctx, { deviceId, lastPolledAt }) => {
182
- await ctx.db.patch("DeviceCode", deviceId, { lastPolledAt });
120
+ handler: async (ctx, { deviceId, data }) => {
121
+ await ctx.db.patch("DeviceCode", deviceId, data);
183
122
  return null;
184
123
  }
185
124
  });
@@ -193,14 +132,6 @@ const deviceUpdateLastPolled = mutation({
193
132
  * @param deviceId - The `_id` of the `DeviceCode` document to delete.
194
133
  * @returns `null` on success.
195
134
  *
196
- * @example
197
- * ```ts
198
- * // Clean up after successful token exchange
199
- * await ctx.runMutation(
200
- * components.auth.factors.devices.deviceDelete,
201
- * { deviceId: deviceCode._id },
202
- * );
203
- * ```
204
135
  */
205
136
  const deviceDelete = mutation({
206
137
  args: { deviceId: v.id("DeviceCode") },
@@ -212,5 +143,5 @@ const deviceDelete = mutation({
212
143
  });
213
144
 
214
145
  //#endregion
215
- export { deviceAuthorize, deviceDelete, deviceGetByCodeHash, deviceGetByUserCode, deviceInsert, deviceUpdateLastPolled };
146
+ export { deviceAuthorize, deviceDelete, deviceGet, deviceInsert, deviceUpdate };
216
147
  //# sourceMappingURL=devices.js.map
@@ -32,24 +32,6 @@ import { v } from "convex/values";
32
32
  * was registered.
33
33
  * @returns The `_id` of the newly created `Passkey` document.
34
34
  *
35
- * @example
36
- * ```ts
37
- * const passkeyId = await ctx.runMutation(
38
- * components.auth.factors.passkeys.passkeyInsert,
39
- * {
40
- * userId: user._id,
41
- * credentialId: "dGVzdC1jcmVkZW50aWFs",
42
- * publicKey: publicKeyBytes,
43
- * algorithm: -7,
44
- * counter: 0,
45
- * transports: ["internal"],
46
- * deviceType: "multiDevice",
47
- * backedUp: true,
48
- * name: "MacBook Pro Touch ID",
49
- * createdAt: Date.now(),
50
- * },
51
- * );
52
- * ```
53
35
  */
54
36
  const passkeyInsert = mutation({
55
37
  args: {
@@ -70,63 +52,30 @@ const passkeyInsert = mutation({
70
52
  }
71
53
  });
72
54
  /**
73
- * Look up a passkey by its credential ID.
55
+ * Read a passkey by identity.
74
56
  *
75
- * Queries the `Passkey` table using the `credential_id` unique index.
76
- * This is the primary lookup during a WebAuthn authentication ceremony:
77
- * the authenticator provides a credential ID, and this function retrieves
78
- * the corresponding public key and counter for signature verification.
57
+ * Accepts exactly one selector:
58
+ * - `id` direct document lookup by `Passkey` `_id`.
59
+ * - `credentialId` lookup via the `credential_id` unique index. This
60
+ * is the primary lookup during a WebAuthn authentication ceremony: the
61
+ * authenticator provides a credential ID, and this resolves the
62
+ * corresponding public key and counter for signature verification.
79
63
  *
80
- * @param credentialId - Base64url-encoded credential identifier to search for.
81
- * @returns The matching `Passkey` document, or `null` if no passkey exists
82
- * with the given credential ID.
64
+ * @param id - Optional `_id` of the `Passkey` document to retrieve.
65
+ * @param credentialId - Optional base64url-encoded credential identifier.
66
+ * @returns The matching `Passkey` document, or `null` if none matches.
83
67
  *
84
- * @example
85
- * ```ts
86
- * const passkey = await ctx.runQuery(
87
- * components.auth.factors.passkeys.passkeyGetByCredentialId,
88
- * { credentialId: "dGVzdC1jcmVkZW50aWFs" },
89
- * );
90
- * if (passkey === null) {
91
- * throw new Error("Unknown credential");
92
- * }
93
- * ```
94
68
  */
95
- const passkeyGetByCredentialId = query({
96
- args: { credentialId: v.string() },
97
- returns: v.union(vPasskeyDoc, v.null()),
98
- handler: async (ctx, { credentialId }) => {
99
- return await ctx.db.query("Passkey").withIndex("credential_id", (q) => q.eq("credentialId", credentialId)).unique();
100
- }
101
- });
102
- /**
103
- * Get a single passkey by its document ID.
104
- *
105
- * Performs a direct point lookup on the `Passkey` table. Returns `null`
106
- * when no passkey exists with the given ID (e.g. it was already deleted).
107
- * Useful when callers already hold a passkey ID and need to inspect the
108
- * document — for example to capture the owning `userId` before deletion.
109
- *
110
- * @param passkeyId - The `_id` of the `Passkey` document to retrieve.
111
- * @returns The `Passkey` document, or `null` if no passkey exists with the
112
- * given ID.
113
- *
114
- * @example
115
- * ```ts
116
- * const passkey = await ctx.runQuery(
117
- * components.auth.factors.passkeys.passkeyGetById,
118
- * { passkeyId },
119
- * );
120
- * if (passkey === null) {
121
- * throw new Error("Passkey not found");
122
- * }
123
- * ```
124
- */
125
- const passkeyGetById = query({
126
- args: { passkeyId: v.id("Passkey") },
69
+ const passkeyGet = query({
70
+ args: {
71
+ id: v.optional(v.id("Passkey")),
72
+ credentialId: v.optional(v.string())
73
+ },
127
74
  returns: v.union(vPasskeyDoc, v.null()),
128
- handler: async (ctx, { passkeyId }) => {
129
- return await ctx.db.get("Passkey", passkeyId);
75
+ handler: async (ctx, args) => {
76
+ if (args.credentialId !== void 0) return await ctx.db.query("Passkey").withIndex("credential_id", (q) => q.eq("credentialId", args.credentialId)).unique();
77
+ if (args.id === void 0) return null;
78
+ return await ctx.db.get("Passkey", args.id);
130
79
  }
131
80
  });
132
81
  /**
@@ -141,19 +90,8 @@ const passkeyGetById = query({
141
90
  * @returns An array of `Passkey` documents. Returns an empty array if the
142
91
  * user has no registered passkeys.
143
92
  *
144
- * @example
145
- * ```ts
146
- * const passkeys = await ctx.runQuery(
147
- * components.auth.factors.passkeys.passkeyListByUserId,
148
- * { userId: user._id },
149
- * );
150
- * // Display each passkey's name and creation date
151
- * for (const pk of passkeys) {
152
- * console.log(pk.name, new Date(pk.createdAt));
153
- * }
154
- * ```
155
93
  */
156
- const passkeyListByUserId = query({
94
+ const passkeyList = query({
157
95
  args: { userId: v.id("User") },
158
96
  returns: v.array(vPasskeyDoc),
159
97
  handler: async (ctx, { userId }) => {
@@ -161,71 +99,20 @@ const passkeyListByUserId = query({
161
99
  }
162
100
  });
163
101
  /**
164
- * Update a passkey's signature counter and last-used timestamp after
165
- * a successful authentication.
166
- *
167
- * After verifying a WebAuthn assertion, the relying party must persist
168
- * the new counter value reported by the authenticator. A counter that
169
- * does not increase may indicate a cloned credential.
170
- *
171
- * @param passkeyId - The `_id` of the `Passkey` document to update.
172
- * @param counter - The new signature counter value returned by the
173
- * authenticator in the assertion response.
174
- * @param lastUsedAt - Unix timestamp (in milliseconds) recording when
175
- * this passkey was most recently used to authenticate.
176
- * @returns `null` on success.
177
- *
178
- * @example
179
- * ```ts
180
- * await ctx.runMutation(
181
- * components.auth.factors.passkeys.passkeyUpdateCounter,
182
- * {
183
- * passkeyId: passkey._id,
184
- * counter: assertionResponse.counter,
185
- * lastUsedAt: Date.now(),
186
- * },
187
- * );
188
- * ```
189
- */
190
- const passkeyUpdateCounter = mutation({
191
- args: {
192
- passkeyId: v.id("Passkey"),
193
- counter: v.number(),
194
- lastUsedAt: v.number()
195
- },
196
- returns: v.null(),
197
- handler: async (ctx, { passkeyId, counter, lastUsedAt }) => {
198
- await ctx.db.patch("Passkey", passkeyId, {
199
- counter,
200
- lastUsedAt
201
- });
202
- return null;
203
- }
204
- });
205
- /**
206
- * Update a passkey's metadata fields.
102
+ * Partially update a passkey document.
207
103
  *
208
- * Performs a partial patch on the `Passkey` document. Typically used to
209
- * rename a passkey (e.g. from `"Security Key"` to `"YubiKey 5C"`), but
210
- * can update any mutable fields via the `data` argument.
104
+ * Performs a partial patch on the `Passkey` document. Used both to
105
+ * rename a passkey (`{ name }`) and to persist the signature counter
106
+ * and last-used timestamp after a successful WebAuthn assertion
107
+ * (`{ counter, lastUsedAt }`) — a counter that does not increase may
108
+ * indicate a cloned credential.
211
109
  *
212
110
  * @param passkeyId - The `_id` of the `Passkey` document to update.
213
- * @param data - An object containing the fields to patch. Commonly
214
- * includes `{ name: "New Label" }`, but accepts any valid passkey fields.
111
+ * @param data - An object containing the fields to patch.
215
112
  * @returns `null` on success.
216
113
  *
217
- * @example
218
- * ```ts
219
- * await ctx.runMutation(
220
- * components.auth.factors.passkeys.passkeyUpdateMeta,
221
- * {
222
- * passkeyId: passkey._id,
223
- * data: { name: "YubiKey 5C NFC" },
224
- * },
225
- * );
226
- * ```
227
114
  */
228
- const passkeyUpdateMeta = mutation({
115
+ const passkeyUpdate = mutation({
229
116
  args: {
230
117
  passkeyId: v.id("Passkey"),
231
118
  data: v.any()
@@ -246,13 +133,6 @@ const passkeyUpdateMeta = mutation({
246
133
  * @param passkeyId - The `_id` of the `Passkey` document to delete.
247
134
  * @returns `null` on success.
248
135
  *
249
- * @example
250
- * ```ts
251
- * await ctx.runMutation(
252
- * components.auth.factors.passkeys.passkeyDelete,
253
- * { passkeyId: passkey._id },
254
- * );
255
- * ```
256
136
  */
257
137
  const passkeyDelete = mutation({
258
138
  args: { passkeyId: v.id("Passkey") },
@@ -264,5 +144,5 @@ const passkeyDelete = mutation({
264
144
  });
265
145
 
266
146
  //#endregion
267
- export { passkeyDelete, passkeyGetByCredentialId, passkeyGetById, passkeyInsert, passkeyListByUserId, passkeyUpdateCounter, passkeyUpdateMeta };
147
+ export { passkeyDelete, passkeyGet, passkeyInsert, passkeyList, passkeyUpdate };
268
148
  //# sourceMappingURL=passkeys.js.map
@@ -24,21 +24,6 @@ import { v } from "convex/values";
24
24
  * was created.
25
25
  * @returns The `_id` of the newly created `TotpFactor` document.
26
26
  *
27
- * @example
28
- * ```ts
29
- * const totpId = await ctx.runMutation(
30
- * components.auth.factors.totp.totpInsert,
31
- * {
32
- * userId: user._id,
33
- * secret: crypto.getRandomValues(new Uint8Array(20)),
34
- * digits: 6,
35
- * period: 30,
36
- * verified: false,
37
- * name: "Authenticator App",
38
- * createdAt: Date.now(),
39
- * },
40
- * );
41
- * ```
42
27
  */
43
28
  const totpInsert = mutation({
44
29
  args: {
@@ -56,34 +41,31 @@ const totpInsert = mutation({
56
41
  }
57
42
  });
58
43
  /**
59
- * Get a verified TOTP enrollment for a user.
44
+ * Read a TOTP enrollment by identity.
60
45
  *
61
- * Queries the `TotpFactor` table using the `user_id_verified` compound
62
- * index to find the first enrollment that has been successfully verified.
63
- * This is the primary lookup during a TOTP authentication challenge --
64
- * only verified enrollments should be used to validate codes.
46
+ * Accepts exactly one selector:
47
+ * - `id` direct document lookup by `TotpFactor` `_id`.
48
+ * - `verifiedForUserId` the first verified enrollment for the given
49
+ * user, via the `user_id_verified` compound index. This is the primary
50
+ * lookup during a TOTP authentication challenge, since only verified
51
+ * enrollments may validate codes.
65
52
  *
66
- * @param userId - The `_id` of the `User` whose verified TOTP enrollment
67
- * to retrieve.
68
- * @returns The first verified `TotpFactor` document for the user, or
69
- * `null` if the user has no verified TOTP enrollment.
53
+ * @param id - Optional `_id` of the `TotpFactor` document to retrieve.
54
+ * @param verifiedForUserId - Optional `_id` of the `User` whose first
55
+ * verified enrollment to retrieve.
56
+ * @returns The matching `TotpFactor` document, or `null` if none matches.
70
57
  *
71
- * @example
72
- * ```ts
73
- * const totp = await ctx.runQuery(
74
- * components.auth.factors.totp.totpGetVerifiedByUserId,
75
- * { userId: user._id },
76
- * );
77
- * if (totp === null) {
78
- * // User does not have TOTP 2FA enabled
79
- * }
80
- * ```
81
58
  */
82
- const totpGetVerifiedByUserId = query({
83
- args: { userId: v.id("User") },
59
+ const totpGet = query({
60
+ args: {
61
+ id: v.optional(v.id("TotpFactor")),
62
+ verifiedForUserId: v.optional(v.id("User"))
63
+ },
84
64
  returns: v.union(vTotpFactorDoc, v.null()),
85
- handler: async (ctx, { userId }) => {
86
- return await ctx.db.query("TotpFactor").withIndex("user_id_verified", (q) => q.eq("userId", userId).eq("verified", true)).first();
65
+ handler: async (ctx, args) => {
66
+ if (args.verifiedForUserId !== void 0) return await ctx.db.query("TotpFactor").withIndex("user_id_verified", (q) => q.eq("userId", args.verifiedForUserId).eq("verified", true)).first();
67
+ if (args.id === void 0) return null;
68
+ return await ctx.db.get("TotpFactor", args.id);
87
69
  }
88
70
  });
89
71
  /**
@@ -99,17 +81,8 @@ const totpGetVerifiedByUserId = query({
99
81
  * @returns An array of `TotpFactor` documents. Returns an empty array if
100
82
  * the user has no TOTP enrollments.
101
83
  *
102
- * @example
103
- * ```ts
104
- * const factors = await ctx.runQuery(
105
- * components.auth.factors.totp.totpListByUserId,
106
- * { userId: user._id },
107
- * );
108
- * const verified = factors.filter((f) => f.verified);
109
- * const pending = factors.filter((f) => !f.verified);
110
- * ```
111
84
  */
112
- const totpListByUserId = query({
85
+ const totpList = query({
113
86
  args: { userId: v.id("User") },
114
87
  returns: v.array(vTotpFactorDoc),
115
88
  handler: async (ctx, { userId }) => {
@@ -117,111 +90,26 @@ const totpListByUserId = query({
117
90
  }
118
91
  });
119
92
  /**
120
- * Get a single TOTP enrollment by its document ID.
121
- *
122
- * Performs a direct document lookup on the `TotpFactor` table. This is
123
- * used when you already have the enrollment's `_id` (e.g. from a
124
- * previous list query) and need to fetch its full details, including
125
- * the secret and verification status.
126
- *
127
- * @param totpId - The `_id` of the `TotpFactor` document to retrieve.
128
- * @returns The `TotpFactor` document, or `null` if no enrollment exists
129
- * with the given ID.
130
- *
131
- * @example
132
- * ```ts
133
- * const totp = await ctx.runQuery(
134
- * components.auth.factors.totp.totpGetById,
135
- * { totpId: enrollmentId },
136
- * );
137
- * if (totp !== null && !totp.verified) {
138
- * // Enrollment is still pending confirmation
139
- * }
140
- * ```
141
- */
142
- const totpGetById = query({
143
- args: { totpId: v.id("TotpFactor") },
144
- returns: v.union(vTotpFactorDoc, v.null()),
145
- handler: async (ctx, { totpId }) => {
146
- return await ctx.db.get("TotpFactor", totpId);
147
- }
148
- });
149
- /**
150
- * Mark a TOTP enrollment as verified, completing the setup process.
151
- *
152
- * Called after the user successfully submits a valid TOTP code during
153
- * enrollment. This transitions the factor from a pending state to an
154
- * active, verified state, enabling it for future authentication
155
- * challenges.
156
- *
157
- * @param totpId - The `_id` of the `TotpFactor` document to mark as
158
- * verified.
159
- * @param lastUsedAt - Unix timestamp (in milliseconds) recording when
160
- * the verification code was successfully validated.
161
- * @returns `null` on success.
162
- *
163
- * @example
164
- * ```ts
165
- * // After validating the user's TOTP code during setup
166
- * await ctx.runMutation(
167
- * components.auth.factors.totp.totpMarkVerified,
168
- * {
169
- * totpId: enrollment._id,
170
- * lastUsedAt: Date.now(),
171
- * },
172
- * );
173
- * ```
174
- */
175
- const totpMarkVerified = mutation({
176
- args: {
177
- totpId: v.id("TotpFactor"),
178
- lastUsedAt: v.number()
179
- },
180
- returns: v.null(),
181
- handler: async (ctx, { totpId, lastUsedAt }) => {
182
- await ctx.db.patch("TotpFactor", totpId, {
183
- verified: true,
184
- lastUsedAt
185
- });
186
- const factor = await ctx.db.get("TotpFactor", totpId);
187
- if (factor !== null) {
188
- const user = await ctx.db.get("User", factor.userId);
189
- if (user !== null && user.hasTotp !== true) await ctx.db.patch("User", factor.userId, { hasTotp: true });
190
- }
191
- return null;
192
- }
193
- });
194
- /**
195
- * Update a TOTP enrollment's last-used timestamp.
93
+ * Partially update a TOTP enrollment.
196
94
  *
197
- * Called after each successful TOTP code validation during sign-in.
198
- * Tracking the last-used time helps detect stale enrollments and can
199
- * be surfaced in security settings for user awareness.
95
+ * Performs a partial patch on the `TotpFactor` document. Used to confirm
96
+ * an enrollment (`{ verified: true, lastUsedAt }`) and to bump
97
+ * `lastUsedAt` after each successful validation (stale-enrollment
98
+ * tracking).
200
99
  *
201
100
  * @param totpId - The `_id` of the `TotpFactor` document to update.
202
- * @param lastUsedAt - Unix timestamp (in milliseconds) recording when
203
- * the TOTP code was most recently validated.
101
+ * @param data - An object containing the fields to patch.
204
102
  * @returns `null` on success.
205
103
  *
206
- * @example
207
- * ```ts
208
- * await ctx.runMutation(
209
- * components.auth.factors.totp.totpUpdateLastUsed,
210
- * {
211
- * totpId: totp._id,
212
- * lastUsedAt: Date.now(),
213
- * },
214
- * );
215
- * ```
216
104
  */
217
- const totpUpdateLastUsed = mutation({
105
+ const totpUpdate = mutation({
218
106
  args: {
219
107
  totpId: v.id("TotpFactor"),
220
- lastUsedAt: v.number()
108
+ data: v.any()
221
109
  },
222
110
  returns: v.null(),
223
- handler: async (ctx, { totpId, lastUsedAt }) => {
224
- await ctx.db.patch("TotpFactor", totpId, { lastUsedAt });
111
+ handler: async (ctx, { totpId, data }) => {
112
+ await ctx.db.patch("TotpFactor", totpId, data);
225
113
  return null;
226
114
  }
227
115
  });
@@ -236,31 +124,16 @@ const totpUpdateLastUsed = mutation({
236
124
  * @param totpId - The `_id` of the `TotpFactor` document to delete.
237
125
  * @returns `null` on success.
238
126
  *
239
- * @example
240
- * ```ts
241
- * // User disables TOTP 2FA
242
- * await ctx.runMutation(
243
- * components.auth.factors.totp.totpDelete,
244
- * { totpId: totp._id },
245
- * );
246
- * ```
247
127
  */
248
128
  const totpDelete = mutation({
249
129
  args: { totpId: v.id("TotpFactor") },
250
130
  returns: v.null(),
251
131
  handler: async (ctx, { totpId }) => {
252
- const factor = await ctx.db.get("TotpFactor", totpId);
253
132
  await ctx.db.delete("TotpFactor", totpId);
254
- if (factor !== null && factor.verified) {
255
- if (await ctx.db.query("TotpFactor").withIndex("user_id_verified", (q) => q.eq("userId", factor.userId).eq("verified", true)).first() === null) {
256
- const user = await ctx.db.get("User", factor.userId);
257
- if (user !== null && user.hasTotp === true) await ctx.db.patch("User", factor.userId, { hasTotp: false });
258
- }
259
- }
260
133
  return null;
261
134
  }
262
135
  });
263
136
 
264
137
  //#endregion
265
- export { totpDelete, totpGetById, totpGetVerifiedByUserId, totpInsert, totpListByUserId, totpMarkVerified, totpUpdateLastUsed };
138
+ export { totpDelete, totpGet, totpInsert, totpList, totpUpdate };
266
139
  //# sourceMappingURL=totp.js.map