@convex-dev/better-auth 0.11.4 → 0.12.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/dist/auth-options.d.ts.map +1 -1
- package/dist/auth-options.js +1 -0
- package/dist/auth-options.js.map +1 -1
- package/dist/client/adapter-utils.d.ts +8 -2
- package/dist/client/adapter-utils.d.ts.map +1 -1
- package/dist/client/adapter-utils.js +8 -3
- package/dist/client/adapter-utils.js.map +1 -1
- package/dist/client/adapter.d.ts.map +1 -1
- package/dist/client/adapter.js +5 -0
- package/dist/client/adapter.js.map +1 -1
- package/dist/client/create-api.d.ts +6 -0
- package/dist/client/create-api.d.ts.map +1 -1
- package/dist/client/create-api.js +1 -0
- package/dist/client/create-api.js.map +1 -1
- package/dist/client/create-client.d.ts.map +1 -1
- package/dist/client/create-client.js +19 -2
- package/dist/client/create-client.js.map +1 -1
- package/dist/client/create-schema.d.ts.map +1 -1
- package/dist/client/create-schema.js +38 -8
- package/dist/client/create-schema.js.map +1 -1
- package/dist/component/_generated/component.d.ts +543 -28
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/adapter.d.ts +6 -0
- package/dist/component/adapter.d.ts.map +1 -1
- package/dist/component/schema.d.ts +9 -4
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +4 -2
- package/dist/component/schema.js.map +1 -1
- package/dist/component/testProfiles/adapterAdditionalFields.d.ts +6 -0
- package/dist/component/testProfiles/adapterAdditionalFields.d.ts.map +1 -1
- package/dist/component/testProfiles/adapterOrganizationJoins.d.ts +6 -0
- package/dist/component/testProfiles/adapterOrganizationJoins.d.ts.map +1 -1
- package/dist/component/testProfiles/adapterPluginTable.d.ts +6 -0
- package/dist/component/testProfiles/adapterPluginTable.d.ts.map +1 -1
- package/dist/component/testProfiles/adapterRenameField.d.ts +6 -0
- package/dist/component/testProfiles/adapterRenameField.d.ts.map +1 -1
- package/dist/component/testProfiles/adapterRenameUserCustom.d.ts +6 -0
- package/dist/component/testProfiles/adapterRenameUserCustom.d.ts.map +1 -1
- package/dist/component/testProfiles/adapterRenameUserTable.d.ts +6 -0
- package/dist/component/testProfiles/adapterRenameUserTable.d.ts.map +1 -1
- package/dist/component/testProfiles/schema.profile-additional-fields.d.ts +3 -1
- package/dist/component/testProfiles/schema.profile-additional-fields.d.ts.map +1 -1
- package/dist/component/testProfiles/schema.profile-plugin-table.d.ts +3 -1
- package/dist/component/testProfiles/schema.profile-plugin-table.d.ts.map +1 -1
- package/dist/nextjs/index.d.ts.map +1 -1
- package/dist/nextjs/index.js +4 -0
- package/dist/nextjs/index.js.map +1 -1
- package/dist/plugins/convex/client.d.ts +1 -0
- package/dist/plugins/convex/client.d.ts.map +1 -1
- package/dist/plugins/convex/client.js +2 -0
- package/dist/plugins/convex/client.js.map +1 -1
- package/dist/plugins/convex/index.d.ts +2 -1
- package/dist/plugins/convex/index.d.ts.map +1 -1
- package/dist/plugins/convex/index.js +15 -2
- package/dist/plugins/convex/index.js.map +1 -1
- package/dist/plugins/cross-domain/client.d.ts +1 -12
- package/dist/plugins/cross-domain/client.d.ts.map +1 -1
- package/dist/plugins/cross-domain/client.js +14 -26
- package/dist/plugins/cross-domain/client.js.map +1 -1
- package/dist/plugins/cross-domain/index.d.ts +1 -0
- package/dist/plugins/cross-domain/index.d.ts.map +1 -1
- package/dist/plugins/cross-domain/index.js +15 -4
- package/dist/plugins/cross-domain/index.js.map +1 -1
- package/dist/react-start/index.d.ts.map +1 -1
- package/dist/react-start/index.js +4 -0
- package/dist/react-start/index.js.map +1 -1
- package/dist/test/adapter-factory/basic.d.ts +4 -0
- package/dist/test/adapter-factory/basic.d.ts.map +1 -1
- package/dist/test/adapter-factory/convex-custom.d.ts +1 -1
- package/dist/test/adapter-factory/convex-custom.d.ts.map +1 -1
- package/dist/test/adapter-factory/convex-custom.js +20 -0
- package/dist/test/adapter-factory/convex-custom.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +5 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +16 -19
- package/src/auth-options.ts +1 -0
- package/src/client/adapter-utils.ts +13 -5
- package/src/client/adapter.ts +7 -0
- package/src/client/create-api.ts +3 -0
- package/src/client/create-client.test.ts +40 -0
- package/src/client/create-client.ts +25 -2
- package/src/client/create-schema.ts +46 -8
- package/src/component/_generated/component.ts +718 -35
- package/src/component/_generated/server.ts +0 -5
- package/src/component/schema.ts +5 -3
- package/src/nextjs/index.ts +4 -0
- package/src/plugins/convex/client.ts +2 -0
- package/src/plugins/convex/index.ts +16 -3
- package/src/plugins/cross-domain/client.test.ts +41 -30
- package/src/plugins/cross-domain/client.ts +17 -44
- package/src/plugins/cross-domain/index.test.ts +129 -51
- package/src/plugins/cross-domain/index.ts +20 -7
- package/src/react-start/index.ts +4 -0
- package/src/test/adapter-factory/convex-custom.ts +23 -0
- package/src/utils/index.ts +6 -1
- package/src/version.ts +1 -0
|
@@ -107,11 +107,6 @@ export const internalAction: ActionBuilder<DataModel, "internal"> =
|
|
|
107
107
|
*/
|
|
108
108
|
export const httpAction: HttpActionBuilder = httpActionGeneric;
|
|
109
109
|
|
|
110
|
-
type GenericCtx =
|
|
111
|
-
| GenericActionCtx<DataModel>
|
|
112
|
-
| GenericMutationCtx<DataModel>
|
|
113
|
-
| GenericQueryCtx<DataModel>;
|
|
114
|
-
|
|
115
110
|
/**
|
|
116
111
|
* A set of services for use within Convex query functions.
|
|
117
112
|
*
|
package/src/component/schema.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* This file is auto-generated. Do not edit this file manually.
|
|
3
|
-
* To regenerate the schema,
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* To regenerate the schema, from your project root:
|
|
4
|
+
*
|
|
5
|
+
* npx auth generate --output src/component/schema.ts
|
|
6
|
+
*
|
|
6
7
|
* To customize the schema, generate to an alternate file and import
|
|
7
8
|
* the table definitions to your schema file. See
|
|
8
9
|
* https://labs.convex.dev/better-auth/features/local-install#adding-custom-indexes.
|
|
@@ -76,6 +77,7 @@ export const tables = {
|
|
|
76
77
|
secret: v.string(),
|
|
77
78
|
backupCodes: v.string(),
|
|
78
79
|
userId: v.string(),
|
|
80
|
+
verified: v.optional(v.union(v.null(), v.boolean())),
|
|
79
81
|
})
|
|
80
82
|
.index("userId", ["userId"]),
|
|
81
83
|
oauthApplication: defineTable({
|
package/src/nextjs/index.ts
CHANGED
|
@@ -46,6 +46,10 @@ const handler = (request: Request, siteUrl: string) => {
|
|
|
46
46
|
const newRequest = new Request(nextUrl, request);
|
|
47
47
|
newRequest.headers.set("accept-encoding", "application/json");
|
|
48
48
|
newRequest.headers.set("host", new URL(siteUrl).host);
|
|
49
|
+
newRequest.headers.set("x-forwarded-host", requestUrl.host);
|
|
50
|
+
newRequest.headers.set("x-forwarded-proto", requestUrl.protocol.replace(/:$/, ""));
|
|
51
|
+
newRequest.headers.set("x-better-auth-forwarded-host", requestUrl.host);
|
|
52
|
+
newRequest.headers.set("x-better-auth-forwarded-proto", requestUrl.protocol.replace(/:$/, ""));
|
|
49
53
|
return fetch(newRequest, { method: request.method, redirect: "manual" });
|
|
50
54
|
};
|
|
51
55
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { BetterAuthClientPlugin } from "better-auth/client";
|
|
2
2
|
import type { convex } from "./index.js";
|
|
3
|
+
import { VERSION } from "../../version.js";
|
|
3
4
|
|
|
4
5
|
export const convexClient = () => {
|
|
5
6
|
return {
|
|
6
7
|
id: "convex",
|
|
8
|
+
version: VERSION,
|
|
7
9
|
$InferServerPlugin: {} as ReturnType<typeof convex>,
|
|
8
10
|
} satisfies BetterAuthClientPlugin;
|
|
9
11
|
};
|
|
@@ -11,6 +11,7 @@ import type { JwtOptions, Jwk } from "better-auth/plugins/jwt";
|
|
|
11
11
|
import { oidcProvider as oidcProviderPlugin } from "better-auth/plugins/oidc-provider";
|
|
12
12
|
import { omit } from "convex-helpers";
|
|
13
13
|
import type { AuthConfig, AuthProvider } from "convex/server";
|
|
14
|
+
import { VERSION } from "../../version.js";
|
|
14
15
|
|
|
15
16
|
export const JWT_COOKIE_NAME = "convex_jwt";
|
|
16
17
|
|
|
@@ -164,7 +165,7 @@ export const convex = (opts: {
|
|
|
164
165
|
* Handles error that occurs when existing JWKS key does not match configured
|
|
165
166
|
* algorithm, which will be common for 0.10 upgrades switching from EdDSA to RS256.
|
|
166
167
|
*
|
|
167
|
-
* @default
|
|
168
|
+
* @default false
|
|
168
169
|
*/
|
|
169
170
|
jwksRotateOnTokenGenerationError?: boolean;
|
|
170
171
|
/**
|
|
@@ -181,6 +182,7 @@ export const convex = (opts: {
|
|
|
181
182
|
issuer: `${process.env.CONVEX_SITE_URL}`,
|
|
182
183
|
jwks_uri: `${process.env.CONVEX_SITE_URL}${opts.options?.basePath ?? "/api/auth"}/convex/jwks`,
|
|
183
184
|
},
|
|
185
|
+
__skipDeprecationWarning: true,
|
|
184
186
|
});
|
|
185
187
|
const providerConfig = parseAuthConfig(opts.authConfig, opts);
|
|
186
188
|
|
|
@@ -189,9 +191,9 @@ export const convex = (opts: {
|
|
|
189
191
|
issuer: `${process.env.CONVEX_SITE_URL}`,
|
|
190
192
|
audience: "convex",
|
|
191
193
|
expirationTime: `${jwtExpirationSeconds}s`,
|
|
192
|
-
definePayload: ({ user, session }) => ({
|
|
194
|
+
definePayload: async ({ user, session }) => ({
|
|
193
195
|
...(opts.jwt?.definePayload
|
|
194
|
-
? opts.jwt.definePayload({ user, session })
|
|
196
|
+
? await opts.jwt.definePayload({ user, session })
|
|
195
197
|
: omit(user, ["id", "image"])),
|
|
196
198
|
sessionId: session.id,
|
|
197
199
|
iat: Math.floor(new Date().getTime() / 1000),
|
|
@@ -253,6 +255,7 @@ export const convex = (opts: {
|
|
|
253
255
|
|
|
254
256
|
return {
|
|
255
257
|
id: "convex",
|
|
258
|
+
version: VERSION,
|
|
256
259
|
init: (ctx) => {
|
|
257
260
|
const { options, logger: _logger } = ctx;
|
|
258
261
|
if (options.basePath !== "/api/auth" && !opts.options?.basePath) {
|
|
@@ -337,6 +340,7 @@ export const convex = (opts: {
|
|
|
337
340
|
...ctx,
|
|
338
341
|
headers: {},
|
|
339
342
|
method: "GET",
|
|
343
|
+
asResponse: false,
|
|
340
344
|
returnHeaders: false,
|
|
341
345
|
returnStatus: false,
|
|
342
346
|
});
|
|
@@ -382,6 +386,7 @@ export const convex = (opts: {
|
|
|
382
386
|
async (ctx) => {
|
|
383
387
|
const response = await oidcProvider.endpoints.getOpenIdConfig({
|
|
384
388
|
...ctx,
|
|
389
|
+
asResponse: false,
|
|
385
390
|
returnHeaders: false,
|
|
386
391
|
returnStatus: false,
|
|
387
392
|
});
|
|
@@ -478,6 +483,7 @@ export const convex = (opts: {
|
|
|
478
483
|
async (ctx) => {
|
|
479
484
|
const response = await jwt.endpoints.getJwks({
|
|
480
485
|
...ctx,
|
|
486
|
+
asResponse: false,
|
|
481
487
|
returnHeaders: false,
|
|
482
488
|
returnStatus: false,
|
|
483
489
|
});
|
|
@@ -504,6 +510,9 @@ export const convex = (opts: {
|
|
|
504
510
|
await jwtPlugin(jwtOptions).endpoints.getJwks({
|
|
505
511
|
...ctx,
|
|
506
512
|
method: "GET",
|
|
513
|
+
asResponse: false,
|
|
514
|
+
returnHeaders: false,
|
|
515
|
+
returnStatus: false,
|
|
507
516
|
});
|
|
508
517
|
const jwks: any[] = await ctx.context.adapter.findMany({
|
|
509
518
|
model: "jwks",
|
|
@@ -542,6 +551,9 @@ export const convex = (opts: {
|
|
|
542
551
|
await jwtPlugin(jwtOptions).endpoints.getJwks({
|
|
543
552
|
...ctx,
|
|
544
553
|
method: "GET",
|
|
554
|
+
asResponse: false,
|
|
555
|
+
returnHeaders: false,
|
|
556
|
+
returnStatus: false,
|
|
545
557
|
});
|
|
546
558
|
const jwks: any[] = await ctx.context.adapter.findMany({
|
|
547
559
|
model: "jwks",
|
|
@@ -588,6 +600,7 @@ export const convex = (opts: {
|
|
|
588
600
|
const runEndpoint = async () => {
|
|
589
601
|
const response = await jwt.endpoints.getToken({
|
|
590
602
|
...ctx,
|
|
603
|
+
asResponse: false,
|
|
591
604
|
returnHeaders: false,
|
|
592
605
|
returnStatus: false,
|
|
593
606
|
});
|
|
@@ -1,29 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getCookie, getSetCookie
|
|
3
|
-
|
|
4
|
-
describe("parseSetCookieHeader", () => {
|
|
5
|
-
it("parses a simple cookie", () => {
|
|
6
|
-
const header = "session_token=abc123";
|
|
7
|
-
const map = parseSetCookieHeader(header);
|
|
8
|
-
expect(map.get("session_token")?.value).toBe("abc123");
|
|
9
|
-
});
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { crossDomainClient, getCookie, getSetCookie } from "./client.js";
|
|
10
3
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
expect(cookie?.value).toBe("abc123");
|
|
16
|
-
});
|
|
4
|
+
describe("getSetCookie", () => {
|
|
5
|
+
it("stores cookies from RFC-1123 Expires headers without splitting the date", () => {
|
|
6
|
+
const header =
|
|
7
|
+
"session_token=abc; Path=/; Expires=Wed, 21 Oct 2030 07:28:00 GMT, other_cookie=xyz; Path=/";
|
|
17
8
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
const result = JSON.parse(getSetCookie(header));
|
|
10
|
+
|
|
11
|
+
expect(result.session_token).toEqual({
|
|
12
|
+
value: "abc",
|
|
13
|
+
expires: "2030-10-21T07:28:00.000Z",
|
|
14
|
+
});
|
|
15
|
+
expect(result.other_cookie).toEqual({
|
|
16
|
+
value: "xyz",
|
|
17
|
+
expires: null,
|
|
18
|
+
});
|
|
23
19
|
});
|
|
24
|
-
});
|
|
25
20
|
|
|
26
|
-
describe("getSetCookie", () => {
|
|
27
21
|
it("stores expires as ISO string", () => {
|
|
28
22
|
const header = "session_token=abc; Max-Age=3600";
|
|
29
23
|
const result = JSON.parse(getSetCookie(header));
|
|
@@ -66,7 +60,10 @@ describe("getSetCookie", () => {
|
|
|
66
60
|
describe("getCookie", () => {
|
|
67
61
|
it("returns cookie string for valid cookies", () => {
|
|
68
62
|
const stored = JSON.stringify({
|
|
69
|
-
session: {
|
|
63
|
+
session: {
|
|
64
|
+
value: "abc",
|
|
65
|
+
expires: new Date(Date.now() + 60000).toISOString(),
|
|
66
|
+
},
|
|
70
67
|
});
|
|
71
68
|
const result = getCookie(stored);
|
|
72
69
|
expect(result).toContain("session=abc");
|
|
@@ -74,8 +71,14 @@ describe("getCookie", () => {
|
|
|
74
71
|
|
|
75
72
|
it("filters out expired cookies", () => {
|
|
76
73
|
const stored = JSON.stringify({
|
|
77
|
-
expired: {
|
|
78
|
-
|
|
74
|
+
expired: {
|
|
75
|
+
value: "old",
|
|
76
|
+
expires: new Date(Date.now() - 60000).toISOString(),
|
|
77
|
+
},
|
|
78
|
+
valid: {
|
|
79
|
+
value: "new",
|
|
80
|
+
expires: new Date(Date.now() + 60000).toISOString(),
|
|
81
|
+
},
|
|
79
82
|
});
|
|
80
83
|
const result = getCookie(stored);
|
|
81
84
|
expect(result).not.toContain("expired=old");
|
|
@@ -112,7 +115,10 @@ describe("getCookie", () => {
|
|
|
112
115
|
|
|
113
116
|
describe("crossDomainClient", () => {
|
|
114
117
|
let storage: Map<string, string>;
|
|
115
|
-
let mockStorage: {
|
|
118
|
+
let mockStorage: {
|
|
119
|
+
getItem: (key: string) => string | null;
|
|
120
|
+
setItem: (key: string, value: string) => void;
|
|
121
|
+
};
|
|
116
122
|
const cookieName = "better-auth_cookie";
|
|
117
123
|
const localCacheName = "better-auth_session_data";
|
|
118
124
|
|
|
@@ -120,7 +126,9 @@ describe("crossDomainClient", () => {
|
|
|
120
126
|
storage = new Map<string, string>();
|
|
121
127
|
mockStorage = {
|
|
122
128
|
getItem: (key) => storage.get(key) ?? null,
|
|
123
|
-
setItem: (key, value) => {
|
|
129
|
+
setItem: (key, value) => {
|
|
130
|
+
storage.set(key, value);
|
|
131
|
+
},
|
|
124
132
|
};
|
|
125
133
|
});
|
|
126
134
|
|
|
@@ -184,9 +192,12 @@ describe("crossDomainClient", () => {
|
|
|
184
192
|
|
|
185
193
|
describe("onSuccess handler", () => {
|
|
186
194
|
it("clears cookies when get-session returns null", async () => {
|
|
187
|
-
storage.set(
|
|
188
|
-
|
|
189
|
-
|
|
195
|
+
storage.set(
|
|
196
|
+
cookieName,
|
|
197
|
+
JSON.stringify({
|
|
198
|
+
"better-auth.session_token": { value: "stale", expires: null },
|
|
199
|
+
})
|
|
200
|
+
);
|
|
190
201
|
|
|
191
202
|
const onSuccess = getOnSuccessHook();
|
|
192
203
|
await onSuccess({
|
|
@@ -1,39 +1,8 @@
|
|
|
1
1
|
import type { BetterAuthClientPlugin, ClientStore } from "better-auth";
|
|
2
|
+
import { parseSetCookieHeader } from "better-auth/cookies";
|
|
2
3
|
import type { BetterFetchOption } from "@better-fetch/fetch";
|
|
3
4
|
import type { crossDomain } from "./index.js";
|
|
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
|
-
}
|
|
5
|
+
import { VERSION } from "../../version.js";
|
|
37
6
|
|
|
38
7
|
interface StoredCookie {
|
|
39
8
|
value: string;
|
|
@@ -77,13 +46,10 @@ export function getCookie(cookie: string) {
|
|
|
77
46
|
} catch {
|
|
78
47
|
// noop
|
|
79
48
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return `${acc}; ${key}=${value.value}`;
|
|
85
|
-
}, "");
|
|
86
|
-
return toSend;
|
|
49
|
+
return Object.entries(parsed)
|
|
50
|
+
.filter(([, value]) => !value.expires || new Date(value.expires) >= new Date())
|
|
51
|
+
.map(([key, value]) => `${key}=${value.value}`)
|
|
52
|
+
.join("; ");
|
|
87
53
|
}
|
|
88
54
|
|
|
89
55
|
export const crossDomainClient = (
|
|
@@ -104,6 +70,7 @@ export const crossDomainClient = (
|
|
|
104
70
|
|
|
105
71
|
return {
|
|
106
72
|
id: "cross-domain",
|
|
73
|
+
version: VERSION,
|
|
107
74
|
$InferServerPlugin: {} as ReturnType<typeof crossDomain>,
|
|
108
75
|
getActions(_, $store) {
|
|
109
76
|
store = $store;
|
|
@@ -154,7 +121,13 @@ export const crossDomainClient = (
|
|
|
154
121
|
if (!sessionData) return null;
|
|
155
122
|
try {
|
|
156
123
|
const parsed = JSON.parse(sessionData);
|
|
157
|
-
if (
|
|
124
|
+
if (
|
|
125
|
+
parsed &&
|
|
126
|
+
typeof parsed === "object" &&
|
|
127
|
+
Object.keys(parsed).length === 0
|
|
128
|
+
) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
158
131
|
return parsed;
|
|
159
132
|
} catch {
|
|
160
133
|
return null;
|
|
@@ -164,8 +137,8 @@ export const crossDomainClient = (
|
|
|
164
137
|
},
|
|
165
138
|
fetchPlugins: [
|
|
166
139
|
{
|
|
167
|
-
id: "
|
|
168
|
-
name: "
|
|
140
|
+
id: "cross-domain",
|
|
141
|
+
name: "Cross Domain",
|
|
169
142
|
hooks: {
|
|
170
143
|
async onSuccess(context) {
|
|
171
144
|
if (!storage) {
|
|
@@ -239,7 +212,7 @@ export const crossDomainClient = (
|
|
|
239
212
|
error: null,
|
|
240
213
|
isPending: false,
|
|
241
214
|
});
|
|
242
|
-
storage.setItem(localCacheName, "{}");
|
|
215
|
+
await storage.setItem(localCacheName, "{}");
|
|
243
216
|
}
|
|
244
217
|
return {
|
|
245
218
|
url,
|
|
@@ -1,67 +1,145 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { betterAuth } from "better-auth/minimal";
|
|
3
|
+
import { memoryAdapter } from "better-auth/adapters/memory";
|
|
4
|
+
import type { MemoryDB } from "better-auth/adapters/memory";
|
|
5
|
+
import { genericOAuth } from "better-auth/plugins";
|
|
6
|
+
import { magicLink } from "better-auth/plugins/magic-link";
|
|
2
7
|
import { crossDomain } from "./index.js";
|
|
3
8
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (!matcher) {
|
|
8
|
-
throw new Error("expected cross-domain POST rewrite matcher");
|
|
9
|
-
}
|
|
10
|
-
return matcher;
|
|
11
|
-
};
|
|
9
|
+
const SITE_URL = "https://myapp.example.com";
|
|
10
|
+
const AUTH_BASE_URL = "http://localhost:3000";
|
|
11
|
+
const BASE_PATH = "/api/auth";
|
|
12
12
|
|
|
13
|
-
describe("crossDomain
|
|
14
|
-
|
|
15
|
-
const matcher = getPostRewriteMatcher();
|
|
16
|
-
type MatcherContext = Parameters<typeof matcher>[0];
|
|
13
|
+
describe("crossDomain plugin", async () => {
|
|
14
|
+
let capturedMagicLinkUrl = "";
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
method: "POST",
|
|
25
|
-
path: "/custom-endpoint",
|
|
26
|
-
headers: new Headers(),
|
|
27
|
-
} satisfies Partial<MatcherContext>;
|
|
16
|
+
const db: MemoryDB = {
|
|
17
|
+
user: [],
|
|
18
|
+
session: [],
|
|
19
|
+
account: [],
|
|
20
|
+
verification: [],
|
|
21
|
+
};
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
const auth = betterAuth({
|
|
24
|
+
baseURL: AUTH_BASE_URL,
|
|
25
|
+
basePath: BASE_PATH,
|
|
26
|
+
secret: "test-secret-at-least-thirty-two-characters-long",
|
|
27
|
+
database: memoryAdapter(db),
|
|
28
|
+
emailAndPassword: {
|
|
29
|
+
enabled: true,
|
|
30
|
+
requireEmailVerification: false,
|
|
31
|
+
},
|
|
32
|
+
plugins: [
|
|
33
|
+
magicLink({
|
|
34
|
+
sendMagicLink: async ({ url }) => {
|
|
35
|
+
capturedMagicLinkUrl = url;
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
genericOAuth({
|
|
39
|
+
config: [
|
|
40
|
+
{
|
|
41
|
+
providerId: "example-oauth",
|
|
42
|
+
clientId: "test-client-id",
|
|
43
|
+
clientSecret: "test-client-secret",
|
|
44
|
+
authorizationUrl: "https://provider.example.com/oauth/authorize",
|
|
45
|
+
tokenUrl: "https://provider.example.com/oauth/token",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
}),
|
|
49
|
+
crossDomain({ siteUrl: SITE_URL }),
|
|
50
|
+
],
|
|
31
51
|
});
|
|
32
52
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
const post = (
|
|
54
|
+
path: string,
|
|
55
|
+
body: Record<string, unknown>,
|
|
56
|
+
extraHeaders?: Record<string, string>
|
|
57
|
+
) =>
|
|
58
|
+
auth.handler(
|
|
59
|
+
new Request(`${AUTH_BASE_URL}${BASE_PATH}${path}`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "Content-Type": "application/json", ...extraHeaders },
|
|
62
|
+
body: JSON.stringify(body),
|
|
63
|
+
})
|
|
64
|
+
);
|
|
36
65
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} satisfies Partial<MatcherContext>;
|
|
42
|
-
const optionsLinkSocialCtx = {
|
|
43
|
-
method: "OPTIONS",
|
|
44
|
-
path: "/link-social",
|
|
45
|
-
headers: new Headers(),
|
|
46
|
-
} satisfies Partial<MatcherContext>;
|
|
47
|
-
|
|
48
|
-
expect(matcher(getSignInCtx as MatcherContext)).toBe(false);
|
|
49
|
-
expect(matcher(optionsLinkSocialCtx as MatcherContext)).toBe(false);
|
|
66
|
+
await post("/sign-up/email", {
|
|
67
|
+
email: "test@example.com",
|
|
68
|
+
password: "testpassword123",
|
|
69
|
+
name: "Test User",
|
|
50
70
|
});
|
|
51
71
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
describe("callbackURL defaulting for magic-link", () => {
|
|
73
|
+
it("injects siteUrl when callbackURL is absent", async () => {
|
|
74
|
+
capturedMagicLinkUrl = "";
|
|
75
|
+
await post("/sign-in/magic-link", { email: "test@example.com" });
|
|
76
|
+
const url = new URL(capturedMagicLinkUrl);
|
|
77
|
+
expect(url.searchParams.get("callbackURL")).toBe(SITE_URL);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("rewrites relative callbackURL to absolute using siteUrl", async () => {
|
|
81
|
+
capturedMagicLinkUrl = "";
|
|
82
|
+
await post("/sign-in/magic-link", {
|
|
83
|
+
email: "test@example.com",
|
|
84
|
+
callbackURL: "/dashboard",
|
|
85
|
+
});
|
|
86
|
+
const url = new URL(capturedMagicLinkUrl);
|
|
87
|
+
expect(url.searchParams.get("callbackURL")).toBe(`${SITE_URL}/dashboard`);
|
|
88
|
+
});
|
|
55
89
|
|
|
56
|
-
|
|
57
|
-
|
|
90
|
+
it("preserves absolute callbackURL", async () => {
|
|
91
|
+
capturedMagicLinkUrl = "";
|
|
92
|
+
await post("/sign-in/magic-link", {
|
|
93
|
+
email: "test@example.com",
|
|
94
|
+
callbackURL: "https://other.example.com/callback",
|
|
95
|
+
});
|
|
96
|
+
const url = new URL(capturedMagicLinkUrl);
|
|
97
|
+
expect(url.searchParams.get("callbackURL")).toBe(
|
|
98
|
+
"https://other.example.com/callback"
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
58
102
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
103
|
+
describe("callbackURL defaulting for oauth2", () => {
|
|
104
|
+
it("injects siteUrl when callbackURL is absent", async () => {
|
|
105
|
+
const response = await post("/sign-in/oauth2", {
|
|
106
|
+
providerId: "example-oauth",
|
|
107
|
+
disableRedirect: true,
|
|
108
|
+
});
|
|
109
|
+
const { url } = (await response.json()) as { url: string };
|
|
110
|
+
const state = new URL(url).searchParams.get("state");
|
|
111
|
+
const verification = db.verification.find(
|
|
112
|
+
(entry) => entry.identifier === state
|
|
113
|
+
);
|
|
114
|
+
expect(verification).toBeDefined();
|
|
115
|
+
expect(JSON.parse(verification!.value).callbackURL).toBe(SITE_URL);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("rewrites relative callbackURL to absolute using siteUrl", async () => {
|
|
119
|
+
const response = await post("/sign-in/oauth2", {
|
|
120
|
+
providerId: "example-oauth",
|
|
121
|
+
callbackURL: "/dashboard",
|
|
122
|
+
disableRedirect: true,
|
|
123
|
+
});
|
|
124
|
+
const { url } = (await response.json()) as { url: string };
|
|
125
|
+
const state = new URL(url).searchParams.get("state");
|
|
126
|
+
const verification = db.verification.find(
|
|
127
|
+
(entry) => entry.identifier === state
|
|
128
|
+
);
|
|
129
|
+
expect(verification).toBeDefined();
|
|
130
|
+
expect(JSON.parse(verification!.value).callbackURL).toBe(
|
|
131
|
+
`${SITE_URL}/dashboard`
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
64
135
|
|
|
65
|
-
|
|
136
|
+
describe("no callbackURL injection for email sign-in", () => {
|
|
137
|
+
it("does not redirect when callbackURL is absent", async () => {
|
|
138
|
+
const response = await post("/sign-in/email", {
|
|
139
|
+
email: "test@example.com",
|
|
140
|
+
password: "testpassword123",
|
|
141
|
+
});
|
|
142
|
+
expect(response.status).not.toBe(302);
|
|
143
|
+
});
|
|
66
144
|
});
|
|
67
145
|
});
|
|
@@ -4,6 +4,7 @@ import { generateRandomString } from "better-auth/crypto";
|
|
|
4
4
|
import { createAuthEndpoint, createAuthMiddleware } from "better-auth/api";
|
|
5
5
|
import { oneTimeToken as oneTimeTokenPlugin } from "better-auth/plugins/one-time-token";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
+
import { VERSION } from "../../version.js";
|
|
7
8
|
|
|
8
9
|
export const crossDomain = ({ siteUrl }: { siteUrl: string }) => {
|
|
9
10
|
const oneTimeToken = oneTimeTokenPlugin();
|
|
@@ -24,6 +25,7 @@ export const crossDomain = ({ siteUrl }: { siteUrl: string }) => {
|
|
|
24
25
|
|
|
25
26
|
return {
|
|
26
27
|
id: "cross-domain",
|
|
28
|
+
version: VERSION,
|
|
27
29
|
// TODO: remove this in the next minor release, it doesn't
|
|
28
30
|
// actually affect ctx.trustedOrigins. cors allowedOrigins
|
|
29
31
|
// is using it, via options.trustedOrigins, though, so it's
|
|
@@ -55,8 +57,7 @@ export const crossDomain = ({ siteUrl }: { siteUrl: string }) => {
|
|
|
55
57
|
Boolean(
|
|
56
58
|
ctx.request?.headers.has("better-auth-cookie") ||
|
|
57
59
|
ctx.headers?.has("better-auth-cookie")
|
|
58
|
-
) &&
|
|
59
|
-
!isExpoNative(ctx)
|
|
60
|
+
) && !isExpoNative(ctx)
|
|
60
61
|
);
|
|
61
62
|
},
|
|
62
63
|
handler: createAuthMiddleware(async (ctx) => {
|
|
@@ -85,8 +86,8 @@ export const crossDomain = ({ siteUrl }: { siteUrl: string }) => {
|
|
|
85
86
|
matcher: (ctx) => {
|
|
86
87
|
return Boolean(
|
|
87
88
|
ctx.method === "GET" &&
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
ctx.path?.startsWith("/verify-email") &&
|
|
90
|
+
!isExpoNative(ctx)
|
|
90
91
|
);
|
|
91
92
|
},
|
|
92
93
|
handler: createAuthMiddleware(async (ctx) => {
|
|
@@ -101,6 +102,18 @@ export const crossDomain = ({ siteUrl }: { siteUrl: string }) => {
|
|
|
101
102
|
return Boolean(ctx.method === "POST" && !isExpoNative(ctx));
|
|
102
103
|
},
|
|
103
104
|
handler: createAuthMiddleware(async (ctx) => {
|
|
105
|
+
// Set callbackURL to siteUrl for redirect-triggering paths with
|
|
106
|
+
// no callbackURL defined.
|
|
107
|
+
if (
|
|
108
|
+
ctx.body &&
|
|
109
|
+
!ctx.body.callbackURL &&
|
|
110
|
+
(ctx.path?.startsWith("/sign-in/social") ||
|
|
111
|
+
ctx.path?.startsWith("/sign-in/oauth2") ||
|
|
112
|
+
ctx.path?.startsWith("/sign-in/magic-link") ||
|
|
113
|
+
ctx.path?.startsWith("/send-verification-email"))
|
|
114
|
+
) {
|
|
115
|
+
ctx.body.callbackURL = siteUrl;
|
|
116
|
+
}
|
|
104
117
|
if (ctx.body?.callbackURL) {
|
|
105
118
|
ctx.body.callbackURL = rewriteCallbackURL(ctx.body.callbackURL);
|
|
106
119
|
}
|
|
@@ -125,8 +138,7 @@ export const crossDomain = ({ siteUrl }: { siteUrl: string }) => {
|
|
|
125
138
|
Boolean(
|
|
126
139
|
ctx.request?.headers.has("better-auth-cookie") ||
|
|
127
140
|
ctx.headers?.has("better-auth-cookie")
|
|
128
|
-
) &&
|
|
129
|
-
!isExpoNative(ctx)
|
|
141
|
+
) && !isExpoNative(ctx)
|
|
130
142
|
);
|
|
131
143
|
},
|
|
132
144
|
handler: createAuthMiddleware(async (ctx) => {
|
|
@@ -144,7 +156,7 @@ export const crossDomain = ({ siteUrl }: { siteUrl: string }) => {
|
|
|
144
156
|
(ctx.path?.startsWith("/callback") ||
|
|
145
157
|
ctx.path?.startsWith("/oauth2/callback") ||
|
|
146
158
|
ctx.path?.startsWith("/magic-link/verify")) &&
|
|
147
|
-
|
|
159
|
+
!isExpoNative(ctx)
|
|
148
160
|
);
|
|
149
161
|
},
|
|
150
162
|
handler: createAuthMiddleware(async (ctx) => {
|
|
@@ -185,6 +197,7 @@ export const crossDomain = ({ siteUrl }: { siteUrl: string }) => {
|
|
|
185
197
|
async (ctx) => {
|
|
186
198
|
const response = await oneTimeToken.endpoints.verifyOneTimeToken({
|
|
187
199
|
...ctx,
|
|
200
|
+
asResponse: false,
|
|
188
201
|
returnHeaders: false,
|
|
189
202
|
returnStatus: false,
|
|
190
203
|
});
|
package/src/react-start/index.ts
CHANGED
|
@@ -65,6 +65,10 @@ const handler = (request: Request, opts: { convexSiteUrl: string }) => {
|
|
|
65
65
|
const headers = new Headers(request.headers);
|
|
66
66
|
headers.set("accept-encoding", "application/json");
|
|
67
67
|
headers.set("host", new URL(opts.convexSiteUrl).host);
|
|
68
|
+
headers.set("x-forwarded-host", requestUrl.host);
|
|
69
|
+
headers.set("x-forwarded-proto", requestUrl.protocol.replace(/:$/, ""));
|
|
70
|
+
headers.set("x-better-auth-forwarded-host", requestUrl.host);
|
|
71
|
+
headers.set("x-better-auth-forwarded-proto", requestUrl.protocol.replace(/:$/, ""));
|
|
68
72
|
return fetch(nextUrl, {
|
|
69
73
|
method: request.method,
|
|
70
74
|
headers,
|