@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.
- package/dist/component/_generated/component.d.ts +1611 -2039
- package/dist/component/account.js +3 -0
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/factor/device.js +3 -0
- package/dist/component/factor/passkey.js +3 -0
- package/dist/component/factor/totp.js +3 -0
- package/dist/component/group/invite.js +3 -0
- package/dist/component/group/member.js +3 -0
- package/dist/component/group.js +3 -0
- package/dist/component/model.d.ts +342 -30
- package/dist/component/model.js +22 -4
- package/dist/component/modules.js +24 -21
- package/dist/component/public/factors/devices.js +37 -106
- package/dist/component/public/factors/passkeys.js +29 -149
- package/dist/component/public/factors/totp.js +32 -159
- package/dist/component/public/groups/core.js +19 -82
- package/dist/component/public/groups/invites.js +15 -104
- package/dist/component/public/groups/members.js +26 -149
- package/dist/component/public/identity/accounts.js +12 -94
- package/dist/component/public/identity/codes.js +13 -73
- package/dist/component/public/identity/sessions.js +5 -107
- package/dist/component/public/identity/tokens.js +13 -103
- package/dist/component/public/identity/users.js +188 -185
- package/dist/component/public/identity/verifiers.js +17 -80
- package/dist/component/public/security/keys.js +13 -120
- package/dist/component/public/security/limits.js +0 -43
- package/dist/component/public/sso/audit.js +0 -28
- package/dist/component/public/sso/core.js +31 -104
- package/dist/component/public/sso/domains.js +0 -71
- package/dist/component/public/sso/scim.js +63 -239
- package/dist/component/public/sso/secrets.js +0 -30
- package/dist/component/public/sso/webhooks.js +23 -128
- package/dist/component/rateLimit.js +3 -0
- package/dist/component/schema.d.ts +378 -342
- package/dist/component/schema.js +11 -1
- package/dist/component/session.js +3 -0
- package/dist/component/sso/audit.js +3 -0
- package/dist/component/sso/connection/domain/verification.js +3 -0
- package/dist/component/sso/connection/domain.js +3 -0
- package/dist/component/sso/connection/scim/config.js +3 -0
- package/dist/component/sso/connection/scim/identity.js +3 -0
- package/dist/component/sso/connection/secret.js +3 -0
- package/dist/component/sso/connection.js +3 -0
- package/dist/component/sso/webhook/delivery.js +3 -0
- package/dist/component/sso/webhook/endpoint.js +3 -0
- package/dist/component/token/pkce.js +3 -0
- package/dist/component/token/refresh.js +3 -0
- package/dist/component/token/verification.js +3 -0
- package/dist/component/user/email.js +3 -0
- package/dist/component/user/key.js +3 -0
- package/dist/component/user.js +62 -0
- package/dist/core/index.d.ts +131 -28
- package/dist/core/index.js +2 -0
- package/dist/model.js +391 -0
- package/dist/providers/credentials.d.ts +1 -1
- package/dist/providers/github.js +6 -0
- package/dist/providers/password.js +1 -2
- package/dist/server/auth.d.ts +73 -7
- package/dist/server/auth.js +4 -1
- package/dist/server/context.js +30 -3
- package/dist/server/contract.js +42 -42
- package/dist/server/core.js +224 -86
- package/dist/server/db.js +45 -37
- package/dist/server/facade.d.ts +39 -0
- package/dist/server/facade.js +16 -0
- package/dist/server/index.d.ts +5 -3
- package/dist/server/index.js +3 -1
- package/dist/server/mounts.d.ts +101 -101
- package/dist/server/mutations/credentials/signin.js +3 -7
- package/dist/server/mutations/oauth.js +9 -6
- package/dist/server/runtime.d.ts +147 -46
- package/dist/server/runtime.js +10 -8
- package/dist/server/services/group.js +9 -9
- package/dist/server/sso/domain.d.ts +1 -1
- package/dist/server/sso/domain.js +40 -40
- package/dist/server/sso/http.js +18 -18
- package/dist/server/sso/oidc.js +1 -1
- package/dist/server/sso/policies.js +3 -3
- package/dist/server/sso/policy.js +12 -4
- package/dist/server/sso/provision.js +9 -9
- package/dist/server/sso/validators.js +2 -2
- package/dist/server/sso/webhook.js +8 -8
- package/dist/server/types.d.ts +185 -124
- package/dist/server/types.js +29 -24
- package/dist/server/users.js +49 -2
- package/dist/server/validators.d.ts +745 -0
- package/dist/server/validators.js +60 -0
- package/package.json +1 -1
- 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,
|
|
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
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
|
68
|
-
args: {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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,
|
|
106
|
+
export { verifierCreate, verifierDelete, verifierGet, verifierPatch };
|
|
170
107
|
//# sourceMappingURL=verifiers.js.map
|