@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.
Files changed (147) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +39 -0
  3. package/dist/auth-config.d.ts +43 -0
  4. package/dist/auth-config.d.ts.map +1 -0
  5. package/dist/auth-config.js +43 -0
  6. package/dist/auth-config.js.map +1 -0
  7. package/dist/auth-options.d.ts +3 -0
  8. package/dist/auth-options.d.ts.map +1 -0
  9. package/dist/auth-options.js +40 -0
  10. package/dist/auth-options.js.map +1 -0
  11. package/dist/auth.d.ts +2 -0
  12. package/dist/auth.d.ts.map +1 -0
  13. package/dist/auth.js +4 -0
  14. package/dist/auth.js.map +1 -0
  15. package/dist/client/adapter-utils.d.ts +66 -0
  16. package/dist/client/adapter-utils.d.ts.map +1 -0
  17. package/dist/client/adapter-utils.js +437 -0
  18. package/dist/client/adapter-utils.js.map +1 -0
  19. package/dist/client/adapter.d.ts +14 -0
  20. package/dist/client/adapter.d.ts.map +1 -0
  21. package/dist/client/adapter.js +274 -0
  22. package/dist/client/adapter.js.map +1 -0
  23. package/dist/client/create-api.d.ts +141 -0
  24. package/dist/client/create-api.d.ts.map +1 -0
  25. package/dist/client/create-api.js +205 -0
  26. package/dist/client/create-api.js.map +1 -0
  27. package/dist/client/create-client.d.ts +183 -0
  28. package/dist/client/create-client.d.ts.map +1 -0
  29. package/dist/client/create-client.js +311 -0
  30. package/dist/client/create-client.js.map +1 -0
  31. package/dist/client/create-schema.d.ts +19 -0
  32. package/dist/client/create-schema.d.ts.map +1 -0
  33. package/dist/client/create-schema.js +114 -0
  34. package/dist/client/create-schema.js.map +1 -0
  35. package/dist/client/index.d.ts +7 -0
  36. package/dist/client/index.d.ts.map +1 -0
  37. package/dist/client/index.js +10 -0
  38. package/dist/client/index.js.map +1 -0
  39. package/dist/client/plugins/index.d.ts +3 -0
  40. package/dist/client/plugins/index.d.ts.map +1 -0
  41. package/dist/client/plugins/index.js +3 -0
  42. package/dist/client/plugins/index.js.map +1 -0
  43. package/dist/component/_generated/api.d.ts +36 -0
  44. package/dist/component/_generated/api.d.ts.map +1 -0
  45. package/dist/component/_generated/api.js +31 -0
  46. package/dist/component/_generated/api.js.map +1 -0
  47. package/dist/component/_generated/component.d.ts +787 -0
  48. package/dist/component/_generated/component.d.ts.map +1 -0
  49. package/dist/component/_generated/component.js +11 -0
  50. package/dist/component/_generated/component.js.map +1 -0
  51. package/dist/component/_generated/dataModel.d.ts +46 -0
  52. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  53. package/dist/component/_generated/dataModel.js +11 -0
  54. package/dist/component/_generated/dataModel.js.map +1 -0
  55. package/dist/component/_generated/server.d.ts +121 -0
  56. package/dist/component/_generated/server.d.ts.map +1 -0
  57. package/dist/component/_generated/server.js +78 -0
  58. package/dist/component/_generated/server.js.map +1 -0
  59. package/dist/component/adapter.d.ts +130 -0
  60. package/dist/component/adapter.d.ts.map +1 -0
  61. package/dist/component/adapter.js +5 -0
  62. package/dist/component/adapter.js.map +1 -0
  63. package/dist/component/adapterTest.d.ts +10 -0
  64. package/dist/component/adapterTest.d.ts.map +1 -0
  65. package/dist/component/adapterTest.js +409 -0
  66. package/dist/component/adapterTest.js.map +1 -0
  67. package/dist/component/convex.config.d.ts +3 -0
  68. package/dist/component/convex.config.d.ts.map +1 -0
  69. package/dist/component/convex.config.js +4 -0
  70. package/dist/component/convex.config.js.map +1 -0
  71. package/dist/component/schema.d.ts +474 -0
  72. package/dist/component/schema.d.ts.map +1 -0
  73. package/dist/component/schema.js +139 -0
  74. package/dist/component/schema.js.map +1 -0
  75. package/dist/nextjs/client.d.ts +4 -0
  76. package/dist/nextjs/client.d.ts.map +1 -0
  77. package/dist/nextjs/client.js +37 -0
  78. package/dist/nextjs/client.js.map +1 -0
  79. package/dist/nextjs/index.d.ts +22 -0
  80. package/dist/nextjs/index.d.ts.map +1 -0
  81. package/dist/nextjs/index.js +98 -0
  82. package/dist/nextjs/index.js.map +1 -0
  83. package/dist/plugins/convex/client.d.ts +6 -0
  84. package/dist/plugins/convex/client.d.ts.map +1 -0
  85. package/dist/plugins/convex/client.js +7 -0
  86. package/dist/plugins/convex/client.js.map +1 -0
  87. package/dist/plugins/convex/index.d.ts +322 -0
  88. package/dist/plugins/convex/index.d.ts.map +1 -0
  89. package/dist/plugins/convex/index.js +422 -0
  90. package/dist/plugins/convex/index.js.map +1 -0
  91. package/dist/plugins/cross-domain/client.d.ts +132 -0
  92. package/dist/plugins/cross-domain/client.d.ts.map +1 -0
  93. package/dist/plugins/cross-domain/client.js +192 -0
  94. package/dist/plugins/cross-domain/client.js.map +1 -0
  95. package/dist/plugins/cross-domain/index.d.ts +51 -0
  96. package/dist/plugins/cross-domain/index.d.ts.map +1 -0
  97. package/dist/plugins/cross-domain/index.js +173 -0
  98. package/dist/plugins/cross-domain/index.js.map +1 -0
  99. package/dist/plugins/index.d.ts +3 -0
  100. package/dist/plugins/index.d.ts.map +1 -0
  101. package/dist/plugins/index.js +3 -0
  102. package/dist/plugins/index.js.map +1 -0
  103. package/dist/react/index.d.ts +80 -0
  104. package/dist/react/index.d.ts.map +1 -0
  105. package/dist/react/index.js +190 -0
  106. package/dist/react/index.js.map +1 -0
  107. package/dist/react-start/index.d.ts +13 -0
  108. package/dist/react-start/index.d.ts.map +1 -0
  109. package/dist/react-start/index.js +101 -0
  110. package/dist/react-start/index.js.map +1 -0
  111. package/dist/utils/index.d.ts +33 -0
  112. package/dist/utils/index.d.ts.map +1 -0
  113. package/dist/utils/index.js +91 -0
  114. package/dist/utils/index.js.map +1 -0
  115. package/package.json +208 -0
  116. package/src/auth-config.ts +80 -0
  117. package/src/auth-options.ts +54 -0
  118. package/src/auth.ts +4 -0
  119. package/src/client/adapter-utils.ts +639 -0
  120. package/src/client/adapter.test.ts +83 -0
  121. package/src/client/adapter.ts +363 -0
  122. package/src/client/create-api.ts +339 -0
  123. package/src/client/create-client.ts +452 -0
  124. package/src/client/create-schema.ts +166 -0
  125. package/src/client/index.ts +22 -0
  126. package/src/client/plugins/index.ts +2 -0
  127. package/src/component/_generated/api.ts +52 -0
  128. package/src/component/_generated/component.ts +2008 -0
  129. package/src/component/_generated/dataModel.ts +60 -0
  130. package/src/component/_generated/server.ts +161 -0
  131. package/src/component/adapter.ts +13 -0
  132. package/src/component/adapterTest.ts +505 -0
  133. package/src/component/convex.config.ts +5 -0
  134. package/src/component/schema.ts +142 -0
  135. package/src/nextjs/client.tsx +54 -0
  136. package/src/nextjs/index.ts +152 -0
  137. package/src/plugins/convex/client.ts +9 -0
  138. package/src/plugins/convex/index.ts +596 -0
  139. package/src/plugins/cross-domain/client.test.ts +217 -0
  140. package/src/plugins/cross-domain/client.ts +234 -0
  141. package/src/plugins/cross-domain/index.ts +199 -0
  142. package/src/plugins/index.ts +2 -0
  143. package/src/react/index.tsx +304 -0
  144. package/src/react-start/index.ts +153 -0
  145. package/src/react-start/vite-env.d.ts +2 -0
  146. package/src/test.ts +18 -0
  147. 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
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./convex/index.js";
2
+ export * from "./cross-domain/index.js";