@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
|
@@ -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
|
-
*
|
|
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
|
|
74
|
-
args: {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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,
|
|
107
|
-
return await ctx.db.query("DeviceCode").withIndex("
|
|
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
|
-
*
|
|
103
|
+
* Partially update a device authorization record.
|
|
154
104
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
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
|
|
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
|
|
114
|
+
const deviceUpdate = mutation({
|
|
176
115
|
args: {
|
|
177
116
|
deviceId: v.id("DeviceCode"),
|
|
178
|
-
|
|
117
|
+
data: v.any()
|
|
179
118
|
},
|
|
180
119
|
returns: v.null(),
|
|
181
|
-
handler: async (ctx, { deviceId,
|
|
182
|
-
await ctx.db.patch("DeviceCode", deviceId,
|
|
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,
|
|
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
|
-
*
|
|
55
|
+
* Read a passkey by identity.
|
|
74
56
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* the
|
|
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
|
|
81
|
-
* @
|
|
82
|
-
*
|
|
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
|
|
96
|
-
args: {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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,
|
|
129
|
-
return await ctx.db.
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
209
|
-
* rename a passkey (
|
|
210
|
-
*
|
|
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.
|
|
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
|
|
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,
|
|
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
|
-
*
|
|
44
|
+
* Read a TOTP enrollment by identity.
|
|
60
45
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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
|
|
83
|
-
args: {
|
|
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,
|
|
86
|
-
return await ctx.db.query("TotpFactor").withIndex("user_id_verified", (q) => q.eq("userId",
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
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
|
|
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
|
|
105
|
+
const totpUpdate = mutation({
|
|
218
106
|
args: {
|
|
219
107
|
totpId: v.id("TotpFactor"),
|
|
220
|
-
|
|
108
|
+
data: v.any()
|
|
221
109
|
},
|
|
222
110
|
returns: v.null(),
|
|
223
|
-
handler: async (ctx, { totpId,
|
|
224
|
-
await ctx.db.patch("TotpFactor", totpId,
|
|
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,
|
|
138
|
+
export { totpDelete, totpGet, totpInsert, totpList, totpUpdate };
|
|
266
139
|
//# sourceMappingURL=totp.js.map
|