@robelest/convex-auth 0.0.2 → 0.0.3-preview.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.cjs +1 -1
- package/dist/client/index.d.ts +33 -9
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +79 -13
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +48 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/index.d.ts +10 -4
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/index.js +8 -3
- package/dist/component/index.js.map +1 -1
- package/dist/component/public.d.ts +163 -3
- package/dist/component/public.d.ts.map +1 -1
- package/dist/component/public.js +124 -0
- package/dist/component/public.js.map +1 -1
- package/dist/component/schema.d.ts +81 -2
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +45 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/providers/anonymous.d.ts +3 -0
- package/dist/providers/anonymous.d.ts.map +1 -1
- package/dist/providers/anonymous.js +3 -0
- package/dist/providers/anonymous.js.map +1 -1
- package/dist/providers/credentials.d.ts +3 -0
- package/dist/providers/credentials.d.ts.map +1 -1
- package/dist/providers/credentials.js +3 -0
- package/dist/providers/credentials.js.map +1 -1
- package/dist/providers/email.d.ts +3 -0
- package/dist/providers/email.d.ts.map +1 -1
- package/dist/providers/email.js +3 -0
- package/dist/providers/email.js.map +1 -1
- package/dist/providers/passkey.d.ts +7 -1
- package/dist/providers/passkey.d.ts.map +1 -1
- package/dist/providers/passkey.js +7 -1
- package/dist/providers/passkey.js.map +1 -1
- package/dist/providers/password.d.ts +3 -0
- package/dist/providers/password.d.ts.map +1 -1
- package/dist/providers/password.js +3 -0
- package/dist/providers/password.js.map +1 -1
- package/dist/providers/phone.d.ts +3 -0
- package/dist/providers/phone.d.ts.map +1 -1
- package/dist/providers/phone.js +3 -0
- package/dist/providers/phone.js.map +1 -1
- package/dist/providers/totp.d.ts +8 -0
- package/dist/providers/totp.d.ts.map +1 -1
- package/dist/providers/totp.js +8 -0
- package/dist/providers/totp.js.map +1 -1
- package/dist/server/convex-auth.d.ts +185 -25
- package/dist/server/convex-auth.d.ts.map +1 -1
- package/dist/server/convex-auth.js +317 -58
- package/dist/server/convex-auth.js.map +1 -1
- package/dist/server/email-templates.d.ts +18 -0
- package/dist/server/email-templates.d.ts.map +1 -0
- package/dist/server/email-templates.js +74 -0
- package/dist/server/email-templates.js.map +1 -0
- package/dist/server/errors.d.ts +146 -0
- package/dist/server/errors.d.ts.map +1 -0
- package/dist/server/errors.js +176 -0
- package/dist/server/errors.js.map +1 -0
- package/dist/server/implementation/apiKey.d.ts +74 -0
- package/dist/server/implementation/apiKey.d.ts.map +1 -0
- package/dist/server/implementation/apiKey.js +139 -0
- package/dist/server/implementation/apiKey.js.map +1 -0
- package/dist/server/implementation/index.d.ts +151 -14
- package/dist/server/implementation/index.d.ts.map +1 -1
- package/dist/server/implementation/index.js +216 -24
- package/dist/server/implementation/index.js.map +1 -1
- package/dist/server/implementation/mutations/createAccountFromCredentials.d.ts.map +1 -1
- package/dist/server/implementation/mutations/createAccountFromCredentials.js +2 -1
- package/dist/server/implementation/mutations/createAccountFromCredentials.js.map +1 -1
- package/dist/server/implementation/mutations/createVerificationCode.d.ts +2 -2
- package/dist/server/implementation/mutations/index.d.ts +6 -6
- package/dist/server/implementation/mutations/modifyAccount.d.ts.map +1 -1
- package/dist/server/implementation/mutations/modifyAccount.js +2 -1
- package/dist/server/implementation/mutations/modifyAccount.js.map +1 -1
- package/dist/server/implementation/mutations/userOAuth.d.ts.map +1 -1
- package/dist/server/implementation/mutations/userOAuth.js +2 -1
- package/dist/server/implementation/mutations/userOAuth.js.map +1 -1
- package/dist/server/implementation/mutations/verifierSignature.d.ts.map +1 -1
- package/dist/server/implementation/mutations/verifierSignature.js +2 -1
- package/dist/server/implementation/mutations/verifierSignature.js.map +1 -1
- package/dist/server/implementation/passkey.d.ts.map +1 -1
- package/dist/server/implementation/passkey.js +28 -29
- package/dist/server/implementation/passkey.js.map +1 -1
- package/dist/server/implementation/provider.d.ts.map +1 -1
- package/dist/server/implementation/provider.js +5 -4
- package/dist/server/implementation/provider.js.map +1 -1
- package/dist/server/implementation/redirects.d.ts.map +1 -1
- package/dist/server/implementation/redirects.js +2 -1
- package/dist/server/implementation/redirects.js.map +1 -1
- package/dist/server/implementation/refreshTokens.d.ts.map +1 -1
- package/dist/server/implementation/refreshTokens.js +2 -1
- package/dist/server/implementation/refreshTokens.js.map +1 -1
- package/dist/server/implementation/signIn.d.ts.map +1 -1
- package/dist/server/implementation/signIn.js +8 -18
- package/dist/server/implementation/signIn.js.map +1 -1
- package/dist/server/implementation/totp.d.ts.map +1 -1
- package/dist/server/implementation/totp.js +16 -17
- package/dist/server/implementation/totp.js.map +1 -1
- package/dist/server/implementation/users.d.ts.map +1 -1
- package/dist/server/implementation/users.js +3 -2
- package/dist/server/implementation/users.js.map +1 -1
- package/dist/server/index.d.ts +157 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +180 -17
- package/dist/server/index.js.map +1 -1
- package/dist/server/oauth/authorizationUrl.d.ts.map +1 -1
- package/dist/server/oauth/authorizationUrl.js +2 -1
- package/dist/server/oauth/authorizationUrl.js.map +1 -1
- package/dist/server/oauth/callback.d.ts.map +1 -1
- package/dist/server/oauth/callback.js +5 -4
- package/dist/server/oauth/callback.js.map +1 -1
- package/dist/server/oauth/checks.d.ts.map +1 -1
- package/dist/server/oauth/checks.js +2 -1
- package/dist/server/oauth/checks.js.map +1 -1
- package/dist/server/oauth/convexAuth.d.ts.map +1 -1
- package/dist/server/oauth/convexAuth.js +3 -2
- package/dist/server/oauth/convexAuth.js.map +1 -1
- package/dist/server/provider_utils.d.ts +2 -0
- package/dist/server/provider_utils.d.ts.map +1 -1
- package/dist/server/types.d.ts +240 -5
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/utils.d.ts.map +1 -1
- package/dist/server/utils.js +2 -1
- package/dist/server/utils.js.map +1 -1
- package/dist/server/version.d.ts +2 -0
- package/dist/server/version.d.ts.map +1 -0
- package/dist/server/version.js +3 -0
- package/dist/server/version.js.map +1 -0
- package/package.json +7 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/utils.ts +248 -0
- package/src/client/index.ts +105 -15
- package/src/component/_generated/component.ts +61 -0
- package/src/component/index.ts +11 -2
- package/src/component/public.ts +142 -0
- package/src/component/schema.ts +52 -0
- package/src/providers/anonymous.ts +3 -0
- package/src/providers/credentials.ts +3 -0
- package/src/providers/email.ts +3 -0
- package/src/providers/passkey.ts +8 -1
- package/src/providers/password.ts +3 -0
- package/src/providers/phone.ts +3 -0
- package/src/providers/totp.ts +9 -0
- package/src/server/convex-auth.ts +385 -73
- package/src/server/email-templates.ts +77 -0
- package/src/server/errors.ts +269 -0
- package/src/server/implementation/apiKey.ts +186 -0
- package/src/server/implementation/index.ts +288 -28
- package/src/server/implementation/mutations/createAccountFromCredentials.ts +2 -1
- package/src/server/implementation/mutations/modifyAccount.ts +2 -3
- package/src/server/implementation/mutations/userOAuth.ts +2 -1
- package/src/server/implementation/mutations/verifierSignature.ts +2 -1
- package/src/server/implementation/passkey.ts +33 -35
- package/src/server/implementation/provider.ts +5 -8
- package/src/server/implementation/redirects.ts +2 -3
- package/src/server/implementation/refreshTokens.ts +2 -1
- package/src/server/implementation/signIn.ts +9 -18
- package/src/server/implementation/totp.ts +18 -21
- package/src/server/implementation/users.ts +4 -7
- package/src/server/index.ts +240 -37
- package/src/server/oauth/authorizationUrl.ts +2 -1
- package/src/server/oauth/callback.ts +5 -4
- package/src/server/oauth/checks.ts +3 -1
- package/src/server/oauth/convexAuth.ts +6 -3
- package/src/server/types.ts +254 -5
- package/src/server/utils.ts +3 -1
- package/src/server/version.ts +2 -0
- package/dist/server/portal.d.ts +0 -116
- package/dist/server/portal.d.ts.map +0 -1
- package/dist/server/portal.js +0 -294
- package/dist/server/portal.js.map +0 -1
- package/src/server/portal.ts +0 -375
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
internalMutationGeneric,
|
|
10
10
|
} from "convex/server";
|
|
11
11
|
import { ConvexError, GenericId, v } from "convex/values";
|
|
12
|
+
import { throwAuthError, isAuthError } from "../errors.js";
|
|
12
13
|
import { parse as parseCookies, serialize as serializeCookie } from "cookie";
|
|
13
14
|
import { redirectToParamCookie, useRedirectToParam } from "../cookies.js";
|
|
14
15
|
import { FunctionReferenceFromExport } from "../convex_types.js";
|
|
@@ -44,6 +45,13 @@ import {
|
|
|
44
45
|
} from "./mutations/index.js";
|
|
45
46
|
import { signInImpl } from "./signIn.js";
|
|
46
47
|
import { redirectAbsoluteUrl, setURLSearchParam } from "./redirects.js";
|
|
48
|
+
import {
|
|
49
|
+
generateApiKey,
|
|
50
|
+
hashApiKey,
|
|
51
|
+
buildScopeChecker,
|
|
52
|
+
validateScopes,
|
|
53
|
+
checkKeyRateLimit,
|
|
54
|
+
} from "./apiKey.js";
|
|
47
55
|
import { getAuthorizationUrl } from "../oauth/authorizationUrl.js";
|
|
48
56
|
import {
|
|
49
57
|
defaultCookiesOptions,
|
|
@@ -106,11 +114,11 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
106
114
|
) => {
|
|
107
115
|
const provider = getProvider(id, allowExtraProviders);
|
|
108
116
|
if (provider === undefined) {
|
|
109
|
-
const
|
|
117
|
+
const detail =
|
|
110
118
|
`Provider \`${id}\` is not configured, ` +
|
|
111
119
|
`available providers are ${listAvailableProviders(config, allowExtraProviders)}.`;
|
|
112
|
-
logWithLevel(LOG_LEVELS.ERROR,
|
|
113
|
-
|
|
120
|
+
logWithLevel(LOG_LEVELS.ERROR, detail);
|
|
121
|
+
throwAuthError("PROVIDER_NOT_CONFIGURED", detail, { provider: id });
|
|
114
122
|
}
|
|
115
123
|
return provider;
|
|
116
124
|
};
|
|
@@ -139,6 +147,9 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
139
147
|
/**
|
|
140
148
|
* Get the current user's ID from the auth context, or `null` if
|
|
141
149
|
* not signed in.
|
|
150
|
+
*
|
|
151
|
+
* @param ctx - Any Convex context with an `auth` field (query, mutation, or action).
|
|
152
|
+
* @returns The user's `Id<"user">`, or `null` when unauthenticated.
|
|
142
153
|
*/
|
|
143
154
|
current: async (ctx: { auth: Auth }) => {
|
|
144
155
|
const identity = await ctx.auth.getUserIdentity();
|
|
@@ -151,24 +162,35 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
151
162
|
/**
|
|
152
163
|
* Get the current user's ID, or throw if not signed in.
|
|
153
164
|
* Use this when authentication is required.
|
|
165
|
+
*
|
|
166
|
+
* @param ctx - Any Convex context with an `auth` field.
|
|
167
|
+
* @returns The user's `Id<"user">`.
|
|
168
|
+
* @throws `ConvexError` with code `NOT_SIGNED_IN` when unauthenticated.
|
|
154
169
|
*/
|
|
155
170
|
require: async (ctx: { auth: Auth }) => {
|
|
156
171
|
const identity = await ctx.auth.getUserIdentity();
|
|
157
172
|
if (identity === null) {
|
|
158
|
-
|
|
173
|
+
throwAuthError("NOT_SIGNED_IN");
|
|
159
174
|
}
|
|
160
175
|
const [userId] = identity.subject.split(TOKEN_SUB_CLAIM_DIVIDER);
|
|
161
176
|
return userId as GenericId<"user">;
|
|
162
177
|
},
|
|
163
178
|
/**
|
|
164
179
|
* Retrieve a user document by their ID.
|
|
180
|
+
*
|
|
181
|
+
* @param ctx - Convex context with `runQuery`.
|
|
182
|
+
* @param userId - The user document ID.
|
|
183
|
+
* @returns The user document, or `null` if not found.
|
|
165
184
|
*/
|
|
166
185
|
get: async (ctx: ComponentReadCtx, userId: string) => {
|
|
167
186
|
return await ctx.runQuery(config.component.public.userGetById, { userId });
|
|
168
187
|
},
|
|
169
188
|
/**
|
|
170
189
|
* Get the currently signed-in user's document, or `null` if not
|
|
171
|
-
* signed in. Convenience
|
|
190
|
+
* signed in. Convenience combining `current()` + `get()`.
|
|
191
|
+
*
|
|
192
|
+
* @param ctx - Convex context with `auth` and `runQuery`.
|
|
193
|
+
* @returns The user document, or `null` when unauthenticated.
|
|
172
194
|
*/
|
|
173
195
|
viewer: async (ctx: ComponentAuthReadCtx) => {
|
|
174
196
|
const userId = await auth.user.current(ctx);
|
|
@@ -177,6 +199,23 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
177
199
|
}
|
|
178
200
|
return await ctx.runQuery(config.component.public.userGetById, { userId });
|
|
179
201
|
},
|
|
202
|
+
/**
|
|
203
|
+
* Update a user document with partial data.
|
|
204
|
+
*
|
|
205
|
+
* @param ctx - Convex context with `runMutation`.
|
|
206
|
+
* @param userId - The user document ID.
|
|
207
|
+
* @param data - Partial data to merge into the user document.
|
|
208
|
+
*/
|
|
209
|
+
patch: async (
|
|
210
|
+
ctx: ComponentCtx,
|
|
211
|
+
userId: string,
|
|
212
|
+
data: Record<string, unknown>,
|
|
213
|
+
) => {
|
|
214
|
+
await ctx.runMutation(config.component.public.userPatch, {
|
|
215
|
+
userId,
|
|
216
|
+
data,
|
|
217
|
+
});
|
|
218
|
+
},
|
|
180
219
|
/**
|
|
181
220
|
* Query a user's group memberships.
|
|
182
221
|
*/
|
|
@@ -208,6 +247,9 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
208
247
|
/**
|
|
209
248
|
* Get the current session ID from the auth context, or `null` if
|
|
210
249
|
* not signed in.
|
|
250
|
+
*
|
|
251
|
+
* @param ctx - Any Convex context with an `auth` field.
|
|
252
|
+
* @returns The session's `Id<"session">`, or `null` when unauthenticated.
|
|
211
253
|
*/
|
|
212
254
|
current: async (ctx: { auth: Auth }) => {
|
|
213
255
|
const identity = await ctx.auth.getUserIdentity();
|
|
@@ -219,6 +261,10 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
219
261
|
},
|
|
220
262
|
/**
|
|
221
263
|
* Invalidate sessions for a user, optionally preserving specific sessions.
|
|
264
|
+
*
|
|
265
|
+
* @param ctx - Convex action context.
|
|
266
|
+
* @param args.userId - The user whose sessions to invalidate.
|
|
267
|
+
* @param args.except - Session IDs to preserve (e.g. the current session).
|
|
222
268
|
*/
|
|
223
269
|
invalidate: async <DataModel extends GenericDataModel>(
|
|
224
270
|
ctx: GenericActionCtx<DataModel>,
|
|
@@ -234,6 +280,10 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
234
280
|
account: {
|
|
235
281
|
/**
|
|
236
282
|
* Create an account and user for a credentials provider.
|
|
283
|
+
*
|
|
284
|
+
* @param ctx - Convex action context.
|
|
285
|
+
* @param args - Provider ID, account credentials, profile data, and link flags.
|
|
286
|
+
* @returns `{ account, user }` — the created account and user documents.
|
|
237
287
|
*/
|
|
238
288
|
create: async <DataModel extends GenericDataModel>(
|
|
239
289
|
ctx: GenericActionCtx<DataModel>,
|
|
@@ -244,6 +294,11 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
244
294
|
},
|
|
245
295
|
/**
|
|
246
296
|
* Retrieve an account and user for a credentials provider.
|
|
297
|
+
*
|
|
298
|
+
* @param ctx - Convex action context.
|
|
299
|
+
* @param args - Provider ID and account credentials (id, optional secret).
|
|
300
|
+
* @returns `{ account, user }` — the matched account and user documents.
|
|
301
|
+
* @throws `ConvexError` with code `ACCOUNT_NOT_FOUND` when no match exists.
|
|
247
302
|
*/
|
|
248
303
|
get: async <DataModel extends GenericDataModel>(
|
|
249
304
|
ctx: GenericActionCtx<DataModel>,
|
|
@@ -252,12 +307,15 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
252
307
|
const actionCtx = ctx as unknown as ActionCtx;
|
|
253
308
|
const result = await callRetreiveAccountWithCredentials(actionCtx, args);
|
|
254
309
|
if (typeof result === "string") {
|
|
255
|
-
|
|
310
|
+
throwAuthError("ACCOUNT_NOT_FOUND", result);
|
|
256
311
|
}
|
|
257
312
|
return result;
|
|
258
313
|
},
|
|
259
314
|
/**
|
|
260
|
-
* Update credentials for an existing account.
|
|
315
|
+
* Update credentials (secret) for an existing account.
|
|
316
|
+
*
|
|
317
|
+
* @param ctx - Convex action context.
|
|
318
|
+
* @param args - Provider ID and new account credentials (id + secret).
|
|
261
319
|
*/
|
|
262
320
|
updateCredentials: async <DataModel extends GenericDataModel>(
|
|
263
321
|
ctx: GenericActionCtx<DataModel>,
|
|
@@ -270,6 +328,11 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
270
328
|
provider: {
|
|
271
329
|
/**
|
|
272
330
|
* Sign in via another provider, typically from a credentials flow.
|
|
331
|
+
*
|
|
332
|
+
* @param ctx - Convex action context.
|
|
333
|
+
* @param provider - The provider config to sign in with.
|
|
334
|
+
* @param args - Optional account ID and params.
|
|
335
|
+
* @returns `{ userId, sessionId }` on success, or `null`.
|
|
273
336
|
*/
|
|
274
337
|
signIn: async <DataModel extends GenericDataModel>(
|
|
275
338
|
ctx: GenericActionCtx<DataModel>,
|
|
@@ -507,15 +570,17 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
507
570
|
* the timestamp. If the invite has a group, the caller is responsible
|
|
508
571
|
* for creating the member record via `auth.group.member.add` in the
|
|
509
572
|
* same Convex mutation for transactional safety.
|
|
510
|
-
*
|
|
511
|
-
* @throws ConvexError with code `INVITE_NOT_FOUND` when the invite does
|
|
512
|
-
* not exist.
|
|
513
|
-
* @throws ConvexError with code `INVITE_NOT_PENDING` when the invite is
|
|
514
|
-
* not in `pending` status.
|
|
515
573
|
*
|
|
574
|
+
* @param ctx - Convex context with `runMutation`.
|
|
575
|
+
* @param inviteId - The invite document ID.
|
|
576
|
+
* @param acceptedByUserId - User accepting the invite (recorded for audit).
|
|
577
|
+
* @throws `ConvexError` with code `INVITE_NOT_FOUND` when the invite does not exist.
|
|
578
|
+
* @throws `ConvexError` with code `INVITE_NOT_PENDING` when the invite is not in `pending` status.
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
516
581
|
* ```ts
|
|
517
|
-
|
|
518
|
-
|
|
582
|
+
* export const acceptInvite = mutation({
|
|
583
|
+
* args: { inviteId: v.string() },
|
|
519
584
|
* handler: async (ctx, { inviteId }) => {
|
|
520
585
|
* const userId = await auth.user.require(ctx);
|
|
521
586
|
* const invite = await auth.invite.get(ctx, inviteId);
|
|
@@ -539,14 +604,14 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
539
604
|
...(acceptedByUserId ? { acceptedByUserId } : {}),
|
|
540
605
|
});
|
|
541
606
|
},
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
607
|
+
/**
|
|
608
|
+
* Revoke a pending invitation.
|
|
609
|
+
*
|
|
610
|
+
* @param ctx - Convex context with `runMutation`.
|
|
611
|
+
* @param inviteId - The invite document ID.
|
|
612
|
+
* @throws `ConvexError` with code `INVITE_NOT_FOUND` when the invite does not exist.
|
|
613
|
+
* @throws `ConvexError` with code `INVITE_NOT_PENDING` when the invite is not in `pending` status.
|
|
614
|
+
*/
|
|
550
615
|
revoke: async (ctx: ComponentCtx, inviteId: string) => {
|
|
551
616
|
await ctx.runMutation(config.component.public.inviteRevoke, { inviteId });
|
|
552
617
|
},
|
|
@@ -631,6 +696,191 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
631
696
|
);
|
|
632
697
|
},
|
|
633
698
|
},
|
|
699
|
+
/**
|
|
700
|
+
* Manage API keys for programmatic access.
|
|
701
|
+
*
|
|
702
|
+
* Keys use SHA-256 hashing (via `@oslojs/crypto`) and support
|
|
703
|
+
* scoped resource:action permissions with optional per-key rate limiting.
|
|
704
|
+
*
|
|
705
|
+
* ```ts
|
|
706
|
+
* const { keyId, raw } = await auth.key.create(ctx, {
|
|
707
|
+
* userId,
|
|
708
|
+
* name: "CI Pipeline",
|
|
709
|
+
* scopes: [{ resource: "users", actions: ["read", "list"] }],
|
|
710
|
+
* });
|
|
711
|
+
* // raw = "sk_live_abc123..." — show once, never stored
|
|
712
|
+
*
|
|
713
|
+
* const result = await auth.key.verify(ctx, rawKey);
|
|
714
|
+
* result.scopes.can("users", "read"); // true
|
|
715
|
+
* ```
|
|
716
|
+
*/
|
|
717
|
+
key: {
|
|
718
|
+
/**
|
|
719
|
+
* Create a new API key. Returns the raw key **once** — it cannot
|
|
720
|
+
* be retrieved again after creation.
|
|
721
|
+
*
|
|
722
|
+
* @param opts.userId - The user this key belongs to.
|
|
723
|
+
* @param opts.name - Human-readable name (e.g. "CI Pipeline").
|
|
724
|
+
* @param opts.scopes - Resource:action permissions for this key.
|
|
725
|
+
* @param opts.rateLimit - Optional per-key rate limit override.
|
|
726
|
+
* @param opts.expiresAt - Optional expiration timestamp.
|
|
727
|
+
* @returns `{ keyId, raw }` where `raw` is the full key string.
|
|
728
|
+
*/
|
|
729
|
+
create: async (
|
|
730
|
+
ctx: ComponentCtx,
|
|
731
|
+
opts: {
|
|
732
|
+
userId: string;
|
|
733
|
+
name: string;
|
|
734
|
+
scopes: import("../types.js").KeyScope[];
|
|
735
|
+
rateLimit?: { maxRequests: number; windowMs: number };
|
|
736
|
+
expiresAt?: number;
|
|
737
|
+
},
|
|
738
|
+
): Promise<{ keyId: string; raw: string }> => {
|
|
739
|
+
const prefix = config.apiKeys?.prefix ?? "sk_live_";
|
|
740
|
+
|
|
741
|
+
// Validate scopes against config if defined
|
|
742
|
+
validateScopes(opts.scopes, config.apiKeys?.scopes);
|
|
743
|
+
|
|
744
|
+
const { raw, hashedKey, displayPrefix } = await generateApiKey(prefix);
|
|
745
|
+
|
|
746
|
+
const keyId = await ctx.runMutation(
|
|
747
|
+
config.component.public.keyInsert,
|
|
748
|
+
{
|
|
749
|
+
userId: opts.userId as any,
|
|
750
|
+
prefix: displayPrefix,
|
|
751
|
+
hashedKey,
|
|
752
|
+
name: opts.name,
|
|
753
|
+
scopes: opts.scopes,
|
|
754
|
+
rateLimit: opts.rateLimit ?? config.apiKeys?.defaultRateLimit,
|
|
755
|
+
expiresAt: opts.expiresAt,
|
|
756
|
+
},
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
return { keyId: keyId as string, raw };
|
|
760
|
+
},
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Verify a raw API key string. Returns the userId and a scope checker
|
|
764
|
+
* if the key is valid, not revoked, not expired, and not rate-limited.
|
|
765
|
+
*
|
|
766
|
+
* Also updates `lastUsedAt` and rate limit state as a side effect.
|
|
767
|
+
*
|
|
768
|
+
* @throws Error if the key is invalid, revoked, expired, or rate-limited.
|
|
769
|
+
*/
|
|
770
|
+
verify: async (
|
|
771
|
+
ctx: ComponentCtx,
|
|
772
|
+
rawKey: string,
|
|
773
|
+
): Promise<{
|
|
774
|
+
userId: string;
|
|
775
|
+
keyId: string;
|
|
776
|
+
scopes: import("../types.js").ScopeChecker;
|
|
777
|
+
}> => {
|
|
778
|
+
const hashedKey = await hashApiKey(rawKey);
|
|
779
|
+
|
|
780
|
+
const key = await ctx.runQuery(
|
|
781
|
+
config.component.public.keyGetByHashedKey,
|
|
782
|
+
{ hashedKey },
|
|
783
|
+
);
|
|
784
|
+
if (!key) {
|
|
785
|
+
throwAuthError("INVALID_API_KEY");
|
|
786
|
+
}
|
|
787
|
+
if (key.revoked) {
|
|
788
|
+
throwAuthError("API_KEY_REVOKED");
|
|
789
|
+
}
|
|
790
|
+
if (key.expiresAt && key.expiresAt < Date.now()) {
|
|
791
|
+
throwAuthError("API_KEY_EXPIRED");
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Check per-key rate limit
|
|
795
|
+
const patchData: Record<string, any> = { lastUsedAt: Date.now() };
|
|
796
|
+
|
|
797
|
+
if (key.rateLimit) {
|
|
798
|
+
const { limited, newState } = checkKeyRateLimit(
|
|
799
|
+
key.rateLimit,
|
|
800
|
+
key.rateLimitState ?? undefined,
|
|
801
|
+
);
|
|
802
|
+
if (limited) {
|
|
803
|
+
throwAuthError("API_KEY_RATE_LIMITED");
|
|
804
|
+
}
|
|
805
|
+
patchData.rateLimitState = newState;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Update lastUsedAt (and rate limit state if applicable)
|
|
809
|
+
await ctx.runMutation(config.component.public.keyPatch, {
|
|
810
|
+
keyId: key._id,
|
|
811
|
+
data: patchData,
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
userId: key.userId as string,
|
|
816
|
+
keyId: key._id as string,
|
|
817
|
+
scopes: buildScopeChecker(key.scopes),
|
|
818
|
+
};
|
|
819
|
+
},
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* List all API keys for a user.
|
|
823
|
+
* Never includes the raw key — only the display prefix.
|
|
824
|
+
*/
|
|
825
|
+
list: async (ctx: ComponentReadCtx, opts: { userId: string }) => {
|
|
826
|
+
return await ctx.runQuery(
|
|
827
|
+
config.component.public.keyListByUserId,
|
|
828
|
+
{ userId: opts.userId as any },
|
|
829
|
+
);
|
|
830
|
+
},
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Get a single API key by its document ID.
|
|
834
|
+
* Returns `null` if not found.
|
|
835
|
+
*/
|
|
836
|
+
get: async (ctx: ComponentReadCtx, keyId: string) => {
|
|
837
|
+
return await ctx.runQuery(
|
|
838
|
+
config.component.public.keyGetById,
|
|
839
|
+
{ keyId: keyId as any },
|
|
840
|
+
);
|
|
841
|
+
},
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Update an API key's metadata (name, scopes, rate limit).
|
|
845
|
+
*/
|
|
846
|
+
update: async (
|
|
847
|
+
ctx: ComponentCtx,
|
|
848
|
+
keyId: string,
|
|
849
|
+
data: {
|
|
850
|
+
name?: string;
|
|
851
|
+
scopes?: import("../types.js").KeyScope[];
|
|
852
|
+
rateLimit?: { maxRequests: number; windowMs: number };
|
|
853
|
+
},
|
|
854
|
+
) => {
|
|
855
|
+
if (data.scopes) {
|
|
856
|
+
validateScopes(data.scopes, config.apiKeys?.scopes);
|
|
857
|
+
}
|
|
858
|
+
await ctx.runMutation(config.component.public.keyPatch, {
|
|
859
|
+
keyId: keyId as any,
|
|
860
|
+
data,
|
|
861
|
+
});
|
|
862
|
+
},
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Revoke an API key (soft delete). The key record is preserved
|
|
866
|
+
* for audit purposes but can no longer be used for authentication.
|
|
867
|
+
*/
|
|
868
|
+
revoke: async (ctx: ComponentCtx, keyId: string) => {
|
|
869
|
+
await ctx.runMutation(config.component.public.keyPatch, {
|
|
870
|
+
keyId: keyId as any,
|
|
871
|
+
data: { revoked: true },
|
|
872
|
+
});
|
|
873
|
+
},
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Hard delete an API key record.
|
|
877
|
+
*/
|
|
878
|
+
remove: async (ctx: ComponentCtx, keyId: string) => {
|
|
879
|
+
await ctx.runMutation(config.component.public.keyDelete, {
|
|
880
|
+
keyId: keyId as any,
|
|
881
|
+
});
|
|
882
|
+
},
|
|
883
|
+
},
|
|
634
884
|
/**
|
|
635
885
|
* Add HTTP actions for JWT verification and OAuth sign-in.
|
|
636
886
|
*
|
|
@@ -707,11 +957,11 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
707
957
|
const pathParts = url.pathname.split("/");
|
|
708
958
|
const providerId = pathParts.at(-1)!;
|
|
709
959
|
if (providerId === null) {
|
|
710
|
-
|
|
960
|
+
throwAuthError("OAUTH_MISSING_PROVIDER");
|
|
711
961
|
}
|
|
712
962
|
const verifier = url.searchParams.get("code");
|
|
713
963
|
if (verifier === null) {
|
|
714
|
-
|
|
964
|
+
throwAuthError("OAUTH_MISSING_VERIFIER");
|
|
715
965
|
}
|
|
716
966
|
const provider = getProviderOrThrow(
|
|
717
967
|
providerId,
|
|
@@ -800,8 +1050,10 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
800
1050
|
);
|
|
801
1051
|
|
|
802
1052
|
if (typeof id !== "string") {
|
|
803
|
-
|
|
1053
|
+
throwAuthError(
|
|
1054
|
+
"OAUTH_INVALID_PROFILE",
|
|
804
1055
|
`The profile method of the ${providerId} config must return a string ID`,
|
|
1056
|
+
{ provider: providerId },
|
|
805
1057
|
);
|
|
806
1058
|
}
|
|
807
1059
|
|
|
@@ -914,7 +1166,7 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
914
1166
|
return { totpSetup: { uri: result.uri, secret: result.secret, totpId: result.totpId }, verifier: result.verifier };
|
|
915
1167
|
default: {
|
|
916
1168
|
const _typecheck: never = result;
|
|
917
|
-
|
|
1169
|
+
throwAuthError("INTERNAL_ERROR", `Unexpected result from signIn, ${result as any}`);
|
|
918
1170
|
}
|
|
919
1171
|
}
|
|
920
1172
|
},
|
|
@@ -951,10 +1203,18 @@ function convertErrorsToResponse(
|
|
|
951
1203
|
try {
|
|
952
1204
|
return await action(ctx, request);
|
|
953
1205
|
} catch (error) {
|
|
954
|
-
if (error
|
|
1206
|
+
if (isAuthError(error)) {
|
|
1207
|
+
return new Response(
|
|
1208
|
+
JSON.stringify({ code: error.data.code, message: error.data.message }),
|
|
1209
|
+
{
|
|
1210
|
+
status: errorStatusCode,
|
|
1211
|
+
headers: { "Content-Type": "application/json" },
|
|
1212
|
+
},
|
|
1213
|
+
);
|
|
1214
|
+
} else if (error instanceof ConvexError) {
|
|
955
1215
|
return new Response(null, {
|
|
956
1216
|
status: errorStatusCode,
|
|
957
|
-
statusText: error.data,
|
|
1217
|
+
statusText: typeof error.data === "string" ? error.data : "Error",
|
|
958
1218
|
});
|
|
959
1219
|
} else {
|
|
960
1220
|
logError(error);
|
|
@@ -7,6 +7,7 @@ import { getAuthSessionId } from "../sessions.js";
|
|
|
7
7
|
import { LOG_LEVELS, logWithLevel, maybeRedact } from "../utils.js";
|
|
8
8
|
import { authDb } from "../db.js";
|
|
9
9
|
import { AUTH_STORE_REF } from "./storeRef.js";
|
|
10
|
+
import { throwAuthError } from "../../errors.js";
|
|
10
11
|
|
|
11
12
|
export const createAccountFromCredentialsArgs = v.object({
|
|
12
13
|
provider: v.string(),
|
|
@@ -53,7 +54,7 @@ export async function createAccountFromCredentialsImpl(
|
|
|
53
54
|
existingAccount.secret ?? "",
|
|
54
55
|
))
|
|
55
56
|
) {
|
|
56
|
-
|
|
57
|
+
throwAuthError("ACCOUNT_ALREADY_EXISTS", `Account ${account.id} already exists`);
|
|
57
58
|
}
|
|
58
59
|
return {
|
|
59
60
|
account: existingAccount,
|
|
@@ -5,6 +5,7 @@ import { LOG_LEVELS, logWithLevel, maybeRedact } from "../utils.js";
|
|
|
5
5
|
import * as Provider from "../provider.js";
|
|
6
6
|
import { authDb } from "../db.js";
|
|
7
7
|
import { AUTH_STORE_REF } from "./storeRef.js";
|
|
8
|
+
import { throwAuthError } from "../../errors.js";
|
|
8
9
|
|
|
9
10
|
export const modifyAccountArgs = v.object({
|
|
10
11
|
provider: v.string(),
|
|
@@ -28,9 +29,7 @@ export async function modifyAccountImpl(
|
|
|
28
29
|
});
|
|
29
30
|
const existingAccount = await db.accounts.get(provider, account.id);
|
|
30
31
|
if (existingAccount === null) {
|
|
31
|
-
|
|
32
|
-
`Cannot modify account with ID ${account.id} because it does not exist`,
|
|
33
|
-
);
|
|
32
|
+
throwAuthError("ACCOUNT_NOT_FOUND", `Cannot modify account with ID ${account.id} because it does not exist`);
|
|
34
33
|
}
|
|
35
34
|
await db.accounts.patch(existingAccount._id, {
|
|
36
35
|
secret: await hash(getProviderOrThrow(provider), account.secret),
|
|
@@ -6,6 +6,7 @@ import { upsertUserAndAccount } from "../users.js";
|
|
|
6
6
|
import { generateRandomString, logWithLevel, sha256 } from "../utils.js";
|
|
7
7
|
import { authDb } from "../db.js";
|
|
8
8
|
import { AUTH_STORE_REF } from "./storeRef.js";
|
|
9
|
+
import { throwAuthError } from "../../errors.js";
|
|
9
10
|
|
|
10
11
|
const OAUTH_SIGN_IN_EXPIRATION_MS = 1000 * 60 * 2; // 2 minutes
|
|
11
12
|
|
|
@@ -32,7 +33,7 @@ export async function userOAuthImpl(
|
|
|
32
33
|
|
|
33
34
|
const verifier = await db.verifiers.getBySignature(signature);
|
|
34
35
|
if (verifier === null) {
|
|
35
|
-
|
|
36
|
+
throwAuthError("OAUTH_INVALID_STATE");
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
const { accountId } = await upsertUserAndAccount(
|
|
@@ -3,6 +3,7 @@ import { ActionCtx, MutationCtx } from "../types.js";
|
|
|
3
3
|
import * as Provider from "../provider.js";
|
|
4
4
|
import { authDb } from "../db.js";
|
|
5
5
|
import { AUTH_STORE_REF } from "./storeRef.js";
|
|
6
|
+
import { throwAuthError } from "../../errors.js";
|
|
6
7
|
|
|
7
8
|
export const verifierSignatureArgs = v.object({
|
|
8
9
|
verifier: v.string(),
|
|
@@ -20,7 +21,7 @@ export async function verifierSignatureImpl(
|
|
|
20
21
|
const db = authDb(ctx, config);
|
|
21
22
|
const verifierDoc = await db.verifiers.getById(verifier as GenericId<"verifier">);
|
|
22
23
|
if (verifierDoc === null) {
|
|
23
|
-
|
|
24
|
+
throwAuthError("INVALID_VERIFIER");
|
|
24
25
|
}
|
|
25
26
|
return await db.verifiers.patch(verifierDoc._id, { signature });
|
|
26
27
|
}
|