@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.
Files changed (181) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +44 -0
  3. package/dist/commonjs/client/adapter.d.ts +4 -0
  4. package/dist/commonjs/client/adapter.d.ts.map +1 -0
  5. package/dist/commonjs/client/adapter.js +189 -0
  6. package/dist/commonjs/client/adapter.js.map +1 -0
  7. package/dist/commonjs/client/cors.d.ts +72 -0
  8. package/dist/commonjs/client/cors.d.ts.map +1 -0
  9. package/dist/commonjs/client/cors.js +281 -0
  10. package/dist/commonjs/client/cors.js.map +1 -0
  11. package/dist/commonjs/client/index.d.ts +302 -0
  12. package/dist/commonjs/client/index.d.ts.map +1 -0
  13. package/dist/commonjs/client/index.js +232 -0
  14. package/dist/commonjs/client/index.js.map +1 -0
  15. package/dist/commonjs/client/plugins/index.d.ts +3 -0
  16. package/dist/commonjs/client/plugins/index.d.ts.map +1 -0
  17. package/dist/commonjs/client/plugins/index.js +3 -0
  18. package/dist/commonjs/client/plugins/index.js.map +1 -0
  19. package/dist/commonjs/component/_generated/api.d.ts +12 -0
  20. package/dist/commonjs/component/_generated/api.d.ts.map +1 -0
  21. package/dist/commonjs/component/_generated/api.js +22 -0
  22. package/dist/commonjs/component/_generated/api.js.map +1 -0
  23. package/dist/commonjs/component/_generated/server.d.ts +64 -0
  24. package/dist/commonjs/component/_generated/server.d.ts.map +1 -0
  25. package/dist/commonjs/component/_generated/server.js +74 -0
  26. package/dist/commonjs/component/_generated/server.js.map +1 -0
  27. package/dist/commonjs/component/convex.config.d.ts +3 -0
  28. package/dist/commonjs/component/convex.config.d.ts.map +1 -0
  29. package/dist/commonjs/component/convex.config.js +4 -0
  30. package/dist/commonjs/component/convex.config.js.map +1 -0
  31. package/dist/commonjs/component/lib.d.ts +584 -0
  32. package/dist/commonjs/component/lib.d.ts.map +1 -0
  33. package/dist/commonjs/component/lib.js +323 -0
  34. package/dist/commonjs/component/lib.js.map +1 -0
  35. package/dist/commonjs/component/schema.d.ts +116 -0
  36. package/dist/commonjs/component/schema.d.ts.map +1 -0
  37. package/dist/commonjs/component/schema.js +68 -0
  38. package/dist/commonjs/component/schema.js.map +1 -0
  39. package/dist/commonjs/component/util.d.ts +394 -0
  40. package/dist/commonjs/component/util.d.ts.map +1 -0
  41. package/dist/commonjs/component/util.js +4 -0
  42. package/dist/commonjs/component/util.js.map +1 -0
  43. package/dist/commonjs/nextjs/index.d.ts +10 -0
  44. package/dist/commonjs/nextjs/index.d.ts.map +1 -0
  45. package/dist/commonjs/nextjs/index.js +23 -0
  46. package/dist/commonjs/nextjs/index.js.map +1 -0
  47. package/dist/commonjs/package.json +3 -0
  48. package/dist/commonjs/plugins/convex/client.d.ts +6 -0
  49. package/dist/commonjs/plugins/convex/client.d.ts.map +1 -0
  50. package/dist/commonjs/plugins/convex/client.js +7 -0
  51. package/dist/commonjs/plugins/convex/client.js.map +1 -0
  52. package/dist/commonjs/plugins/convex/index.d.ts +280 -0
  53. package/dist/commonjs/plugins/convex/index.d.ts.map +1 -0
  54. package/dist/commonjs/plugins/convex/index.js +253 -0
  55. package/dist/commonjs/plugins/convex/index.js.map +1 -0
  56. package/dist/commonjs/plugins/cross-domain/client.d.ts +123 -0
  57. package/dist/commonjs/plugins/cross-domain/client.d.ts.map +1 -0
  58. package/dist/commonjs/plugins/cross-domain/client.js +164 -0
  59. package/dist/commonjs/plugins/cross-domain/client.js.map +1 -0
  60. package/dist/commonjs/plugins/cross-domain/index.d.ts +81 -0
  61. package/dist/commonjs/plugins/cross-domain/index.d.ts.map +1 -0
  62. package/dist/commonjs/plugins/cross-domain/index.js +135 -0
  63. package/dist/commonjs/plugins/cross-domain/index.js.map +1 -0
  64. package/dist/commonjs/plugins/index.d.ts +3 -0
  65. package/dist/commonjs/plugins/index.d.ts.map +1 -0
  66. package/dist/commonjs/plugins/index.js +3 -0
  67. package/dist/commonjs/plugins/index.js.map +1 -0
  68. package/dist/commonjs/react/client.d.ts +31 -0
  69. package/dist/commonjs/react/client.d.ts.map +1 -0
  70. package/dist/commonjs/react/client.js +102 -0
  71. package/dist/commonjs/react/client.js.map +1 -0
  72. package/dist/commonjs/react/index.d.ts +9 -0
  73. package/dist/commonjs/react/index.d.ts.map +1 -0
  74. package/dist/commonjs/react/index.js +15 -0
  75. package/dist/commonjs/react/index.js.map +1 -0
  76. package/dist/commonjs/react-start/index.d.ts +10 -0
  77. package/dist/commonjs/react-start/index.d.ts.map +1 -0
  78. package/dist/commonjs/react-start/index.js +32 -0
  79. package/dist/commonjs/react-start/index.js.map +1 -0
  80. package/dist/esm/client/adapter.d.ts +4 -0
  81. package/dist/esm/client/adapter.d.ts.map +1 -0
  82. package/dist/esm/client/adapter.js +189 -0
  83. package/dist/esm/client/adapter.js.map +1 -0
  84. package/dist/esm/client/cors.d.ts +72 -0
  85. package/dist/esm/client/cors.d.ts.map +1 -0
  86. package/dist/esm/client/cors.js +281 -0
  87. package/dist/esm/client/cors.js.map +1 -0
  88. package/dist/esm/client/index.d.ts +302 -0
  89. package/dist/esm/client/index.d.ts.map +1 -0
  90. package/dist/esm/client/index.js +232 -0
  91. package/dist/esm/client/index.js.map +1 -0
  92. package/dist/esm/client/plugins/index.d.ts +3 -0
  93. package/dist/esm/client/plugins/index.d.ts.map +1 -0
  94. package/dist/esm/client/plugins/index.js +3 -0
  95. package/dist/esm/client/plugins/index.js.map +1 -0
  96. package/dist/esm/component/_generated/api.d.ts +12 -0
  97. package/dist/esm/component/_generated/api.d.ts.map +1 -0
  98. package/dist/esm/component/_generated/api.js +22 -0
  99. package/dist/esm/component/_generated/api.js.map +1 -0
  100. package/dist/esm/component/_generated/server.d.ts +64 -0
  101. package/dist/esm/component/_generated/server.d.ts.map +1 -0
  102. package/dist/esm/component/_generated/server.js +74 -0
  103. package/dist/esm/component/_generated/server.js.map +1 -0
  104. package/dist/esm/component/convex.config.d.ts +3 -0
  105. package/dist/esm/component/convex.config.d.ts.map +1 -0
  106. package/dist/esm/component/convex.config.js +4 -0
  107. package/dist/esm/component/convex.config.js.map +1 -0
  108. package/dist/esm/component/lib.d.ts +584 -0
  109. package/dist/esm/component/lib.d.ts.map +1 -0
  110. package/dist/esm/component/lib.js +323 -0
  111. package/dist/esm/component/lib.js.map +1 -0
  112. package/dist/esm/component/schema.d.ts +116 -0
  113. package/dist/esm/component/schema.d.ts.map +1 -0
  114. package/dist/esm/component/schema.js +68 -0
  115. package/dist/esm/component/schema.js.map +1 -0
  116. package/dist/esm/component/util.d.ts +394 -0
  117. package/dist/esm/component/util.d.ts.map +1 -0
  118. package/dist/esm/component/util.js +4 -0
  119. package/dist/esm/component/util.js.map +1 -0
  120. package/dist/esm/nextjs/index.d.ts +10 -0
  121. package/dist/esm/nextjs/index.d.ts.map +1 -0
  122. package/dist/esm/nextjs/index.js +23 -0
  123. package/dist/esm/nextjs/index.js.map +1 -0
  124. package/dist/esm/package.json +3 -0
  125. package/dist/esm/plugins/convex/client.d.ts +6 -0
  126. package/dist/esm/plugins/convex/client.d.ts.map +1 -0
  127. package/dist/esm/plugins/convex/client.js +7 -0
  128. package/dist/esm/plugins/convex/client.js.map +1 -0
  129. package/dist/esm/plugins/convex/index.d.ts +280 -0
  130. package/dist/esm/plugins/convex/index.d.ts.map +1 -0
  131. package/dist/esm/plugins/convex/index.js +253 -0
  132. package/dist/esm/plugins/convex/index.js.map +1 -0
  133. package/dist/esm/plugins/cross-domain/client.d.ts +123 -0
  134. package/dist/esm/plugins/cross-domain/client.d.ts.map +1 -0
  135. package/dist/esm/plugins/cross-domain/client.js +164 -0
  136. package/dist/esm/plugins/cross-domain/client.js.map +1 -0
  137. package/dist/esm/plugins/cross-domain/index.d.ts +81 -0
  138. package/dist/esm/plugins/cross-domain/index.d.ts.map +1 -0
  139. package/dist/esm/plugins/cross-domain/index.js +135 -0
  140. package/dist/esm/plugins/cross-domain/index.js.map +1 -0
  141. package/dist/esm/plugins/index.d.ts +3 -0
  142. package/dist/esm/plugins/index.d.ts.map +1 -0
  143. package/dist/esm/plugins/index.js +3 -0
  144. package/dist/esm/plugins/index.js.map +1 -0
  145. package/dist/esm/react/client.d.ts +31 -0
  146. package/dist/esm/react/client.d.ts.map +1 -0
  147. package/dist/esm/react/client.js +102 -0
  148. package/dist/esm/react/client.js.map +1 -0
  149. package/dist/esm/react/index.d.ts +9 -0
  150. package/dist/esm/react/index.d.ts.map +1 -0
  151. package/dist/esm/react/index.js +15 -0
  152. package/dist/esm/react/index.js.map +1 -0
  153. package/dist/esm/react-start/index.d.ts +10 -0
  154. package/dist/esm/react-start/index.d.ts.map +1 -0
  155. package/dist/esm/react-start/index.js +32 -0
  156. package/dist/esm/react-start/index.js.map +1 -0
  157. package/package.json +161 -0
  158. package/plugins/package.json +5 -0
  159. package/react/package.json +5 -0
  160. package/src/client/adapter.ts +236 -0
  161. package/src/client/cors.ts +403 -0
  162. package/src/client/index.ts +381 -0
  163. package/src/client/plugins/index.ts +2 -0
  164. package/src/component/_generated/api.d.ts +313 -0
  165. package/src/component/_generated/api.js +23 -0
  166. package/src/component/_generated/dataModel.d.ts +60 -0
  167. package/src/component/_generated/server.d.ts +149 -0
  168. package/src/component/_generated/server.js +90 -0
  169. package/src/component/convex.config.ts +5 -0
  170. package/src/component/lib.ts +391 -0
  171. package/src/component/schema.ts +74 -0
  172. package/src/component/util.ts +4 -0
  173. package/src/nextjs/index.ts +30 -0
  174. package/src/plugins/convex/client.ts +9 -0
  175. package/src/plugins/convex/index.ts +296 -0
  176. package/src/plugins/cross-domain/client.ts +209 -0
  177. package/src/plugins/cross-domain/index.ts +156 -0
  178. package/src/plugins/index.ts +2 -0
  179. package/src/react/client.tsx +184 -0
  180. package/src/react/index.tsx +38 -0
  181. 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,4 @@
1
+ import { typedV } from "convex-helpers/validators";
2
+ import schema from "./schema";
3
+
4
+ export const vv = typedV(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,9 @@
1
+ import { BetterAuthClientPlugin } from "better-auth/client";
2
+ import { convex } from ".";
3
+
4
+ export const convexClient = () => {
5
+ return {
6
+ id: "convex",
7
+ $InferServerPlugin: {} as ReturnType<typeof convex>,
8
+ } satisfies BetterAuthClientPlugin;
9
+ };
@@ -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
+ };