@neutralauth/internal-auth 0.10.11
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 +39 -0
- package/dist/auth-config.d.ts +43 -0
- package/dist/auth-config.d.ts.map +1 -0
- package/dist/auth-config.js +43 -0
- package/dist/auth-config.js.map +1 -0
- package/dist/auth-options.d.ts +3 -0
- package/dist/auth-options.d.ts.map +1 -0
- package/dist/auth-options.js +40 -0
- package/dist/auth-options.js.map +1 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +4 -0
- package/dist/auth.js.map +1 -0
- package/dist/client/adapter-utils.d.ts +66 -0
- package/dist/client/adapter-utils.d.ts.map +1 -0
- package/dist/client/adapter-utils.js +437 -0
- package/dist/client/adapter-utils.js.map +1 -0
- package/dist/client/adapter.d.ts +14 -0
- package/dist/client/adapter.d.ts.map +1 -0
- package/dist/client/adapter.js +274 -0
- package/dist/client/adapter.js.map +1 -0
- package/dist/client/create-api.d.ts +141 -0
- package/dist/client/create-api.d.ts.map +1 -0
- package/dist/client/create-api.js +205 -0
- package/dist/client/create-api.js.map +1 -0
- package/dist/client/create-client.d.ts +183 -0
- package/dist/client/create-client.d.ts.map +1 -0
- package/dist/client/create-client.js +311 -0
- package/dist/client/create-client.js.map +1 -0
- package/dist/client/create-schema.d.ts +19 -0
- package/dist/client/create-schema.d.ts.map +1 -0
- package/dist/client/create-schema.js +114 -0
- package/dist/client/create-schema.js.map +1 -0
- package/dist/client/index.d.ts +7 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +10 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/plugins/index.d.ts +3 -0
- package/dist/client/plugins/index.d.ts.map +1 -0
- package/dist/client/plugins/index.js +3 -0
- package/dist/client/plugins/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +36 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +787 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/adapter.d.ts +130 -0
- package/dist/component/adapter.d.ts.map +1 -0
- package/dist/component/adapter.js +5 -0
- package/dist/component/adapter.js.map +1 -0
- package/dist/component/adapterTest.d.ts +10 -0
- package/dist/component/adapterTest.d.ts.map +1 -0
- package/dist/component/adapterTest.js +409 -0
- package/dist/component/adapterTest.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +4 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/schema.d.ts +474 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +139 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/nextjs/client.d.ts +4 -0
- package/dist/nextjs/client.d.ts.map +1 -0
- package/dist/nextjs/client.js +37 -0
- package/dist/nextjs/client.js.map +1 -0
- package/dist/nextjs/index.d.ts +22 -0
- package/dist/nextjs/index.d.ts.map +1 -0
- package/dist/nextjs/index.js +98 -0
- package/dist/nextjs/index.js.map +1 -0
- package/dist/plugins/convex/client.d.ts +6 -0
- package/dist/plugins/convex/client.d.ts.map +1 -0
- package/dist/plugins/convex/client.js +7 -0
- package/dist/plugins/convex/client.js.map +1 -0
- package/dist/plugins/convex/index.d.ts +322 -0
- package/dist/plugins/convex/index.d.ts.map +1 -0
- package/dist/plugins/convex/index.js +422 -0
- package/dist/plugins/convex/index.js.map +1 -0
- package/dist/plugins/cross-domain/client.d.ts +132 -0
- package/dist/plugins/cross-domain/client.d.ts.map +1 -0
- package/dist/plugins/cross-domain/client.js +192 -0
- package/dist/plugins/cross-domain/client.js.map +1 -0
- package/dist/plugins/cross-domain/index.d.ts +51 -0
- package/dist/plugins/cross-domain/index.d.ts.map +1 -0
- package/dist/plugins/cross-domain/index.js +173 -0
- package/dist/plugins/cross-domain/index.js.map +1 -0
- package/dist/plugins/index.d.ts +3 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +3 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/react/index.d.ts +80 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +190 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react-start/index.d.ts +13 -0
- package/dist/react-start/index.d.ts.map +1 -0
- package/dist/react-start/index.js +101 -0
- package/dist/react-start/index.js.map +1 -0
- package/dist/utils/index.d.ts +33 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +91 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +208 -0
- package/src/auth-config.ts +80 -0
- package/src/auth-options.ts +54 -0
- package/src/auth.ts +4 -0
- package/src/client/adapter-utils.ts +639 -0
- package/src/client/adapter.test.ts +83 -0
- package/src/client/adapter.ts +363 -0
- package/src/client/create-api.ts +339 -0
- package/src/client/create-client.ts +452 -0
- package/src/client/create-schema.ts +166 -0
- package/src/client/index.ts +22 -0
- package/src/client/plugins/index.ts +2 -0
- package/src/component/_generated/api.ts +52 -0
- package/src/component/_generated/component.ts +2008 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +161 -0
- package/src/component/adapter.ts +13 -0
- package/src/component/adapterTest.ts +505 -0
- package/src/component/convex.config.ts +5 -0
- package/src/component/schema.ts +142 -0
- package/src/nextjs/client.tsx +54 -0
- package/src/nextjs/index.ts +152 -0
- package/src/plugins/convex/client.ts +9 -0
- package/src/plugins/convex/index.ts +596 -0
- package/src/plugins/cross-domain/client.test.ts +217 -0
- package/src/plugins/cross-domain/client.ts +234 -0
- package/src/plugins/cross-domain/index.ts +199 -0
- package/src/plugins/index.ts +2 -0
- package/src/react/index.tsx +304 -0
- package/src/react-start/index.ts +153 -0
- package/src/react-start/vite-env.d.ts +2 -0
- package/src/test.ts +18 -0
- package/src/utils/index.ts +171 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { getCookie, getSetCookie, parseSetCookieHeader, crossDomainClient } from "./client.js";
|
|
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
|
+
});
|
|
10
|
+
|
|
11
|
+
it("parses cookie with attributes", () => {
|
|
12
|
+
const header = "session_token=abc123; Path=/; Secure; HttpOnly";
|
|
13
|
+
const map = parseSetCookieHeader(header);
|
|
14
|
+
const cookie = map.get("session_token");
|
|
15
|
+
expect(cookie?.value).toBe("abc123");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("parses multiple cookies", () => {
|
|
19
|
+
const header = "a=1, b=2";
|
|
20
|
+
const map = parseSetCookieHeader(header);
|
|
21
|
+
expect(map.get("a")?.value).toBe("1");
|
|
22
|
+
expect(map.get("b")?.value).toBe("2");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("getSetCookie", () => {
|
|
27
|
+
it("stores expires as ISO string", () => {
|
|
28
|
+
const header = "session_token=abc; Max-Age=3600";
|
|
29
|
+
const result = JSON.parse(getSetCookie(header));
|
|
30
|
+
expect(typeof result.session_token.expires).toBe("string");
|
|
31
|
+
expect(result.session_token.expires).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("stores null expires when no expiry is set", () => {
|
|
35
|
+
const header = "session_token=abc";
|
|
36
|
+
const result = JSON.parse(getSetCookie(header));
|
|
37
|
+
expect(result.session_token.expires).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("merges with previous cookies", () => {
|
|
41
|
+
const prev = JSON.stringify({
|
|
42
|
+
old_cookie: { value: "old", expires: null },
|
|
43
|
+
});
|
|
44
|
+
const header = "new_cookie=new";
|
|
45
|
+
const result = JSON.parse(getSetCookie(header, prev));
|
|
46
|
+
expect(result.old_cookie.value).toBe("old");
|
|
47
|
+
expect(result.new_cookie.value).toBe("new");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("overwrites previous cookies with same name", () => {
|
|
51
|
+
const prev = JSON.stringify({
|
|
52
|
+
token: { value: "old", expires: null },
|
|
53
|
+
});
|
|
54
|
+
const header = "token=new";
|
|
55
|
+
const result = JSON.parse(getSetCookie(header, prev));
|
|
56
|
+
expect(result.token.value).toBe("new");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("survives invalid previous cookie JSON", () => {
|
|
60
|
+
const header = "token=abc";
|
|
61
|
+
const result = JSON.parse(getSetCookie(header, "not-json"));
|
|
62
|
+
expect(result.token.value).toBe("abc");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("getCookie", () => {
|
|
67
|
+
it("returns cookie string for valid cookies", () => {
|
|
68
|
+
const stored = JSON.stringify({
|
|
69
|
+
session: { value: "abc", expires: new Date(Date.now() + 60000).toISOString() },
|
|
70
|
+
});
|
|
71
|
+
const result = getCookie(stored);
|
|
72
|
+
expect(result).toContain("session=abc");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("filters out expired cookies", () => {
|
|
76
|
+
const stored = JSON.stringify({
|
|
77
|
+
expired: { value: "old", expires: new Date(Date.now() - 60000).toISOString() },
|
|
78
|
+
valid: { value: "new", expires: new Date(Date.now() + 60000).toISOString() },
|
|
79
|
+
});
|
|
80
|
+
const result = getCookie(stored);
|
|
81
|
+
expect(result).not.toContain("expired=old");
|
|
82
|
+
expect(result).toContain("valid=new");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("keeps cookies with no expiry", () => {
|
|
86
|
+
const stored = JSON.stringify({
|
|
87
|
+
session: { value: "abc", expires: null },
|
|
88
|
+
});
|
|
89
|
+
const result = getCookie(stored);
|
|
90
|
+
expect(result).toContain("session=abc");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("handles expires after JSON round-trip (string, not Date)", () => {
|
|
94
|
+
// This is the core bug #1 scenario: expires is a string after JSON.parse
|
|
95
|
+
const past = new Date(Date.now() - 60000);
|
|
96
|
+
const stored = JSON.stringify({
|
|
97
|
+
expired: { value: "old", expires: past.toISOString() },
|
|
98
|
+
});
|
|
99
|
+
// After JSON.parse, expires is a string — getCookie must handle this
|
|
100
|
+
const result = getCookie(stored);
|
|
101
|
+
expect(result).not.toContain("expired=old");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns empty string for empty cookie object", () => {
|
|
105
|
+
expect(getCookie("{}")).toBe("");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("returns empty string for invalid JSON", () => {
|
|
109
|
+
expect(getCookie("not-json")).toBe("");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("crossDomainClient", () => {
|
|
114
|
+
let storage: Map<string, string>;
|
|
115
|
+
let mockStorage: { getItem: (key: string) => string | null; setItem: (key: string, value: string) => void };
|
|
116
|
+
const cookieName = "better-auth_cookie";
|
|
117
|
+
const localCacheName = "better-auth_session_data";
|
|
118
|
+
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
storage = new Map<string, string>();
|
|
121
|
+
mockStorage = {
|
|
122
|
+
getItem: (key) => storage.get(key) ?? null,
|
|
123
|
+
setItem: (key, value) => { storage.set(key, value); },
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
function getActions() {
|
|
128
|
+
const plugin = crossDomainClient({ storage: mockStorage });
|
|
129
|
+
const mockStore = {
|
|
130
|
+
notify: () => {},
|
|
131
|
+
atoms: { session: { set: () => {}, get: () => ({}) } },
|
|
132
|
+
};
|
|
133
|
+
return plugin.getActions({} as any, mockStore as any);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getOnSuccessHook() {
|
|
137
|
+
const plugin = crossDomainClient({ storage: mockStorage });
|
|
138
|
+
return plugin.fetchPlugins[0].hooks!.onSuccess!;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
describe("getSessionData", () => {
|
|
142
|
+
it("returns null when storage is empty", () => {
|
|
143
|
+
const actions = getActions();
|
|
144
|
+
expect(actions.getSessionData()).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("returns null for empty object in storage", () => {
|
|
148
|
+
storage.set(localCacheName, "{}");
|
|
149
|
+
const actions = getActions();
|
|
150
|
+
expect(actions.getSessionData()).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("returns parsed session data", () => {
|
|
154
|
+
const sessionData = { session: { id: "123" }, user: { name: "test" } };
|
|
155
|
+
storage.set(localCacheName, JSON.stringify(sessionData));
|
|
156
|
+
const actions = getActions();
|
|
157
|
+
expect(actions.getSessionData()).toEqual(sessionData);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("returns null for stored 'null' string", () => {
|
|
161
|
+
storage.set(localCacheName, "null");
|
|
162
|
+
const actions = getActions();
|
|
163
|
+
expect(actions.getSessionData()).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("returns null for corrupt JSON in storage", () => {
|
|
167
|
+
storage.set(localCacheName, "not-valid-json");
|
|
168
|
+
const actions = getActions();
|
|
169
|
+
expect(actions.getSessionData()).toBeNull();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("onSuccess handler", () => {
|
|
174
|
+
it("clears cookies when get-session returns null", async () => {
|
|
175
|
+
storage.set(cookieName, JSON.stringify({
|
|
176
|
+
"better-auth.session_token": { value: "stale", expires: null },
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
const onSuccess = getOnSuccessHook();
|
|
180
|
+
await onSuccess({
|
|
181
|
+
data: null,
|
|
182
|
+
request: { url: new URL("https://example.com/api/auth/get-session") },
|
|
183
|
+
response: { headers: new Headers() },
|
|
184
|
+
} as any);
|
|
185
|
+
|
|
186
|
+
expect(storage.get(cookieName)).toBe("{}");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("preserves cookies when get-session returns data", async () => {
|
|
190
|
+
const existingCookies = JSON.stringify({
|
|
191
|
+
"better-auth.session_token": { value: "valid", expires: null },
|
|
192
|
+
});
|
|
193
|
+
storage.set(cookieName, existingCookies);
|
|
194
|
+
|
|
195
|
+
const onSuccess = getOnSuccessHook();
|
|
196
|
+
await onSuccess({
|
|
197
|
+
data: { session: { id: "123" }, user: { name: "test" } },
|
|
198
|
+
request: { url: new URL("https://example.com/api/auth/get-session") },
|
|
199
|
+
response: { headers: new Headers() },
|
|
200
|
+
} as any);
|
|
201
|
+
|
|
202
|
+
expect(storage.get(cookieName)).toBe(existingCookies);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("caches session data on get-session", async () => {
|
|
206
|
+
const sessionData = { session: { id: "123" }, user: { name: "test" } };
|
|
207
|
+
const onSuccess = getOnSuccessHook();
|
|
208
|
+
await onSuccess({
|
|
209
|
+
data: sessionData,
|
|
210
|
+
request: { url: new URL("https://example.com/api/auth/get-session") },
|
|
211
|
+
response: { headers: new Headers() },
|
|
212
|
+
} as any);
|
|
213
|
+
|
|
214
|
+
expect(storage.get(localCacheName)).toBe(JSON.stringify(sessionData));
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { BetterAuthClientPlugin, ClientStore } from "better-auth";
|
|
2
|
+
import type { BetterFetchOption } from "@better-fetch/fetch";
|
|
3
|
+
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
|
+
}
|
|
37
|
+
|
|
38
|
+
interface StoredCookie {
|
|
39
|
+
value: string;
|
|
40
|
+
expires: string | 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: expires ? expires.toISOString() : null,
|
|
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 && new Date(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: ClientStore | 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 localStorage !== "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
|
+
* Get the stored session data.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* const sessionData = client.getSessionData();
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
getSessionData: (): Record<string, unknown> | null => {
|
|
153
|
+
const sessionData = storage?.getItem(localCacheName);
|
|
154
|
+
if (!sessionData) return null;
|
|
155
|
+
try {
|
|
156
|
+
const parsed = JSON.parse(sessionData);
|
|
157
|
+
if (parsed && typeof parsed === "object" && Object.keys(parsed).length === 0) return null;
|
|
158
|
+
return parsed;
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
fetchPlugins: [
|
|
166
|
+
{
|
|
167
|
+
id: "convex",
|
|
168
|
+
name: "Convex",
|
|
169
|
+
hooks: {
|
|
170
|
+
async onSuccess(context) {
|
|
171
|
+
if (!storage) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const setCookie = context.response.headers.get(
|
|
175
|
+
"set-better-auth-cookie"
|
|
176
|
+
);
|
|
177
|
+
if (setCookie) {
|
|
178
|
+
const prevCookie = storage.getItem(cookieName);
|
|
179
|
+
const toSetCookie = getSetCookie(
|
|
180
|
+
setCookie || "",
|
|
181
|
+
prevCookie ?? undefined
|
|
182
|
+
);
|
|
183
|
+
await storage.setItem(cookieName, toSetCookie);
|
|
184
|
+
// Only notify on session cookie set
|
|
185
|
+
if (setCookie.includes(".session_token=")) {
|
|
186
|
+
store?.notify("$sessionSignal");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
context.request.url.toString().includes("/get-session") &&
|
|
192
|
+
!opts?.disableCache
|
|
193
|
+
) {
|
|
194
|
+
const data = context.data;
|
|
195
|
+
storage.setItem(localCacheName, JSON.stringify(data));
|
|
196
|
+
if (data === null) {
|
|
197
|
+
storage.setItem(cookieName, "{}");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
async init(url, options) {
|
|
203
|
+
if (!storage) {
|
|
204
|
+
return {
|
|
205
|
+
url,
|
|
206
|
+
options: options as BetterFetchOption,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
options = options || {};
|
|
210
|
+
const storedCookie = storage.getItem(cookieName);
|
|
211
|
+
const cookie = getCookie(storedCookie || "{}");
|
|
212
|
+
options.credentials = "omit";
|
|
213
|
+
options.headers = {
|
|
214
|
+
...options.headers,
|
|
215
|
+
"Better-Auth-Cookie": cookie,
|
|
216
|
+
};
|
|
217
|
+
if (url.includes("/sign-out")) {
|
|
218
|
+
await storage.setItem(cookieName, "{}");
|
|
219
|
+
store?.atoms.session?.set({
|
|
220
|
+
data: null,
|
|
221
|
+
error: null,
|
|
222
|
+
isPending: false,
|
|
223
|
+
});
|
|
224
|
+
storage.setItem(localCacheName, "{}");
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
url,
|
|
228
|
+
options: options as BetterFetchOption,
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
} satisfies BetterAuthClientPlugin;
|
|
234
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { BetterAuthPlugin } from "better-auth";
|
|
2
|
+
import { setSessionCookie } from "better-auth/cookies";
|
|
3
|
+
import { generateRandomString } from "better-auth/crypto";
|
|
4
|
+
import {
|
|
5
|
+
createAuthEndpoint,
|
|
6
|
+
createAuthMiddleware,
|
|
7
|
+
oneTimeToken as oneTimeTokenPlugin,
|
|
8
|
+
} from "better-auth/plugins";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
export const crossDomain = ({ siteUrl }: { siteUrl: string }) => {
|
|
12
|
+
const oneTimeToken = oneTimeTokenPlugin();
|
|
13
|
+
|
|
14
|
+
const rewriteCallbackURL = (callbackURL?: string) => {
|
|
15
|
+
if (callbackURL && !callbackURL.startsWith("/")) {
|
|
16
|
+
return callbackURL;
|
|
17
|
+
}
|
|
18
|
+
const relativeCallbackURL = callbackURL || "/";
|
|
19
|
+
return new URL(relativeCallbackURL, siteUrl).toString();
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const isExpoNative = (ctx: { headers?: Headers }) => {
|
|
23
|
+
return ctx.headers?.has("expo-origin");
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
id: "cross-domain",
|
|
28
|
+
// TODO: remove this in the next minor release, it doesn't
|
|
29
|
+
// actually affect ctx.trustedOrigins. cors allowedOrigins
|
|
30
|
+
// is using it, via options.trustedOrigins, though, so it's
|
|
31
|
+
// a breaking change.
|
|
32
|
+
init() {
|
|
33
|
+
return {
|
|
34
|
+
options: {
|
|
35
|
+
trustedOrigins: [siteUrl],
|
|
36
|
+
},
|
|
37
|
+
context: {
|
|
38
|
+
oauthConfig: {
|
|
39
|
+
storeStateStrategy: "database",
|
|
40
|
+
// We could fake the cookie by sending a header, but it would need
|
|
41
|
+
// to be set on a 302 redirect from the identity provider, and we
|
|
42
|
+
// don't have a way to do that. This only means we can't stop an
|
|
43
|
+
// oauth flow that started in one browser from continuing in
|
|
44
|
+
// another. We still verify the state token from the query string
|
|
45
|
+
// against the database.
|
|
46
|
+
skipStateCookieCheck: true,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
hooks: {
|
|
52
|
+
before: [
|
|
53
|
+
{
|
|
54
|
+
matcher(ctx) {
|
|
55
|
+
return (
|
|
56
|
+
Boolean(
|
|
57
|
+
ctx.request?.headers.get("better-auth-cookie") ||
|
|
58
|
+
ctx.headers?.get("better-auth-cookie")
|
|
59
|
+
) && !isExpoNative(ctx)
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
63
|
+
const existingHeaders = (ctx.request?.headers ||
|
|
64
|
+
ctx.headers) as Headers;
|
|
65
|
+
const headers = new Headers({
|
|
66
|
+
...Object.fromEntries(existingHeaders?.entries()),
|
|
67
|
+
});
|
|
68
|
+
// Skip if the request has an authorization header
|
|
69
|
+
if (headers.get("authorization")) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const cookie = headers.get("better-auth-cookie");
|
|
73
|
+
if (!cookie) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
headers.append("cookie", cookie);
|
|
77
|
+
return {
|
|
78
|
+
context: {
|
|
79
|
+
headers,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
matcher: (ctx) => {
|
|
86
|
+
return (
|
|
87
|
+
ctx.method === "GET" &&
|
|
88
|
+
ctx.path.startsWith("/verify-email") &&
|
|
89
|
+
!isExpoNative(ctx)
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
93
|
+
if (ctx.query?.callbackURL) {
|
|
94
|
+
ctx.query.callbackURL = rewriteCallbackURL(ctx.query.callbackURL);
|
|
95
|
+
}
|
|
96
|
+
return { context: ctx };
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
matcher: (ctx) => {
|
|
101
|
+
return (
|
|
102
|
+
((ctx.method === "POST" && ctx.path.startsWith("/link-social")) ||
|
|
103
|
+
ctx.path.startsWith("/send-verification-email") ||
|
|
104
|
+
ctx.path.startsWith("/sign-in/email") ||
|
|
105
|
+
ctx.path.startsWith("/sign-in/social") ||
|
|
106
|
+
ctx.path.startsWith("/sign-in/magic-link") ||
|
|
107
|
+
ctx.path.startsWith("/delete-user") ||
|
|
108
|
+
ctx.path.startsWith("/change-email")) &&
|
|
109
|
+
!isExpoNative(ctx)
|
|
110
|
+
);
|
|
111
|
+
},
|
|
112
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
113
|
+
const isSignIn = ctx.path.startsWith("/sign-in");
|
|
114
|
+
ctx.body.callbackURL = rewriteCallbackURL(ctx.body.callbackURL);
|
|
115
|
+
if (isSignIn && ctx.body.newUserCallbackURL) {
|
|
116
|
+
ctx.body.newUserCallbackURL = rewriteCallbackURL(
|
|
117
|
+
ctx.body.newUserCallbackURL
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (isSignIn && ctx.body.errorCallbackURL) {
|
|
121
|
+
ctx.body.errorCallbackURL = rewriteCallbackURL(
|
|
122
|
+
ctx.body.errorCallbackURL
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return { context: ctx };
|
|
126
|
+
}),
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
after: [
|
|
130
|
+
{
|
|
131
|
+
matcher(ctx) {
|
|
132
|
+
return !isExpoNative(ctx);
|
|
133
|
+
},
|
|
134
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
135
|
+
const setCookie = ctx.context.responseHeaders?.get("set-cookie");
|
|
136
|
+
if (!setCookie) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
ctx.context.responseHeaders?.delete("set-cookie");
|
|
140
|
+
ctx.setHeader("Set-Better-Auth-Cookie", setCookie);
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
matcher: (ctx) => {
|
|
145
|
+
return (
|
|
146
|
+
(ctx.path?.startsWith("/callback") ||
|
|
147
|
+
ctx.path?.startsWith("/oauth2/callback") ||
|
|
148
|
+
ctx.path?.startsWith("/magic-link/verify")) &&
|
|
149
|
+
!isExpoNative(ctx)
|
|
150
|
+
);
|
|
151
|
+
},
|
|
152
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
153
|
+
// Mostly copied from the one-time-token plugin
|
|
154
|
+
const session = ctx.context.newSession;
|
|
155
|
+
if (!session) {
|
|
156
|
+
ctx.context.logger.error("No session found");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const token = generateRandomString(32);
|
|
160
|
+
const expiresAt = new Date(Date.now() + 3 * 60 * 1000);
|
|
161
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
162
|
+
value: session.session.token,
|
|
163
|
+
identifier: `one-time-token:${token}`,
|
|
164
|
+
expiresAt,
|
|
165
|
+
});
|
|
166
|
+
const redirectTo = ctx.context.responseHeaders?.get("location");
|
|
167
|
+
if (!redirectTo) {
|
|
168
|
+
ctx.context.logger.error("No redirect to found");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const url = new URL(redirectTo);
|
|
172
|
+
url.searchParams.set("ott", token);
|
|
173
|
+
throw ctx.redirect(url.toString());
|
|
174
|
+
}),
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
endpoints: {
|
|
179
|
+
verifyOneTimeToken: createAuthEndpoint(
|
|
180
|
+
"/cross-domain/one-time-token/verify",
|
|
181
|
+
{
|
|
182
|
+
method: "POST",
|
|
183
|
+
body: z.object({
|
|
184
|
+
token: z.string(),
|
|
185
|
+
}),
|
|
186
|
+
},
|
|
187
|
+
async (ctx) => {
|
|
188
|
+
const response = await oneTimeToken.endpoints.verifyOneTimeToken({
|
|
189
|
+
...ctx,
|
|
190
|
+
returnHeaders: false,
|
|
191
|
+
returnStatus: false,
|
|
192
|
+
});
|
|
193
|
+
await setSessionCookie(ctx, response);
|
|
194
|
+
return response;
|
|
195
|
+
}
|
|
196
|
+
),
|
|
197
|
+
},
|
|
198
|
+
} satisfies BetterAuthPlugin;
|
|
199
|
+
};
|