@convex-dev/better-auth 0.6.0
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/LICENSE +201 -0
- package/README.md +44 -0
- package/dist/commonjs/client/adapter.d.ts +4 -0
- package/dist/commonjs/client/adapter.d.ts.map +1 -0
- package/dist/commonjs/client/adapter.js +189 -0
- package/dist/commonjs/client/adapter.js.map +1 -0
- package/dist/commonjs/client/cors.d.ts +72 -0
- package/dist/commonjs/client/cors.d.ts.map +1 -0
- package/dist/commonjs/client/cors.js +281 -0
- package/dist/commonjs/client/cors.js.map +1 -0
- package/dist/commonjs/client/index.d.ts +302 -0
- package/dist/commonjs/client/index.d.ts.map +1 -0
- package/dist/commonjs/client/index.js +232 -0
- package/dist/commonjs/client/index.js.map +1 -0
- package/dist/commonjs/client/plugins/index.d.ts +3 -0
- package/dist/commonjs/client/plugins/index.d.ts.map +1 -0
- package/dist/commonjs/client/plugins/index.js +3 -0
- package/dist/commonjs/client/plugins/index.js.map +1 -0
- package/dist/commonjs/component/_generated/api.d.ts +12 -0
- package/dist/commonjs/component/_generated/api.d.ts.map +1 -0
- package/dist/commonjs/component/_generated/api.js +22 -0
- package/dist/commonjs/component/_generated/api.js.map +1 -0
- package/dist/commonjs/component/_generated/server.d.ts +64 -0
- package/dist/commonjs/component/_generated/server.d.ts.map +1 -0
- package/dist/commonjs/component/_generated/server.js +74 -0
- package/dist/commonjs/component/_generated/server.js.map +1 -0
- package/dist/commonjs/component/convex.config.d.ts +3 -0
- package/dist/commonjs/component/convex.config.d.ts.map +1 -0
- package/dist/commonjs/component/convex.config.js +4 -0
- package/dist/commonjs/component/convex.config.js.map +1 -0
- package/dist/commonjs/component/lib.d.ts +584 -0
- package/dist/commonjs/component/lib.d.ts.map +1 -0
- package/dist/commonjs/component/lib.js +323 -0
- package/dist/commonjs/component/lib.js.map +1 -0
- package/dist/commonjs/component/schema.d.ts +116 -0
- package/dist/commonjs/component/schema.d.ts.map +1 -0
- package/dist/commonjs/component/schema.js +68 -0
- package/dist/commonjs/component/schema.js.map +1 -0
- package/dist/commonjs/component/util.d.ts +394 -0
- package/dist/commonjs/component/util.d.ts.map +1 -0
- package/dist/commonjs/component/util.js +4 -0
- package/dist/commonjs/component/util.js.map +1 -0
- package/dist/commonjs/nextjs/index.d.ts +10 -0
- package/dist/commonjs/nextjs/index.d.ts.map +1 -0
- package/dist/commonjs/nextjs/index.js +23 -0
- package/dist/commonjs/nextjs/index.js.map +1 -0
- package/dist/commonjs/package.json +3 -0
- package/dist/commonjs/plugins/convex/client.d.ts +6 -0
- package/dist/commonjs/plugins/convex/client.d.ts.map +1 -0
- package/dist/commonjs/plugins/convex/client.js +7 -0
- package/dist/commonjs/plugins/convex/client.js.map +1 -0
- package/dist/commonjs/plugins/convex/index.d.ts +280 -0
- package/dist/commonjs/plugins/convex/index.d.ts.map +1 -0
- package/dist/commonjs/plugins/convex/index.js +253 -0
- package/dist/commonjs/plugins/convex/index.js.map +1 -0
- package/dist/commonjs/plugins/cross-domain/client.d.ts +123 -0
- package/dist/commonjs/plugins/cross-domain/client.d.ts.map +1 -0
- package/dist/commonjs/plugins/cross-domain/client.js +164 -0
- package/dist/commonjs/plugins/cross-domain/client.js.map +1 -0
- package/dist/commonjs/plugins/cross-domain/index.d.ts +81 -0
- package/dist/commonjs/plugins/cross-domain/index.d.ts.map +1 -0
- package/dist/commonjs/plugins/cross-domain/index.js +135 -0
- package/dist/commonjs/plugins/cross-domain/index.js.map +1 -0
- package/dist/commonjs/plugins/index.d.ts +3 -0
- package/dist/commonjs/plugins/index.d.ts.map +1 -0
- package/dist/commonjs/plugins/index.js +3 -0
- package/dist/commonjs/plugins/index.js.map +1 -0
- package/dist/commonjs/react/client.d.ts +31 -0
- package/dist/commonjs/react/client.d.ts.map +1 -0
- package/dist/commonjs/react/client.js +102 -0
- package/dist/commonjs/react/client.js.map +1 -0
- package/dist/commonjs/react/index.d.ts +9 -0
- package/dist/commonjs/react/index.d.ts.map +1 -0
- package/dist/commonjs/react/index.js +15 -0
- package/dist/commonjs/react/index.js.map +1 -0
- package/dist/commonjs/react-start/index.d.ts +10 -0
- package/dist/commonjs/react-start/index.d.ts.map +1 -0
- package/dist/commonjs/react-start/index.js +32 -0
- package/dist/commonjs/react-start/index.js.map +1 -0
- package/dist/esm/client/adapter.d.ts +4 -0
- package/dist/esm/client/adapter.d.ts.map +1 -0
- package/dist/esm/client/adapter.js +189 -0
- package/dist/esm/client/adapter.js.map +1 -0
- package/dist/esm/client/cors.d.ts +72 -0
- package/dist/esm/client/cors.d.ts.map +1 -0
- package/dist/esm/client/cors.js +281 -0
- package/dist/esm/client/cors.js.map +1 -0
- package/dist/esm/client/index.d.ts +302 -0
- package/dist/esm/client/index.d.ts.map +1 -0
- package/dist/esm/client/index.js +232 -0
- package/dist/esm/client/index.js.map +1 -0
- package/dist/esm/client/plugins/index.d.ts +3 -0
- package/dist/esm/client/plugins/index.d.ts.map +1 -0
- package/dist/esm/client/plugins/index.js +3 -0
- package/dist/esm/client/plugins/index.js.map +1 -0
- package/dist/esm/component/_generated/api.d.ts +12 -0
- package/dist/esm/component/_generated/api.d.ts.map +1 -0
- package/dist/esm/component/_generated/api.js +22 -0
- package/dist/esm/component/_generated/api.js.map +1 -0
- package/dist/esm/component/_generated/server.d.ts +64 -0
- package/dist/esm/component/_generated/server.d.ts.map +1 -0
- package/dist/esm/component/_generated/server.js +74 -0
- package/dist/esm/component/_generated/server.js.map +1 -0
- package/dist/esm/component/convex.config.d.ts +3 -0
- package/dist/esm/component/convex.config.d.ts.map +1 -0
- package/dist/esm/component/convex.config.js +4 -0
- package/dist/esm/component/convex.config.js.map +1 -0
- package/dist/esm/component/lib.d.ts +584 -0
- package/dist/esm/component/lib.d.ts.map +1 -0
- package/dist/esm/component/lib.js +323 -0
- package/dist/esm/component/lib.js.map +1 -0
- package/dist/esm/component/schema.d.ts +116 -0
- package/dist/esm/component/schema.d.ts.map +1 -0
- package/dist/esm/component/schema.js +68 -0
- package/dist/esm/component/schema.js.map +1 -0
- package/dist/esm/component/util.d.ts +394 -0
- package/dist/esm/component/util.d.ts.map +1 -0
- package/dist/esm/component/util.js +4 -0
- package/dist/esm/component/util.js.map +1 -0
- package/dist/esm/nextjs/index.d.ts +10 -0
- package/dist/esm/nextjs/index.d.ts.map +1 -0
- package/dist/esm/nextjs/index.js +23 -0
- package/dist/esm/nextjs/index.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/plugins/convex/client.d.ts +6 -0
- package/dist/esm/plugins/convex/client.d.ts.map +1 -0
- package/dist/esm/plugins/convex/client.js +7 -0
- package/dist/esm/plugins/convex/client.js.map +1 -0
- package/dist/esm/plugins/convex/index.d.ts +280 -0
- package/dist/esm/plugins/convex/index.d.ts.map +1 -0
- package/dist/esm/plugins/convex/index.js +253 -0
- package/dist/esm/plugins/convex/index.js.map +1 -0
- package/dist/esm/plugins/cross-domain/client.d.ts +123 -0
- package/dist/esm/plugins/cross-domain/client.d.ts.map +1 -0
- package/dist/esm/plugins/cross-domain/client.js +164 -0
- package/dist/esm/plugins/cross-domain/client.js.map +1 -0
- package/dist/esm/plugins/cross-domain/index.d.ts +81 -0
- package/dist/esm/plugins/cross-domain/index.d.ts.map +1 -0
- package/dist/esm/plugins/cross-domain/index.js +135 -0
- package/dist/esm/plugins/cross-domain/index.js.map +1 -0
- package/dist/esm/plugins/index.d.ts +3 -0
- package/dist/esm/plugins/index.d.ts.map +1 -0
- package/dist/esm/plugins/index.js +3 -0
- package/dist/esm/plugins/index.js.map +1 -0
- package/dist/esm/react/client.d.ts +31 -0
- package/dist/esm/react/client.d.ts.map +1 -0
- package/dist/esm/react/client.js +102 -0
- package/dist/esm/react/client.js.map +1 -0
- package/dist/esm/react/index.d.ts +9 -0
- package/dist/esm/react/index.d.ts.map +1 -0
- package/dist/esm/react/index.js +15 -0
- package/dist/esm/react/index.js.map +1 -0
- package/dist/esm/react-start/index.d.ts +10 -0
- package/dist/esm/react-start/index.d.ts.map +1 -0
- package/dist/esm/react-start/index.js +32 -0
- package/dist/esm/react-start/index.js.map +1 -0
- package/package.json +161 -0
- package/plugins/package.json +5 -0
- package/react/package.json +5 -0
- package/src/client/adapter.ts +236 -0
- package/src/client/cors.ts +403 -0
- package/src/client/index.ts +381 -0
- package/src/client/plugins/index.ts +2 -0
- package/src/component/_generated/api.d.ts +313 -0
- package/src/component/_generated/api.js +23 -0
- package/src/component/_generated/dataModel.d.ts +60 -0
- package/src/component/_generated/server.d.ts +149 -0
- package/src/component/_generated/server.js +90 -0
- package/src/component/convex.config.ts +5 -0
- package/src/component/lib.ts +391 -0
- package/src/component/schema.ts +74 -0
- package/src/component/util.ts +4 -0
- package/src/nextjs/index.ts +30 -0
- package/src/plugins/convex/client.ts +9 -0
- package/src/plugins/convex/index.ts +296 -0
- package/src/plugins/cross-domain/client.ts +209 -0
- package/src/plugins/cross-domain/index.ts +156 -0
- package/src/plugins/index.ts +2 -0
- package/src/react/client.tsx +184 -0
- package/src/react/index.tsx +38 -0
- package/src/react-start/index.ts +51 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
|
|
4
|
+
const schema = defineSchema({
|
|
5
|
+
user: defineTable({
|
|
6
|
+
name: v.string(),
|
|
7
|
+
email: v.string(),
|
|
8
|
+
emailVerified: v.boolean(),
|
|
9
|
+
image: v.optional(v.string()),
|
|
10
|
+
twoFactorEnabled: v.optional(v.boolean()),
|
|
11
|
+
userId: v.string(),
|
|
12
|
+
createdAt: v.number(),
|
|
13
|
+
updatedAt: v.number(),
|
|
14
|
+
})
|
|
15
|
+
.index("email", ["email"])
|
|
16
|
+
.index("userId", ["userId"]),
|
|
17
|
+
|
|
18
|
+
session: defineTable({
|
|
19
|
+
expiresAt: v.number(),
|
|
20
|
+
token: v.string(),
|
|
21
|
+
createdAt: v.number(),
|
|
22
|
+
updatedAt: v.number(),
|
|
23
|
+
ipAddress: v.optional(v.string()),
|
|
24
|
+
userAgent: v.optional(v.string()),
|
|
25
|
+
userId: v.string(),
|
|
26
|
+
})
|
|
27
|
+
.index("token", ["token"])
|
|
28
|
+
.index("userId", ["userId"]),
|
|
29
|
+
|
|
30
|
+
account: defineTable({
|
|
31
|
+
accountId: v.string(),
|
|
32
|
+
providerId: v.string(),
|
|
33
|
+
userId: v.string(),
|
|
34
|
+
accessToken: v.optional(v.string()),
|
|
35
|
+
refreshToken: v.optional(v.string()),
|
|
36
|
+
idToken: v.optional(v.string()),
|
|
37
|
+
accessTokenExpiresAt: v.optional(v.number()),
|
|
38
|
+
refreshTokenExpiresAt: v.optional(v.number()),
|
|
39
|
+
scope: v.optional(v.string()),
|
|
40
|
+
password: v.optional(v.string()),
|
|
41
|
+
createdAt: v.number(),
|
|
42
|
+
updatedAt: v.number(),
|
|
43
|
+
})
|
|
44
|
+
.index("userId", ["userId"])
|
|
45
|
+
.index("accountId", ["accountId"])
|
|
46
|
+
.index("providerId_accountId", ["providerId", "accountId"])
|
|
47
|
+
.index("userId_providerId", ["userId", "providerId"]),
|
|
48
|
+
|
|
49
|
+
twoFactor: defineTable({
|
|
50
|
+
secret: v.string(),
|
|
51
|
+
backupCodes: v.string(),
|
|
52
|
+
userId: v.string(),
|
|
53
|
+
}).index("userId", ["userId"]),
|
|
54
|
+
|
|
55
|
+
verification: defineTable({
|
|
56
|
+
identifier: v.string(),
|
|
57
|
+
value: v.string(),
|
|
58
|
+
expiresAt: v.number(),
|
|
59
|
+
createdAt: v.optional(v.number()),
|
|
60
|
+
updatedAt: v.optional(v.number()),
|
|
61
|
+
})
|
|
62
|
+
.index("identifier", ["identifier"])
|
|
63
|
+
.index("expiresAt", ["expiresAt"]),
|
|
64
|
+
|
|
65
|
+
jwks: defineTable({
|
|
66
|
+
publicKey: v.string(),
|
|
67
|
+
privateKey: v.string(),
|
|
68
|
+
createdAt: v.number(),
|
|
69
|
+
// no longer used
|
|
70
|
+
id: v.optional(v.string()),
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export default schema;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { betterAuth } from "better-auth";
|
|
2
|
+
import { createCookieGetter } from "better-auth/cookies";
|
|
3
|
+
import { GenericActionCtx } from "convex/server";
|
|
4
|
+
|
|
5
|
+
export const getToken = async (
|
|
6
|
+
createAuth: (ctx: GenericActionCtx<any>) => ReturnType<typeof betterAuth>
|
|
7
|
+
) => {
|
|
8
|
+
const { cookies } = await import("next/headers");
|
|
9
|
+
const cookieStore = await cookies();
|
|
10
|
+
const auth = createAuth({} as any);
|
|
11
|
+
const createCookie = createCookieGetter(auth.options);
|
|
12
|
+
const cookie = createCookie("convex_jwt");
|
|
13
|
+
const token = cookieStore.get(cookie.name);
|
|
14
|
+
return typeof token === "string" ? token : token?.value;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const handler = (request: Request, opts?: { convexSiteUrl?: string }) => {
|
|
18
|
+
const requestUrl = new URL(request.url);
|
|
19
|
+
const convexSiteUrl =
|
|
20
|
+
opts?.convexSiteUrl ?? process.env.NEXT_PUBLIC_CONVEX_SITE_URL;
|
|
21
|
+
const nextUrl = `${convexSiteUrl}${requestUrl.pathname}${requestUrl.search}`;
|
|
22
|
+
const newRequest = new Request(nextUrl, request);
|
|
23
|
+
newRequest.headers.set("accept-encoding", "application/json");
|
|
24
|
+
return fetch(newRequest, { method: request.method, redirect: "manual" });
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const nextJsHandler = (opts?: { convexSiteUrl?: string }) => ({
|
|
28
|
+
GET: (request: Request) => handler(request, opts),
|
|
29
|
+
POST: (request: Request) => handler(request, opts),
|
|
30
|
+
});
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAuthMiddleware,
|
|
3
|
+
getSession,
|
|
4
|
+
sessionMiddleware,
|
|
5
|
+
} from "better-auth/api";
|
|
6
|
+
import {
|
|
7
|
+
BetterAuthPlugin,
|
|
8
|
+
createAuthEndpoint,
|
|
9
|
+
customSession as customSessionPlugin,
|
|
10
|
+
jwt as jwtPlugin,
|
|
11
|
+
bearer as bearerPlugin,
|
|
12
|
+
oidcProvider as oidcProviderPlugin,
|
|
13
|
+
} from "better-auth/plugins";
|
|
14
|
+
import { omit } from "convex-helpers";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
|
|
17
|
+
const JWT_COOKIE_NAME = "convex_jwt";
|
|
18
|
+
|
|
19
|
+
export const convex = (opts: { jwtExpirationSeconds?: number } = {}) => {
|
|
20
|
+
const { jwtExpirationSeconds = 60 * 15 } = opts;
|
|
21
|
+
const customSession = customSessionPlugin(async ({ user, session }) => {
|
|
22
|
+
const { userId, ...userData } = omit(user, ["id"]) as typeof user & {
|
|
23
|
+
userId: string;
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
user: { ...userData, id: userId },
|
|
27
|
+
session: {
|
|
28
|
+
...session,
|
|
29
|
+
userId,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
const oidcProvider = oidcProviderPlugin({
|
|
34
|
+
loginPage: "/not-used",
|
|
35
|
+
metadata: {
|
|
36
|
+
issuer: `${process.env.CONVEX_SITE_URL}`,
|
|
37
|
+
jwks_uri: `${process.env.CONVEX_SITE_URL}/api/auth/convex/jwks`,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const jwt = jwtPlugin({
|
|
41
|
+
jwt: {
|
|
42
|
+
issuer: `${process.env.CONVEX_SITE_URL}`,
|
|
43
|
+
audience: "convex",
|
|
44
|
+
expirationTime: `${jwtExpirationSeconds}s`,
|
|
45
|
+
getSubject: (session) => {
|
|
46
|
+
// Return the userId from the app user table
|
|
47
|
+
return session.user.userId;
|
|
48
|
+
},
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
50
|
+
definePayload: ({ user: { id, userId, ...user }, session }) => ({
|
|
51
|
+
...user,
|
|
52
|
+
sessionId: session.id,
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
// Bearer plugin converts the session token to a cookie
|
|
57
|
+
// for cross domain social login after code verification, and is required for
|
|
58
|
+
// the headers() helper to work.
|
|
59
|
+
const bearer = bearerPlugin();
|
|
60
|
+
const schema = {
|
|
61
|
+
user: {
|
|
62
|
+
fields: { userId: { type: "string", required: false, input: false } },
|
|
63
|
+
} as const,
|
|
64
|
+
...jwt.schema,
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
id: "convex",
|
|
68
|
+
hooks: {
|
|
69
|
+
before: [...bearer.hooks.before],
|
|
70
|
+
after: [
|
|
71
|
+
...oidcProvider.hooks.after,
|
|
72
|
+
{
|
|
73
|
+
matcher: (ctx) => {
|
|
74
|
+
return ctx.path?.startsWith("/sign-out");
|
|
75
|
+
},
|
|
76
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
77
|
+
const jwtCookie = ctx.context.createAuthCookie(JWT_COOKIE_NAME, {
|
|
78
|
+
maxAge: 0,
|
|
79
|
+
});
|
|
80
|
+
ctx.setCookie(jwtCookie.name, "", jwtCookie.attributes);
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
endpoints: {
|
|
86
|
+
getSession: createAuthEndpoint(
|
|
87
|
+
"/get-session",
|
|
88
|
+
{
|
|
89
|
+
method: "GET",
|
|
90
|
+
query: z.optional(
|
|
91
|
+
z.object({
|
|
92
|
+
// If cookie cache is enabled, it will disable the cache
|
|
93
|
+
// and fetch the session from the database
|
|
94
|
+
disableCookieCache: z
|
|
95
|
+
.boolean({
|
|
96
|
+
description:
|
|
97
|
+
"Disable cookie cache and fetch session from database",
|
|
98
|
+
})
|
|
99
|
+
.or(z.string().transform((v) => v === "true"))
|
|
100
|
+
.optional(),
|
|
101
|
+
disableRefresh: z
|
|
102
|
+
.boolean({
|
|
103
|
+
description:
|
|
104
|
+
"Disable session refresh. Useful for checking session status, without updating the session",
|
|
105
|
+
})
|
|
106
|
+
.optional(),
|
|
107
|
+
})
|
|
108
|
+
),
|
|
109
|
+
metadata: {
|
|
110
|
+
CUSTOM_SESSION: true,
|
|
111
|
+
openapi: {
|
|
112
|
+
description: "Get custom session data",
|
|
113
|
+
responses: {
|
|
114
|
+
"200": {
|
|
115
|
+
description: "Success",
|
|
116
|
+
content: {
|
|
117
|
+
"application/json": {
|
|
118
|
+
schema: {
|
|
119
|
+
type: "array",
|
|
120
|
+
nullable: true,
|
|
121
|
+
items: {
|
|
122
|
+
$ref: "#/components/schemas/Session",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
requireHeaders: true,
|
|
132
|
+
},
|
|
133
|
+
async (ctx) => {
|
|
134
|
+
const response = await customSession.endpoints.getSession({
|
|
135
|
+
...ctx,
|
|
136
|
+
returnHeaders: false,
|
|
137
|
+
});
|
|
138
|
+
return response;
|
|
139
|
+
}
|
|
140
|
+
) as unknown as ReturnType<typeof getSession>,
|
|
141
|
+
getOpenIdConfig: createAuthEndpoint(
|
|
142
|
+
"/convex/.well-known/openid-configuration",
|
|
143
|
+
{
|
|
144
|
+
method: "GET",
|
|
145
|
+
metadata: {
|
|
146
|
+
isAction: false,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
async (ctx) => {
|
|
150
|
+
const response = await oidcProvider.endpoints.getOpenIdConfig({
|
|
151
|
+
...ctx,
|
|
152
|
+
returnHeaders: false,
|
|
153
|
+
});
|
|
154
|
+
return response;
|
|
155
|
+
}
|
|
156
|
+
),
|
|
157
|
+
getJwks: createAuthEndpoint(
|
|
158
|
+
"/convex/jwks",
|
|
159
|
+
{
|
|
160
|
+
method: "GET",
|
|
161
|
+
metadata: {
|
|
162
|
+
openapi: {
|
|
163
|
+
description: "Get the JSON Web Key Set",
|
|
164
|
+
responses: {
|
|
165
|
+
"200": {
|
|
166
|
+
description: "JSON Web Key Set retrieved successfully",
|
|
167
|
+
content: {
|
|
168
|
+
"application/json": {
|
|
169
|
+
schema: {
|
|
170
|
+
type: "object",
|
|
171
|
+
properties: {
|
|
172
|
+
keys: {
|
|
173
|
+
type: "array",
|
|
174
|
+
description: "Array of public JSON Web Keys",
|
|
175
|
+
items: {
|
|
176
|
+
type: "object",
|
|
177
|
+
properties: {
|
|
178
|
+
kid: {
|
|
179
|
+
type: "string",
|
|
180
|
+
description:
|
|
181
|
+
"Key ID uniquely identifying the key, corresponds to the 'id' from the stored Jwk",
|
|
182
|
+
},
|
|
183
|
+
kty: {
|
|
184
|
+
type: "string",
|
|
185
|
+
description:
|
|
186
|
+
"Key type (e.g., 'RSA', 'EC', 'OKP')",
|
|
187
|
+
},
|
|
188
|
+
alg: {
|
|
189
|
+
type: "string",
|
|
190
|
+
description:
|
|
191
|
+
"Algorithm intended for use with the key (e.g., 'EdDSA', 'RS256')",
|
|
192
|
+
},
|
|
193
|
+
use: {
|
|
194
|
+
type: "string",
|
|
195
|
+
description:
|
|
196
|
+
"Intended use of the public key (e.g., 'sig' for signature)",
|
|
197
|
+
enum: ["sig"],
|
|
198
|
+
nullable: true,
|
|
199
|
+
},
|
|
200
|
+
n: {
|
|
201
|
+
type: "string",
|
|
202
|
+
description:
|
|
203
|
+
"Modulus for RSA keys (base64url-encoded)",
|
|
204
|
+
nullable: true,
|
|
205
|
+
},
|
|
206
|
+
e: {
|
|
207
|
+
type: "string",
|
|
208
|
+
description:
|
|
209
|
+
"Exponent for RSA keys (base64url-encoded)",
|
|
210
|
+
nullable: true,
|
|
211
|
+
},
|
|
212
|
+
crv: {
|
|
213
|
+
type: "string",
|
|
214
|
+
description:
|
|
215
|
+
"Curve name for elliptic curve keys (e.g., 'Ed25519', 'P-256')",
|
|
216
|
+
nullable: true,
|
|
217
|
+
},
|
|
218
|
+
x: {
|
|
219
|
+
type: "string",
|
|
220
|
+
description:
|
|
221
|
+
"X coordinate for elliptic curve keys (base64url-encoded)",
|
|
222
|
+
nullable: true,
|
|
223
|
+
},
|
|
224
|
+
y: {
|
|
225
|
+
type: "string",
|
|
226
|
+
description:
|
|
227
|
+
"Y coordinate for elliptic curve keys (base64url-encoded)",
|
|
228
|
+
nullable: true,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
required: ["kid", "kty", "alg"],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
required: ["keys"],
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
async (ctx) => {
|
|
245
|
+
const response = await jwt.endpoints.getJwks({
|
|
246
|
+
...ctx,
|
|
247
|
+
returnHeaders: false,
|
|
248
|
+
});
|
|
249
|
+
return response;
|
|
250
|
+
}
|
|
251
|
+
),
|
|
252
|
+
getToken: createAuthEndpoint(
|
|
253
|
+
"/convex/token",
|
|
254
|
+
{
|
|
255
|
+
method: "GET",
|
|
256
|
+
requireHeaders: true,
|
|
257
|
+
use: [sessionMiddleware],
|
|
258
|
+
metadata: {
|
|
259
|
+
openapi: {
|
|
260
|
+
description: "Get a JWT token",
|
|
261
|
+
responses: {
|
|
262
|
+
200: {
|
|
263
|
+
description: "Success",
|
|
264
|
+
content: {
|
|
265
|
+
"application/json": {
|
|
266
|
+
schema: {
|
|
267
|
+
type: "object",
|
|
268
|
+
properties: {
|
|
269
|
+
token: {
|
|
270
|
+
type: "string",
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
async (ctx) => {
|
|
282
|
+
const response = await jwt.endpoints.getToken({
|
|
283
|
+
...ctx,
|
|
284
|
+
returnHeaders: false,
|
|
285
|
+
});
|
|
286
|
+
const jwtCookie = ctx.context.createAuthCookie(JWT_COOKIE_NAME, {
|
|
287
|
+
maxAge: jwtExpirationSeconds,
|
|
288
|
+
});
|
|
289
|
+
ctx.setCookie(jwtCookie.name, response.token, jwtCookie.attributes);
|
|
290
|
+
return response;
|
|
291
|
+
}
|
|
292
|
+
),
|
|
293
|
+
},
|
|
294
|
+
schema,
|
|
295
|
+
} satisfies BetterAuthPlugin;
|
|
296
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { BetterAuthClientPlugin, Store } from "better-auth";
|
|
2
|
+
import { BetterFetchOption } from "@better-fetch/fetch";
|
|
3
|
+
import { crossDomain } from ".";
|
|
4
|
+
|
|
5
|
+
interface CookieAttributes {
|
|
6
|
+
value: string;
|
|
7
|
+
expires?: Date;
|
|
8
|
+
"max-age"?: number;
|
|
9
|
+
domain?: string;
|
|
10
|
+
path?: string;
|
|
11
|
+
secure?: boolean;
|
|
12
|
+
httpOnly?: boolean;
|
|
13
|
+
sameSite?: "Strict" | "Lax" | "None";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseSetCookieHeader(
|
|
17
|
+
header: string
|
|
18
|
+
): Map<string, CookieAttributes> {
|
|
19
|
+
const cookieMap = new Map<string, CookieAttributes>();
|
|
20
|
+
const cookies = header.split(", ");
|
|
21
|
+
cookies.forEach((cookie) => {
|
|
22
|
+
const [nameValue, ...attributes] = cookie.split("; ");
|
|
23
|
+
const [name, value] = nameValue.split("=");
|
|
24
|
+
|
|
25
|
+
const cookieObj: CookieAttributes = { value };
|
|
26
|
+
|
|
27
|
+
attributes.forEach((attr) => {
|
|
28
|
+
const [attrName, attrValue] = attr.split("=");
|
|
29
|
+
cookieObj[attrName.toLowerCase() as "value"] = attrValue;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
cookieMap.set(name, cookieObj);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return cookieMap;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface StoredCookie {
|
|
39
|
+
value: string;
|
|
40
|
+
expires: Date | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getSetCookie(header: string, prevCookie?: string) {
|
|
44
|
+
const parsed = parseSetCookieHeader(header);
|
|
45
|
+
let toSetCookie: Record<string, StoredCookie> = {};
|
|
46
|
+
parsed.forEach((cookie, key) => {
|
|
47
|
+
const expiresAt = cookie["expires"];
|
|
48
|
+
const maxAge = cookie["max-age"];
|
|
49
|
+
const expires = expiresAt
|
|
50
|
+
? new Date(String(expiresAt))
|
|
51
|
+
: maxAge
|
|
52
|
+
? new Date(Date.now() + Number(maxAge) * 1000)
|
|
53
|
+
: null;
|
|
54
|
+
toSetCookie[key] = {
|
|
55
|
+
value: cookie["value"],
|
|
56
|
+
expires,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
if (prevCookie) {
|
|
60
|
+
try {
|
|
61
|
+
const prevCookieParsed = JSON.parse(prevCookie);
|
|
62
|
+
toSetCookie = {
|
|
63
|
+
...prevCookieParsed,
|
|
64
|
+
...toSetCookie,
|
|
65
|
+
};
|
|
66
|
+
} catch {
|
|
67
|
+
//
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return JSON.stringify(toSetCookie);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getCookie(cookie: string) {
|
|
74
|
+
let parsed = {} as Record<string, StoredCookie>;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(cookie) as Record<string, StoredCookie>;
|
|
77
|
+
} catch {
|
|
78
|
+
// noop
|
|
79
|
+
}
|
|
80
|
+
const toSend = Object.entries(parsed).reduce((acc, [key, value]) => {
|
|
81
|
+
if (value.expires && value.expires < new Date()) {
|
|
82
|
+
return acc;
|
|
83
|
+
}
|
|
84
|
+
return `${acc}; ${key}=${value.value}`;
|
|
85
|
+
}, "");
|
|
86
|
+
return toSend;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const crossDomainClient = (
|
|
90
|
+
opts: {
|
|
91
|
+
storage?: {
|
|
92
|
+
setItem: (key: string, value: string) => any;
|
|
93
|
+
getItem: (key: string) => string | null;
|
|
94
|
+
};
|
|
95
|
+
storagePrefix?: string;
|
|
96
|
+
disableCache?: boolean;
|
|
97
|
+
} = {}
|
|
98
|
+
) => {
|
|
99
|
+
let store: Store | null = null;
|
|
100
|
+
const cookieName = `${opts?.storagePrefix || "better-auth"}_cookie`;
|
|
101
|
+
const localCacheName = `${opts?.storagePrefix || "better-auth"}_session_data`;
|
|
102
|
+
const storage =
|
|
103
|
+
opts?.storage || (typeof window !== "undefined" ? localStorage : undefined);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
id: "cross-domain",
|
|
107
|
+
$InferServerPlugin: {} as ReturnType<typeof crossDomain>,
|
|
108
|
+
getActions(_, $store) {
|
|
109
|
+
store = $store;
|
|
110
|
+
return {
|
|
111
|
+
/**
|
|
112
|
+
* Get the stored cookie.
|
|
113
|
+
*
|
|
114
|
+
* You can use this to get the cookie stored in the device and use it in your fetch
|
|
115
|
+
* requests.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* const cookie = client.getCookie();
|
|
120
|
+
* fetch("https://api.example.com", {
|
|
121
|
+
* headers: {
|
|
122
|
+
* cookie,
|
|
123
|
+
* },
|
|
124
|
+
* });
|
|
125
|
+
*/
|
|
126
|
+
getCookie: () => {
|
|
127
|
+
const cookie = storage?.getItem(cookieName);
|
|
128
|
+
return getCookie(cookie || "{}");
|
|
129
|
+
},
|
|
130
|
+
/**
|
|
131
|
+
* Notify the session signal.
|
|
132
|
+
*
|
|
133
|
+
* This is used to trigger an update in useSession, generally when a new session
|
|
134
|
+
* token is set.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* client.notifySessionSignal();
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
updateSession: () => {
|
|
142
|
+
$store.notify("$sessionSignal");
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
fetchPlugins: [
|
|
147
|
+
{
|
|
148
|
+
id: "convex",
|
|
149
|
+
name: "Convex",
|
|
150
|
+
hooks: {
|
|
151
|
+
async onSuccess(context) {
|
|
152
|
+
if (!storage) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const setCookie = context.response.headers.get(
|
|
156
|
+
"set-better-auth-cookie"
|
|
157
|
+
);
|
|
158
|
+
if (setCookie) {
|
|
159
|
+
const prevCookie = await storage.getItem(cookieName);
|
|
160
|
+
const toSetCookie = getSetCookie(
|
|
161
|
+
setCookie || "",
|
|
162
|
+
prevCookie ?? undefined
|
|
163
|
+
);
|
|
164
|
+
await storage.setItem(cookieName, toSetCookie);
|
|
165
|
+
store?.notify("$sessionSignal");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
context.request.url.toString().includes("/get-session") &&
|
|
170
|
+
!opts?.disableCache
|
|
171
|
+
) {
|
|
172
|
+
const data = context.data;
|
|
173
|
+
storage.setItem(localCacheName, JSON.stringify(data));
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
async init(url, options) {
|
|
178
|
+
if (!storage) {
|
|
179
|
+
return {
|
|
180
|
+
url,
|
|
181
|
+
options: options as BetterFetchOption,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
options = options || {};
|
|
185
|
+
const storedCookie = storage.getItem(cookieName);
|
|
186
|
+
const cookie = getCookie(storedCookie || "{}");
|
|
187
|
+
options.credentials = "omit";
|
|
188
|
+
options.headers = {
|
|
189
|
+
...options.headers,
|
|
190
|
+
"Better-Auth-Cookie": cookie,
|
|
191
|
+
};
|
|
192
|
+
if (url.includes("/sign-out")) {
|
|
193
|
+
await storage.setItem(cookieName, "{}");
|
|
194
|
+
store?.atoms.session?.set({
|
|
195
|
+
data: null,
|
|
196
|
+
error: null,
|
|
197
|
+
isPending: false,
|
|
198
|
+
});
|
|
199
|
+
storage.setItem(localCacheName, "{}");
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
url,
|
|
203
|
+
options: options as BetterFetchOption,
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
} satisfies BetterAuthClientPlugin;
|
|
209
|
+
};
|