@robelest/convex-auth 0.0.2 → 0.0.3-preview
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.map +1 -1
- package/dist/client/index.js +10 -1
- 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 +1 -2
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/index.js +0 -1
- package/dist/component/index.js.map +1 -1
- package/dist/component/public.d.ts +160 -0
- 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 +79 -0
- 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/server/convex-auth.d.ts +66 -13
- package/dist/server/convex-auth.d.ts.map +1 -1
- package/dist/server/convex-auth.js +154 -39
- 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/implementation/apiKey.d.ts +74 -0
- package/dist/server/implementation/apiKey.d.ts.map +1 -0
- package/dist/server/implementation/apiKey.js +140 -0
- package/dist/server/implementation/apiKey.js.map +1 -0
- package/dist/server/implementation/index.d.ts +89 -0
- package/dist/server/implementation/index.d.ts.map +1 -1
- package/dist/server/implementation/index.js +132 -0
- package/dist/server/implementation/index.js.map +1 -1
- package/dist/server/implementation/signIn.js +3 -14
- package/dist/server/implementation/signIn.js.map +1 -1
- package/dist/server/index.d.ts +26 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +63 -16
- package/dist/server/index.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 +205 -2
- package/dist/server/types.d.ts.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 +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/utils.ts +248 -0
- package/src/client/index.ts +12 -1
- package/src/component/_generated/component.ts +61 -0
- package/src/component/index.ts +4 -1
- package/src/component/public.ts +142 -0
- package/src/component/schema.ts +52 -0
- package/src/server/convex-auth.ts +188 -56
- package/src/server/email-templates.ts +77 -0
- package/src/server/implementation/apiKey.ts +185 -0
- package/src/server/implementation/index.ts +192 -0
- package/src/server/implementation/signIn.ts +2 -12
- package/src/server/index.ts +98 -34
- package/src/server/types.ts +219 -2
- 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
|
@@ -44,6 +44,13 @@ import {
|
|
|
44
44
|
} from "./mutations/index.js";
|
|
45
45
|
import { signInImpl } from "./signIn.js";
|
|
46
46
|
import { redirectAbsoluteUrl, setURLSearchParam } from "./redirects.js";
|
|
47
|
+
import {
|
|
48
|
+
generateApiKey,
|
|
49
|
+
hashApiKey,
|
|
50
|
+
buildScopeChecker,
|
|
51
|
+
validateScopes,
|
|
52
|
+
checkKeyRateLimit,
|
|
53
|
+
} from "./apiKey.js";
|
|
47
54
|
import { getAuthorizationUrl } from "../oauth/authorizationUrl.js";
|
|
48
55
|
import {
|
|
49
56
|
defaultCookiesOptions,
|
|
@@ -631,6 +638,191 @@ export function Auth(config_: ConvexAuthConfig) {
|
|
|
631
638
|
);
|
|
632
639
|
},
|
|
633
640
|
},
|
|
641
|
+
/**
|
|
642
|
+
* Manage API keys for programmatic access.
|
|
643
|
+
*
|
|
644
|
+
* Keys use SHA-256 hashing (via `@oslojs/crypto`) and support
|
|
645
|
+
* scoped resource:action permissions with optional per-key rate limiting.
|
|
646
|
+
*
|
|
647
|
+
* ```ts
|
|
648
|
+
* const { keyId, raw } = await auth.key.create(ctx, {
|
|
649
|
+
* userId,
|
|
650
|
+
* name: "CI Pipeline",
|
|
651
|
+
* scopes: [{ resource: "users", actions: ["read", "list"] }],
|
|
652
|
+
* });
|
|
653
|
+
* // raw = "sk_live_abc123..." — show once, never stored
|
|
654
|
+
*
|
|
655
|
+
* const result = await auth.key.verify(ctx, rawKey);
|
|
656
|
+
* result.scopes.can("users", "read"); // true
|
|
657
|
+
* ```
|
|
658
|
+
*/
|
|
659
|
+
key: {
|
|
660
|
+
/**
|
|
661
|
+
* Create a new API key. Returns the raw key **once** — it cannot
|
|
662
|
+
* be retrieved again after creation.
|
|
663
|
+
*
|
|
664
|
+
* @param opts.userId - The user this key belongs to.
|
|
665
|
+
* @param opts.name - Human-readable name (e.g. "CI Pipeline").
|
|
666
|
+
* @param opts.scopes - Resource:action permissions for this key.
|
|
667
|
+
* @param opts.rateLimit - Optional per-key rate limit override.
|
|
668
|
+
* @param opts.expiresAt - Optional expiration timestamp.
|
|
669
|
+
* @returns `{ keyId, raw }` where `raw` is the full key string.
|
|
670
|
+
*/
|
|
671
|
+
create: async (
|
|
672
|
+
ctx: ComponentCtx,
|
|
673
|
+
opts: {
|
|
674
|
+
userId: string;
|
|
675
|
+
name: string;
|
|
676
|
+
scopes: import("../types.js").KeyScope[];
|
|
677
|
+
rateLimit?: { maxRequests: number; windowMs: number };
|
|
678
|
+
expiresAt?: number;
|
|
679
|
+
},
|
|
680
|
+
): Promise<{ keyId: string; raw: string }> => {
|
|
681
|
+
const prefix = config.apiKeys?.prefix ?? "sk_live_";
|
|
682
|
+
|
|
683
|
+
// Validate scopes against config if defined
|
|
684
|
+
validateScopes(opts.scopes, config.apiKeys?.scopes);
|
|
685
|
+
|
|
686
|
+
const { raw, hashedKey, displayPrefix } = await generateApiKey(prefix);
|
|
687
|
+
|
|
688
|
+
const keyId = await ctx.runMutation(
|
|
689
|
+
config.component.public.keyInsert,
|
|
690
|
+
{
|
|
691
|
+
userId: opts.userId as any,
|
|
692
|
+
prefix: displayPrefix,
|
|
693
|
+
hashedKey,
|
|
694
|
+
name: opts.name,
|
|
695
|
+
scopes: opts.scopes,
|
|
696
|
+
rateLimit: opts.rateLimit ?? config.apiKeys?.defaultRateLimit,
|
|
697
|
+
expiresAt: opts.expiresAt,
|
|
698
|
+
},
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
return { keyId: keyId as string, raw };
|
|
702
|
+
},
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Verify a raw API key string. Returns the userId and a scope checker
|
|
706
|
+
* if the key is valid, not revoked, not expired, and not rate-limited.
|
|
707
|
+
*
|
|
708
|
+
* Also updates `lastUsedAt` and rate limit state as a side effect.
|
|
709
|
+
*
|
|
710
|
+
* @throws Error if the key is invalid, revoked, expired, or rate-limited.
|
|
711
|
+
*/
|
|
712
|
+
verify: async (
|
|
713
|
+
ctx: ComponentCtx,
|
|
714
|
+
rawKey: string,
|
|
715
|
+
): Promise<{
|
|
716
|
+
userId: string;
|
|
717
|
+
keyId: string;
|
|
718
|
+
scopes: import("../types.js").ScopeChecker;
|
|
719
|
+
}> => {
|
|
720
|
+
const hashedKey = await hashApiKey(rawKey);
|
|
721
|
+
|
|
722
|
+
const key = await ctx.runQuery(
|
|
723
|
+
config.component.public.keyGetByHashedKey,
|
|
724
|
+
{ hashedKey },
|
|
725
|
+
);
|
|
726
|
+
if (!key) {
|
|
727
|
+
throw new Error("Invalid API key");
|
|
728
|
+
}
|
|
729
|
+
if (key.revoked) {
|
|
730
|
+
throw new Error("API key has been revoked");
|
|
731
|
+
}
|
|
732
|
+
if (key.expiresAt && key.expiresAt < Date.now()) {
|
|
733
|
+
throw new Error("API key has expired");
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Check per-key rate limit
|
|
737
|
+
const patchData: Record<string, any> = { lastUsedAt: Date.now() };
|
|
738
|
+
|
|
739
|
+
if (key.rateLimit) {
|
|
740
|
+
const { limited, newState } = checkKeyRateLimit(
|
|
741
|
+
key.rateLimit,
|
|
742
|
+
key.rateLimitState ?? undefined,
|
|
743
|
+
);
|
|
744
|
+
if (limited) {
|
|
745
|
+
throw new Error("API key rate limit exceeded");
|
|
746
|
+
}
|
|
747
|
+
patchData.rateLimitState = newState;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Update lastUsedAt (and rate limit state if applicable)
|
|
751
|
+
await ctx.runMutation(config.component.public.keyPatch, {
|
|
752
|
+
keyId: key._id,
|
|
753
|
+
data: patchData,
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
userId: key.userId as string,
|
|
758
|
+
keyId: key._id as string,
|
|
759
|
+
scopes: buildScopeChecker(key.scopes),
|
|
760
|
+
};
|
|
761
|
+
},
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* List all API keys for a user.
|
|
765
|
+
* Never includes the raw key — only the display prefix.
|
|
766
|
+
*/
|
|
767
|
+
list: async (ctx: ComponentReadCtx, opts: { userId: string }) => {
|
|
768
|
+
return await ctx.runQuery(
|
|
769
|
+
config.component.public.keyListByUserId,
|
|
770
|
+
{ userId: opts.userId as any },
|
|
771
|
+
);
|
|
772
|
+
},
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Get a single API key by its document ID.
|
|
776
|
+
* Returns `null` if not found.
|
|
777
|
+
*/
|
|
778
|
+
get: async (ctx: ComponentReadCtx, keyId: string) => {
|
|
779
|
+
return await ctx.runQuery(
|
|
780
|
+
config.component.public.keyGetById,
|
|
781
|
+
{ keyId: keyId as any },
|
|
782
|
+
);
|
|
783
|
+
},
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Update an API key's metadata (name, scopes, rate limit).
|
|
787
|
+
*/
|
|
788
|
+
update: async (
|
|
789
|
+
ctx: ComponentCtx,
|
|
790
|
+
keyId: string,
|
|
791
|
+
data: {
|
|
792
|
+
name?: string;
|
|
793
|
+
scopes?: import("../types.js").KeyScope[];
|
|
794
|
+
rateLimit?: { maxRequests: number; windowMs: number };
|
|
795
|
+
},
|
|
796
|
+
) => {
|
|
797
|
+
if (data.scopes) {
|
|
798
|
+
validateScopes(data.scopes, config.apiKeys?.scopes);
|
|
799
|
+
}
|
|
800
|
+
await ctx.runMutation(config.component.public.keyPatch, {
|
|
801
|
+
keyId: keyId as any,
|
|
802
|
+
data,
|
|
803
|
+
});
|
|
804
|
+
},
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Revoke an API key (soft delete). The key record is preserved
|
|
808
|
+
* for audit purposes but can no longer be used for authentication.
|
|
809
|
+
*/
|
|
810
|
+
revoke: async (ctx: ComponentCtx, keyId: string) => {
|
|
811
|
+
await ctx.runMutation(config.component.public.keyPatch, {
|
|
812
|
+
keyId: keyId as any,
|
|
813
|
+
data: { revoked: true },
|
|
814
|
+
});
|
|
815
|
+
},
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Hard delete an API key record.
|
|
819
|
+
*/
|
|
820
|
+
remove: async (ctx: ComponentCtx, keyId: string) => {
|
|
821
|
+
await ctx.runMutation(config.component.public.keyDelete, {
|
|
822
|
+
keyId: keyId as any,
|
|
823
|
+
});
|
|
824
|
+
},
|
|
825
|
+
},
|
|
634
826
|
/**
|
|
635
827
|
* Add HTTP actions for JWT verification and OAuth sign-in.
|
|
636
828
|
*
|
|
@@ -170,20 +170,10 @@ async function handleEmailAndPhoneProvider(
|
|
|
170
170
|
await provider.sendVerificationRequest(
|
|
171
171
|
{
|
|
172
172
|
...verificationArgs,
|
|
173
|
-
provider
|
|
174
|
-
|
|
175
|
-
from:
|
|
176
|
-
// Simplifies demo configuration of Resend
|
|
177
|
-
provider.from === "Auth.js <no-reply@authjs.dev>" &&
|
|
178
|
-
provider.id === "resend"
|
|
179
|
-
? "My App <onboarding@resend.dev>"
|
|
180
|
-
: provider.from,
|
|
181
|
-
},
|
|
182
|
-
request: new Request("http://localhost"), // TODO: Document
|
|
173
|
+
provider,
|
|
174
|
+
request: new Request("http://localhost"),
|
|
183
175
|
theme: ctx.auth.config.theme,
|
|
184
176
|
},
|
|
185
|
-
// @ts-expect-error Figure out typing for email providers so they can
|
|
186
|
-
// access ctx.
|
|
187
177
|
ctx,
|
|
188
178
|
);
|
|
189
179
|
} else if (provider.type === "phone") {
|
package/src/server/index.ts
CHANGED
|
@@ -17,6 +17,20 @@ export type AuthCookies = {
|
|
|
17
17
|
verifier: string | null;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
/** A structured cookie ready to be set via any framework's cookie API. */
|
|
21
|
+
export type AuthCookie = {
|
|
22
|
+
name: string;
|
|
23
|
+
value: string;
|
|
24
|
+
options: {
|
|
25
|
+
path: string;
|
|
26
|
+
httpOnly: boolean;
|
|
27
|
+
secure: boolean;
|
|
28
|
+
sameSite: "lax" | "strict" | "none";
|
|
29
|
+
maxAge?: number;
|
|
30
|
+
expires?: Date;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
20
34
|
export type ServerOptions = {
|
|
21
35
|
/** Convex deployment URL. */
|
|
22
36
|
url: string;
|
|
@@ -27,8 +41,12 @@ export type ServerOptions = {
|
|
|
27
41
|
};
|
|
28
42
|
|
|
29
43
|
export type RefreshResult = {
|
|
30
|
-
response
|
|
31
|
-
cookies
|
|
44
|
+
/** Structured cookies to set on the response. */
|
|
45
|
+
cookies: AuthCookie[];
|
|
46
|
+
/** URL to redirect to (set after OAuth code exchange). */
|
|
47
|
+
redirect?: string;
|
|
48
|
+
/** JWT for SSR hydration, or `null` if not authenticated. */
|
|
49
|
+
token: string | null;
|
|
32
50
|
};
|
|
33
51
|
|
|
34
52
|
export function authCookieNames(host?: string) {
|
|
@@ -86,6 +104,57 @@ export function serializeAuthCookies(
|
|
|
86
104
|
];
|
|
87
105
|
}
|
|
88
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Build structured cookie objects for any SSR framework.
|
|
109
|
+
*
|
|
110
|
+
* Use with SvelteKit's `event.cookies.set()`, TanStack Start's `setCookie()`,
|
|
111
|
+
* Next.js's `cookies().set()`, or any other framework cookie API.
|
|
112
|
+
*/
|
|
113
|
+
export function structuredAuthCookies(
|
|
114
|
+
cookies: AuthCookies,
|
|
115
|
+
host?: string,
|
|
116
|
+
config: AuthCookieConfig = { maxAge: null },
|
|
117
|
+
): AuthCookie[] {
|
|
118
|
+
const names = authCookieNames(host);
|
|
119
|
+
const secure = !isLocalHost(host);
|
|
120
|
+
const base = {
|
|
121
|
+
path: "/" as const,
|
|
122
|
+
httpOnly: true as const,
|
|
123
|
+
secure,
|
|
124
|
+
sameSite: "lax" as const,
|
|
125
|
+
};
|
|
126
|
+
const maxAge = config.maxAge ?? undefined;
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
name: names.token,
|
|
130
|
+
value: cookies.token ?? "",
|
|
131
|
+
options: {
|
|
132
|
+
...base,
|
|
133
|
+
maxAge: cookies.token === null ? 0 : maxAge,
|
|
134
|
+
expires: cookies.token === null ? new Date(0) : undefined,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: names.refreshToken,
|
|
139
|
+
value: cookies.refreshToken ?? "",
|
|
140
|
+
options: {
|
|
141
|
+
...base,
|
|
142
|
+
maxAge: cookies.refreshToken === null ? 0 : maxAge,
|
|
143
|
+
expires: cookies.refreshToken === null ? new Date(0) : undefined,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: names.verifier,
|
|
148
|
+
value: cookies.verifier ?? "",
|
|
149
|
+
options: {
|
|
150
|
+
...base,
|
|
151
|
+
maxAge: cookies.verifier === null ? 0 : maxAge,
|
|
152
|
+
expires: cookies.verifier === null ? new Date(0) : undefined,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
|
|
89
158
|
export function shouldProxyAuthAction(pathname: string, apiRoute: string) {
|
|
90
159
|
if (apiRoute.endsWith("/")) {
|
|
91
160
|
return pathname === apiRoute || pathname === apiRoute.slice(0, -1);
|
|
@@ -348,21 +417,21 @@ export function server(options: ServerOptions) {
|
|
|
348
417
|
|
|
349
418
|
async refresh(request: Request): Promise<RefreshResult> {
|
|
350
419
|
const host = cookieHost(request);
|
|
420
|
+
const currentToken = parseRequestCookies(request).token;
|
|
351
421
|
|
|
422
|
+
// CORS request — clear all auth cookies.
|
|
352
423
|
if (isCorsRequest(request)) {
|
|
353
424
|
return {
|
|
354
|
-
cookies:
|
|
355
|
-
{
|
|
356
|
-
token: null,
|
|
357
|
-
refreshToken: null,
|
|
358
|
-
verifier: null,
|
|
359
|
-
},
|
|
425
|
+
cookies: structuredAuthCookies(
|
|
426
|
+
{ token: null, refreshToken: null, verifier: null },
|
|
360
427
|
host,
|
|
361
428
|
cookieConfig,
|
|
362
429
|
),
|
|
430
|
+
token: null,
|
|
363
431
|
};
|
|
364
432
|
}
|
|
365
433
|
|
|
434
|
+
// OAuth code exchange — exchange code for tokens and redirect.
|
|
366
435
|
const requestUrl = new URL(request.url);
|
|
367
436
|
const code = requestUrl.searchParams.get("code");
|
|
368
437
|
const shouldHandleCode =
|
|
@@ -392,47 +461,41 @@ export function server(options: ServerOptions) {
|
|
|
392
461
|
if (result.tokens === undefined) {
|
|
393
462
|
throw new Error("Invalid `auth:signIn` result for code exchange");
|
|
394
463
|
}
|
|
395
|
-
const response = Response.redirect(redirectUrl.toString(), 302);
|
|
396
464
|
return {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
host,
|
|
406
|
-
cookieConfig,
|
|
407
|
-
),
|
|
465
|
+
cookies: structuredAuthCookies(
|
|
466
|
+
{
|
|
467
|
+
token: result.tokens?.token ?? null,
|
|
468
|
+
refreshToken: result.tokens?.refreshToken ?? null,
|
|
469
|
+
verifier: null,
|
|
470
|
+
},
|
|
471
|
+
host,
|
|
472
|
+
cookieConfig,
|
|
408
473
|
),
|
|
474
|
+
redirect: redirectUrl.toString(),
|
|
475
|
+
token: result.tokens?.token ?? null,
|
|
409
476
|
};
|
|
410
477
|
} catch (error) {
|
|
411
478
|
console.error(error);
|
|
412
|
-
const response = Response.redirect(redirectUrl.toString(), 302);
|
|
413
479
|
return {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
token: null,
|
|
419
|
-
refreshToken: null,
|
|
420
|
-
verifier: null,
|
|
421
|
-
},
|
|
422
|
-
host,
|
|
423
|
-
cookieConfig,
|
|
424
|
-
),
|
|
480
|
+
cookies: structuredAuthCookies(
|
|
481
|
+
{ token: null, refreshToken: null, verifier: null },
|
|
482
|
+
host,
|
|
483
|
+
cookieConfig,
|
|
425
484
|
),
|
|
485
|
+
redirect: redirectUrl.toString(),
|
|
486
|
+
token: null,
|
|
426
487
|
};
|
|
427
488
|
}
|
|
428
489
|
}
|
|
429
490
|
|
|
491
|
+
// Normal page load — refresh tokens if needed.
|
|
430
492
|
const tokens = await refreshTokens(request);
|
|
431
493
|
if (tokens === undefined) {
|
|
432
|
-
return
|
|
494
|
+
// No refresh needed — return current token for hydration.
|
|
495
|
+
return { cookies: [], token: currentToken };
|
|
433
496
|
}
|
|
434
497
|
return {
|
|
435
|
-
cookies:
|
|
498
|
+
cookies: structuredAuthCookies(
|
|
436
499
|
{
|
|
437
500
|
token: tokens?.token ?? null,
|
|
438
501
|
refreshToken: tokens?.refreshToken ?? null,
|
|
@@ -441,6 +504,7 @@ export function server(options: ServerOptions) {
|
|
|
441
504
|
host,
|
|
442
505
|
cookieConfig,
|
|
443
506
|
),
|
|
507
|
+
token: tokens?.token ?? null,
|
|
444
508
|
};
|
|
445
509
|
},
|
|
446
510
|
};
|
package/src/server/types.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
OAuth2Config,
|
|
6
6
|
OIDCConfig,
|
|
7
7
|
} from "@auth/core/providers";
|
|
8
|
-
import { Theme } from "@auth/core/types";
|
|
8
|
+
import { Awaitable, Theme } from "@auth/core/types";
|
|
9
9
|
import {
|
|
10
10
|
AnyDataModel,
|
|
11
11
|
FunctionReference,
|
|
@@ -82,6 +82,60 @@ export type ConvexAuthConfig = {
|
|
|
82
82
|
*/
|
|
83
83
|
maxFailedAttempsPerHour?: number;
|
|
84
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* API key configuration for programmatic access.
|
|
87
|
+
*
|
|
88
|
+
* Enables `auth.key.*` helpers for creating, verifying, and managing
|
|
89
|
+
* API keys with scoped permissions and optional per-key rate limiting.
|
|
90
|
+
*/
|
|
91
|
+
apiKeys?: ApiKeyConfig;
|
|
92
|
+
/**
|
|
93
|
+
* Email transport configuration.
|
|
94
|
+
*
|
|
95
|
+
* Required for magic link authentication and the admin portal.
|
|
96
|
+
* The library generates email content (subject, styled HTML); you
|
|
97
|
+
* provide the delivery mechanism — Resend, SendGrid, SES, Postmark,
|
|
98
|
+
* or any other provider.
|
|
99
|
+
*
|
|
100
|
+
* When configured, a magic link email provider (`id: "email"`) is
|
|
101
|
+
* auto-registered — no need to add a separate Auth.js email provider
|
|
102
|
+
* to `providers`.
|
|
103
|
+
*
|
|
104
|
+
* Works seamlessly with the `@convex-dev/resend` Convex component:
|
|
105
|
+
*
|
|
106
|
+
* ```ts
|
|
107
|
+
* import { Resend } from "@convex-dev/resend";
|
|
108
|
+
*
|
|
109
|
+
* const resend = new Resend(components.resend, { testMode: false });
|
|
110
|
+
*
|
|
111
|
+
* const auth = new Auth(components.auth, {
|
|
112
|
+
* providers: [google],
|
|
113
|
+
* email: {
|
|
114
|
+
* from: "My App <noreply@example.com>",
|
|
115
|
+
* send: (ctx, params) => resend.sendEmail(ctx, params),
|
|
116
|
+
* },
|
|
117
|
+
* });
|
|
118
|
+
* ```
|
|
119
|
+
*
|
|
120
|
+
* Or with any email API directly:
|
|
121
|
+
*
|
|
122
|
+
* ```ts
|
|
123
|
+
* email: {
|
|
124
|
+
* from: "My App <noreply@example.com>",
|
|
125
|
+
* send: async (_ctx, { from, to, subject, html }) => {
|
|
126
|
+
* await fetch("https://api.resend.com/emails", {
|
|
127
|
+
* method: "POST",
|
|
128
|
+
* headers: {
|
|
129
|
+
* Authorization: `Bearer ${process.env.AUTH_RESEND_KEY}`,
|
|
130
|
+
* "Content-Type": "application/json",
|
|
131
|
+
* },
|
|
132
|
+
* body: JSON.stringify({ from, to, subject, html }),
|
|
133
|
+
* });
|
|
134
|
+
* },
|
|
135
|
+
* },
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
email?: EmailTransport;
|
|
85
139
|
callbacks?: {
|
|
86
140
|
/**
|
|
87
141
|
* Control which URLs are allowed as a destination after OAuth sign-in
|
|
@@ -258,11 +312,30 @@ export type AuthProviderConfig =
|
|
|
258
312
|
export interface EmailConfig<
|
|
259
313
|
DataModel extends GenericDataModel = GenericDataModel,
|
|
260
314
|
> extends AuthjsEmailConfig {
|
|
315
|
+
/**
|
|
316
|
+
* Send the verification token to the user.
|
|
317
|
+
*
|
|
318
|
+
* Overrides the Auth.js 1-arg signature to accept an optional
|
|
319
|
+
* Convex action context as the second argument. Library-native
|
|
320
|
+
* email providers use `ctx` to call `email.send(ctx, params)`.
|
|
321
|
+
*/
|
|
322
|
+
sendVerificationRequest: (
|
|
323
|
+
params: {
|
|
324
|
+
identifier: string;
|
|
325
|
+
url: string;
|
|
326
|
+
expires: Date;
|
|
327
|
+
provider: AuthjsEmailConfig;
|
|
328
|
+
token: string;
|
|
329
|
+
theme: Theme;
|
|
330
|
+
request: Request;
|
|
331
|
+
},
|
|
332
|
+
ctx?: GenericActionCtx<AnyDataModel>,
|
|
333
|
+
) => Awaitable<void>;
|
|
261
334
|
/**
|
|
262
335
|
* Before the token is verified, check other
|
|
263
336
|
* provided parameters.
|
|
264
337
|
*
|
|
265
|
-
* Used to make sure
|
|
338
|
+
* Used to make sure that OTPs are accompanied
|
|
266
339
|
* with the correct email address.
|
|
267
340
|
*/
|
|
268
341
|
authorize?: (
|
|
@@ -524,6 +597,143 @@ export type AuthProviderMaterializedConfig =
|
|
|
524
597
|
| PasskeyProviderConfig
|
|
525
598
|
| TotpProviderConfig;
|
|
526
599
|
|
|
600
|
+
// ============================================================================
|
|
601
|
+
// Email transport types
|
|
602
|
+
// ============================================================================
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Email delivery parameters passed to `EmailTransport.send`.
|
|
606
|
+
*/
|
|
607
|
+
export interface EmailMessage {
|
|
608
|
+
/** Sender address (from `email.from` in your Auth config). */
|
|
609
|
+
from: string;
|
|
610
|
+
/** Recipient email address. */
|
|
611
|
+
to: string;
|
|
612
|
+
/** Email subject line. */
|
|
613
|
+
subject: string;
|
|
614
|
+
/** HTML body content. */
|
|
615
|
+
html: string;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Email transport configuration for the Auth library.
|
|
620
|
+
*
|
|
621
|
+
* Provides a delivery mechanism for library-generated emails
|
|
622
|
+
* (magic links, portal admin sign-in). The library owns the
|
|
623
|
+
* email content; you provide the transport.
|
|
624
|
+
*/
|
|
625
|
+
export interface EmailTransport {
|
|
626
|
+
/** Sender address shown in the From field (e.g. "My App \<noreply@example.com\>"). */
|
|
627
|
+
from: string;
|
|
628
|
+
/**
|
|
629
|
+
* Deliver an email. Called by the library for magic links and portal emails.
|
|
630
|
+
*
|
|
631
|
+
* Receives the Convex action context as the first argument, enabling
|
|
632
|
+
* use with Convex components like `@convex-dev/resend`:
|
|
633
|
+
*
|
|
634
|
+
* ```ts
|
|
635
|
+
* send: (ctx, params) => resend.sendEmail(ctx, params)
|
|
636
|
+
* ```
|
|
637
|
+
*
|
|
638
|
+
* For plain HTTP email APIs, ignore the `ctx` parameter:
|
|
639
|
+
*
|
|
640
|
+
* ```ts
|
|
641
|
+
* send: async (_ctx, { from, to, subject, html }) => {
|
|
642
|
+
* await fetch("https://api.resend.com/emails", { ... });
|
|
643
|
+
* }
|
|
644
|
+
* ```
|
|
645
|
+
*/
|
|
646
|
+
send: (
|
|
647
|
+
ctx: GenericActionCtx<any>,
|
|
648
|
+
params: EmailMessage,
|
|
649
|
+
) => Promise<void>;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ============================================================================
|
|
653
|
+
// API Key types
|
|
654
|
+
// ============================================================================
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* A single scope entry stored per API key.
|
|
658
|
+
* Uses a resource:action pattern for structured permissions.
|
|
659
|
+
*
|
|
660
|
+
* ```ts
|
|
661
|
+
* { resource: "users", actions: ["read", "list"] }
|
|
662
|
+
* ```
|
|
663
|
+
*/
|
|
664
|
+
export interface KeyScope {
|
|
665
|
+
resource: string;
|
|
666
|
+
actions: string[];
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Result of scope verification. Provides a `.can()` helper
|
|
671
|
+
* for checking if a key has a specific permission.
|
|
672
|
+
*
|
|
673
|
+
* ```ts
|
|
674
|
+
* const result = await auth.key.verify(ctx, rawKey);
|
|
675
|
+
* if (result.scopes.can("users", "read")) {
|
|
676
|
+
* // authorized
|
|
677
|
+
* }
|
|
678
|
+
* ```
|
|
679
|
+
*/
|
|
680
|
+
export interface ScopeChecker {
|
|
681
|
+
/** Check if the key has permission for a given resource:action. */
|
|
682
|
+
can(resource: string, action: string): boolean;
|
|
683
|
+
/** The raw scope entries from the key. */
|
|
684
|
+
scopes: KeyScope[];
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Configuration for API key support on the Auth class.
|
|
689
|
+
*
|
|
690
|
+
* ```ts
|
|
691
|
+
* const auth = new Auth(components.auth, {
|
|
692
|
+
* providers: [github],
|
|
693
|
+
* apiKeys: {
|
|
694
|
+
* scopes: {
|
|
695
|
+
* users: ["read", "list", "create", "delete"],
|
|
696
|
+
* messages: ["read", "write"],
|
|
697
|
+
* },
|
|
698
|
+
* defaultRateLimit: { maxRequests: 1000, windowMs: 3600000 },
|
|
699
|
+
* },
|
|
700
|
+
* });
|
|
701
|
+
* ```
|
|
702
|
+
*/
|
|
703
|
+
export interface ApiKeyConfig {
|
|
704
|
+
/**
|
|
705
|
+
* Define the available resource:action scopes for your API keys.
|
|
706
|
+
* Keys can only be created with scopes that are a subset of these.
|
|
707
|
+
*/
|
|
708
|
+
scopes?: Record<string, string[]>;
|
|
709
|
+
/**
|
|
710
|
+
* Default rate limit applied to new keys when not specified per-key.
|
|
711
|
+
* Uses a token-bucket algorithm.
|
|
712
|
+
*/
|
|
713
|
+
defaultRateLimit?: { maxRequests: number; windowMs: number };
|
|
714
|
+
/**
|
|
715
|
+
* Key prefix. Defaults to `"sk_live_"`.
|
|
716
|
+
*/
|
|
717
|
+
prefix?: string;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* An API key record as returned by `auth.key.list()` and `auth.key.get()`.
|
|
722
|
+
* Never includes the raw key material — only the display prefix.
|
|
723
|
+
*/
|
|
724
|
+
export interface KeyRecord {
|
|
725
|
+
_id: string;
|
|
726
|
+
userId: string;
|
|
727
|
+
prefix: string;
|
|
728
|
+
name: string;
|
|
729
|
+
scopes: KeyScope[];
|
|
730
|
+
rateLimit?: { maxRequests: number; windowMs: number };
|
|
731
|
+
expiresAt?: number;
|
|
732
|
+
lastUsedAt?: number;
|
|
733
|
+
createdAt: number;
|
|
734
|
+
revoked: boolean;
|
|
735
|
+
}
|
|
736
|
+
|
|
527
737
|
/**
|
|
528
738
|
* Component function references required by core auth runtime.
|
|
529
739
|
*/
|
|
@@ -582,6 +792,13 @@ export type AuthComponentApi = {
|
|
|
582
792
|
inviteList: FunctionReference<"query", "internal">;
|
|
583
793
|
inviteAccept: FunctionReference<"mutation", "internal">;
|
|
584
794
|
inviteRevoke: FunctionReference<"mutation", "internal">;
|
|
795
|
+
keyInsert: FunctionReference<"mutation", "internal">;
|
|
796
|
+
keyGetByHashedKey: FunctionReference<"query", "internal">;
|
|
797
|
+
keyGetById: FunctionReference<"query", "internal">;
|
|
798
|
+
keyList: FunctionReference<"query", "internal">;
|
|
799
|
+
keyListByUserId: FunctionReference<"query", "internal">;
|
|
800
|
+
keyPatch: FunctionReference<"mutation", "internal">;
|
|
801
|
+
keyDelete: FunctionReference<"mutation", "internal">;
|
|
585
802
|
passkeyInsert: FunctionReference<"mutation", "internal">;
|
|
586
803
|
passkeyGetByCredentialId: FunctionReference<"query", "internal">;
|
|
587
804
|
passkeyListByUserId: FunctionReference<"query", "internal">;
|