@better-auth/expo 0.6.3-beta.4

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 (45) hide show
  1. package/.turbo/turbo-build.log +25 -0
  2. package/dist/client.d.mts +136 -0
  3. package/dist/client.d.ts +136 -0
  4. package/dist/client.js +1 -0
  5. package/dist/client.mjs +1 -0
  6. package/dist/index.d.mts +22 -0
  7. package/dist/index.d.ts +22 -0
  8. package/dist/index.js +1 -0
  9. package/dist/index.mjs +1 -0
  10. package/examples/tanstack-example/node_modules/better-sqlite3/LICENSE +21 -0
  11. package/examples/tanstack-example/node_modules/better-sqlite3/README.md +92 -0
  12. package/examples/tanstack-example/node_modules/better-sqlite3/binding.gyp +38 -0
  13. package/examples/tanstack-example/node_modules/better-sqlite3/build/Release/better_sqlite3.node +0 -0
  14. package/examples/tanstack-example/node_modules/better-sqlite3/deps/common.gypi +68 -0
  15. package/examples/tanstack-example/node_modules/better-sqlite3/deps/copy.js +31 -0
  16. package/examples/tanstack-example/node_modules/better-sqlite3/deps/defines.gypi +41 -0
  17. package/examples/tanstack-example/node_modules/better-sqlite3/deps/download.sh +122 -0
  18. package/examples/tanstack-example/node_modules/better-sqlite3/deps/patches/1208.patch +15 -0
  19. package/examples/tanstack-example/node_modules/better-sqlite3/deps/sqlite3/sqlite3.c +260455 -0
  20. package/examples/tanstack-example/node_modules/better-sqlite3/deps/sqlite3/sqlite3.h +13574 -0
  21. package/examples/tanstack-example/node_modules/better-sqlite3/deps/sqlite3/sqlite3ext.h +719 -0
  22. package/examples/tanstack-example/node_modules/better-sqlite3/deps/sqlite3.gyp +80 -0
  23. package/examples/tanstack-example/node_modules/better-sqlite3/deps/test_extension.c +21 -0
  24. package/examples/tanstack-example/node_modules/better-sqlite3/lib/database.js +90 -0
  25. package/examples/tanstack-example/node_modules/better-sqlite3/lib/index.js +3 -0
  26. package/examples/tanstack-example/node_modules/better-sqlite3/lib/methods/aggregate.js +43 -0
  27. package/examples/tanstack-example/node_modules/better-sqlite3/lib/methods/backup.js +67 -0
  28. package/examples/tanstack-example/node_modules/better-sqlite3/lib/methods/function.js +31 -0
  29. package/examples/tanstack-example/node_modules/better-sqlite3/lib/methods/inspect.js +7 -0
  30. package/examples/tanstack-example/node_modules/better-sqlite3/lib/methods/pragma.js +12 -0
  31. package/examples/tanstack-example/node_modules/better-sqlite3/lib/methods/serialize.js +16 -0
  32. package/examples/tanstack-example/node_modules/better-sqlite3/lib/methods/table.js +189 -0
  33. package/examples/tanstack-example/node_modules/better-sqlite3/lib/methods/transaction.js +75 -0
  34. package/examples/tanstack-example/node_modules/better-sqlite3/lib/methods/wrappers.js +54 -0
  35. package/examples/tanstack-example/node_modules/better-sqlite3/lib/sqlite-error.js +20 -0
  36. package/examples/tanstack-example/node_modules/better-sqlite3/lib/util.js +12 -0
  37. package/examples/tanstack-example/node_modules/better-sqlite3/package.json +54 -0
  38. package/examples/tanstack-example/node_modules/better-sqlite3/src/better_sqlite3.cpp +2165 -0
  39. package/examples/tanstack-example/node_modules/better-sqlite3/src/better_sqlite3.hpp +1036 -0
  40. package/package.json +42 -0
  41. package/src/client.ts +198 -0
  42. package/src/expo.test.ts +133 -0
  43. package/src/index.ts +55 -0
  44. package/tsconfig.json +20 -0
  45. package/tsup.config.ts +13 -0
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@better-auth/expo",
3
+ "version": "0.6.3-beta.4",
4
+ "description": "",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js"
12
+ },
13
+ "./client": {
14
+ "types": "./dist/client.d.ts",
15
+ "import": "./dist/client.mjs",
16
+ "require": "./dist/client.js"
17
+ }
18
+ },
19
+ "keywords": [],
20
+ "author": "",
21
+ "license": "ISC",
22
+ "devDependencies": {
23
+ "better-sqlite3": "^11.5.0",
24
+ "expo-constants": "~16.0.2",
25
+ "expo-linking": "~6.3.1",
26
+ "expo-secure-store": "~13.0.2",
27
+ "expo-web-browser": "~13.0.3",
28
+ "vitest": "^1.6.0",
29
+ "better-auth": "0.6.3-beta.4"
30
+ },
31
+ "dependencies": {
32
+ "nanostores": "^0.11.2"
33
+ },
34
+ "peerDependencies": {
35
+ "better-auth": "0.6.3-beta.4"
36
+ },
37
+ "scripts": {
38
+ "test": "vitest",
39
+ "build": "tsup --dts --minify --clean",
40
+ "dev": "tsup --watch --sourcemap --dts"
41
+ }
42
+ }
package/src/client.ts ADDED
@@ -0,0 +1,198 @@
1
+ import type { BetterAuthClientPlugin, Store } from "better-auth";
2
+ import * as Browser from "expo-web-browser";
3
+ import * as Linking from "expo-linking";
4
+ import { Platform } from "react-native";
5
+ import * as SecureStore from "expo-secure-store";
6
+ import Constants from "expo-constants";
7
+
8
+ interface CookieAttributes {
9
+ value: string;
10
+ expires?: Date;
11
+ "max-age"?: number;
12
+ domain?: string;
13
+ path?: string;
14
+ secure?: boolean;
15
+ httpOnly?: boolean;
16
+ sameSite?: "Strict" | "Lax" | "None";
17
+ }
18
+
19
+ function parseSetCookieHeader(header: string): Map<string, CookieAttributes> {
20
+ const cookieMap = new Map<string, CookieAttributes>();
21
+ const cookies = header.split(", ");
22
+ cookies.forEach((cookie) => {
23
+ const [nameValue, ...attributes] = cookie.split("; ");
24
+ const [name, value] = nameValue.split("=");
25
+
26
+ const cookieObj: CookieAttributes = { value };
27
+
28
+ attributes.forEach((attr) => {
29
+ const [attrName, attrValue] = attr.split("=");
30
+ cookieObj[attrName.toLowerCase() as "value"] = attrValue;
31
+ });
32
+
33
+ cookieMap.set(name, cookieObj);
34
+ });
35
+
36
+ return cookieMap;
37
+ }
38
+
39
+ interface ExpoClientOptions {
40
+ scheme?: string;
41
+ storage?: {
42
+ setItem: (key: string, value: string) => any;
43
+ getItem: (key: string) => string | null;
44
+ };
45
+ storagePrefix?: string;
46
+ disableCache?: boolean;
47
+ }
48
+
49
+ interface StoredCookie {
50
+ value: string;
51
+ expires: Date | null;
52
+ }
53
+
54
+ function getSetCookie(header: string) {
55
+ const parsed = parseSetCookieHeader(header);
56
+ const toSetCookie: Record<string, StoredCookie> = {};
57
+ parsed.forEach((cookie, key) => {
58
+ const expiresAt = cookie["expires"];
59
+ const maxAge = cookie["max-age"];
60
+ const expires = expiresAt
61
+ ? new Date(String(expiresAt))
62
+ : maxAge
63
+ ? new Date(Date.now() + Number(maxAge))
64
+ : null;
65
+ toSetCookie[key] = {
66
+ value: cookie["value"],
67
+ expires,
68
+ };
69
+ });
70
+ return JSON.stringify(toSetCookie);
71
+ }
72
+
73
+ function getCookie(cookie: string) {
74
+ let parsed = {} as Record<string, StoredCookie>;
75
+ try {
76
+ parsed = JSON.parse(cookie) as Record<string, StoredCookie>;
77
+ } catch (e) {}
78
+ const toSend = Object.entries(parsed).reduce((acc, [key, value]) => {
79
+ if (value.expires && value.expires < new Date()) {
80
+ return acc;
81
+ }
82
+ return `${acc}; ${key}=${value.value}`;
83
+ }, "");
84
+ return toSend;
85
+ }
86
+
87
+ function getOrigin(scheme: string) {
88
+ const schemeURI = Linking.createURL("", { scheme });
89
+ return schemeURI;
90
+ }
91
+
92
+ export const expoClient = (opts: ExpoClientOptions) => {
93
+ let store: Store | null = null;
94
+ const cookieName = `${opts.storagePrefix || "better-auth"}_cookie`;
95
+ const localCacheName = `${opts.storagePrefix || "better-auth"}_session_data`;
96
+ const storage = opts.storage || SecureStore;
97
+ const scheme = opts.scheme || Constants.platform?.scheme;
98
+ const isWeb = Platform.OS === "web";
99
+ if (!scheme && !isWeb) {
100
+ throw new Error(
101
+ "Scheme not found in app.json. Please provide a scheme in the options.",
102
+ );
103
+ }
104
+ return {
105
+ id: "expo",
106
+ getActions(_, $store) {
107
+ if (Platform.OS === "web") return {};
108
+ store = $store;
109
+ const localSession = storage.getItem(cookieName);
110
+ localSession &&
111
+ $store.atoms.session.set({
112
+ data: JSON.parse(localSession),
113
+ error: null,
114
+ isPending: false,
115
+ });
116
+ return {};
117
+ },
118
+ fetchPlugins: [
119
+ {
120
+ id: "expo",
121
+ name: "Expo",
122
+ hooks: {
123
+ async onSuccess(context) {
124
+ if (isWeb) return;
125
+ const setCookie = context.response.headers.get("set-cookie");
126
+ if (setCookie) {
127
+ const toSetCookie = getSetCookie(setCookie || "");
128
+ await storage.setItem(cookieName, toSetCookie);
129
+ store?.notify("$sessionSignal");
130
+ }
131
+
132
+ if (
133
+ context.request.url.toString().includes("/get-session") &&
134
+ !opts.disableCache
135
+ ) {
136
+ const data = context.data;
137
+ storage.setItem(localCacheName, JSON.stringify(data));
138
+ }
139
+
140
+ if (
141
+ context.data.redirect &&
142
+ context.request.url.toString().includes("/sign-in")
143
+ ) {
144
+ const callbackURL = JSON.parse(context.request.body)?.callbackURL;
145
+ const to = callbackURL;
146
+ const signInURL = context.data?.url;
147
+ const result = await Browser.openAuthSessionAsync(signInURL, to);
148
+ if (result.type !== "success") return;
149
+ const url = new URL(result.url);
150
+ const cookie = String(url.searchParams.get("cookie"));
151
+ if (!cookie) return;
152
+ storage.setItem(cookieName, getSetCookie(cookie));
153
+ store?.notify("$sessionSignal");
154
+ }
155
+ },
156
+ },
157
+ async init(url, options) {
158
+ if (isWeb) {
159
+ return {
160
+ url,
161
+ options,
162
+ };
163
+ }
164
+ options = options || {};
165
+ const storedCookie = storage.getItem(cookieName);
166
+ const cookie = getCookie(storedCookie || "{}");
167
+ options.credentials = "omit";
168
+ options.headers = {
169
+ ...options.headers,
170
+ cookie,
171
+ origin: getOrigin(scheme!),
172
+ };
173
+ if (options.body?.callbackURL) {
174
+ if (options.body.callbackURL.startsWith("/")) {
175
+ const url = Linking.createURL(options.body.callbackURL, {
176
+ scheme,
177
+ });
178
+ options.body.callbackURL = url;
179
+ }
180
+ }
181
+ if (url.includes("/sign-out")) {
182
+ await storage.setItem(cookieName, "{}");
183
+ store?.atoms.session?.set({
184
+ data: null,
185
+ error: null,
186
+ isPending: false,
187
+ });
188
+ storage.setItem(localCacheName, "{}");
189
+ }
190
+ return {
191
+ url,
192
+ options,
193
+ };
194
+ },
195
+ },
196
+ ],
197
+ } satisfies BetterAuthClientPlugin;
198
+ };
@@ -0,0 +1,133 @@
1
+ import { createAuthClient } from "better-auth/client";
2
+ import Database from "better-sqlite3";
3
+ import { beforeAll, describe, expect, it, vi } from "vitest";
4
+ import { expo } from ".";
5
+ import { expoClient } from "./client";
6
+ import { betterAuth } from "better-auth";
7
+ import { getMigrations } from "better-auth/db";
8
+
9
+ vi.mock("expo-web-browser", async () => {
10
+ return {
11
+ openAuthSessionAsync: vi.fn(async (...args) => {
12
+ fn(...args);
13
+ return {
14
+ type: "success",
15
+ url: "better-auth://?cookie=better-auth.session_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYxMzQwZj",
16
+ };
17
+ }),
18
+ };
19
+ });
20
+
21
+ vi.mock("react-native", async () => {
22
+ return {
23
+ Platform: {
24
+ OS: "android",
25
+ },
26
+ };
27
+ });
28
+
29
+ vi.mock("expo-constants", async () => {
30
+ return {
31
+ default: {
32
+ platform: {
33
+ scheme: "better-auth",
34
+ },
35
+ },
36
+ };
37
+ });
38
+
39
+ vi.mock("expo-linking", async () => {
40
+ return {
41
+ createURL: vi.fn((url) => `better-auth://${url}`),
42
+ };
43
+ });
44
+
45
+ vi.mock("expo-secure-store", async () => {
46
+ return {
47
+ getItemAsync: vi.fn(async (key) => null),
48
+ setItemAsync: vi.fn(),
49
+ deleteItemAsync: vi.fn(),
50
+ };
51
+ });
52
+
53
+ const fn = vi.fn();
54
+
55
+ describe("expo", async () => {
56
+ const storage = new Map<string, string>();
57
+
58
+ const auth = betterAuth({
59
+ baseURL: "http://localhost:3000",
60
+ database: new Database(":memory:"),
61
+ emailAndPassword: {
62
+ enabled: true,
63
+ },
64
+ socialProviders: {
65
+ google: {
66
+ clientId: "test",
67
+ clientSecret: "test",
68
+ },
69
+ },
70
+ plugins: [expo()],
71
+ trustedOrigins: ["better-auth://"],
72
+ });
73
+
74
+ const client = createAuthClient({
75
+ baseURL: "http://localhost:3000",
76
+ fetchOptions: {
77
+ customFetchImpl: (url, init) => {
78
+ const req = new Request(url.toString(), init);
79
+ return auth.handler(req);
80
+ },
81
+ },
82
+ plugins: [
83
+ expoClient({
84
+ storage: {
85
+ getItem: (key) => storage.get(key) || null,
86
+ setItem: async (key, value) => storage.set(key, value),
87
+ },
88
+ }),
89
+ ],
90
+ });
91
+ beforeAll(async () => {
92
+ const { runMigrations } = await getMigrations(auth.options);
93
+ await runMigrations();
94
+ });
95
+
96
+ it("should store cookie with expires date", async () => {
97
+ const testUser = {
98
+ email: "test@test.com",
99
+ password: "password",
100
+ name: "Test User",
101
+ };
102
+ await client.signUp.email(testUser);
103
+ const storedCookie = storage.get("better-auth_cookie");
104
+ expect(storedCookie).toBeDefined();
105
+ const parsedCookie = JSON.parse(storedCookie || "");
106
+ expect(parsedCookie["better-auth.session_token"]).toMatchObject({
107
+ value: expect.any(String),
108
+ expires: expect.any(String),
109
+ });
110
+ });
111
+
112
+ it("should send cookie and get session", async () => {
113
+ const { data } = await client.getSession();
114
+ expect(data).toMatchObject({
115
+ session: expect.any(Object),
116
+ user: expect.any(Object),
117
+ });
118
+ });
119
+
120
+ it("should use the scheme to open the browser", async () => {
121
+ const { data: res } = await client.signIn.social({
122
+ provider: "google",
123
+ callbackURL: "/dashboard",
124
+ });
125
+ expect(res).toMatchObject({
126
+ url: expect.stringContaining("accounts.google"),
127
+ });
128
+ expect(fn).toHaveBeenCalledWith(
129
+ expect.stringContaining("accounts.google"),
130
+ "better-auth:///dashboard",
131
+ );
132
+ });
133
+ });
package/src/index.ts ADDED
@@ -0,0 +1,55 @@
1
+ import type { BetterAuthPlugin } from "better-auth";
2
+
3
+ export const expo = () => {
4
+ return {
5
+ id: "expo",
6
+ init: (ctx) => {
7
+ const trustedOrigins =
8
+ process.env.NODE_ENV === "development"
9
+ ? [...(ctx.options.trustedOrigins || []), "exp://"]
10
+ : ctx.options.trustedOrigins;
11
+ return {
12
+ options: {
13
+ trustedOrigins,
14
+ },
15
+ };
16
+ },
17
+ hooks: {
18
+ after: [
19
+ {
20
+ matcher(context) {
21
+ return context.path?.startsWith("/callback");
22
+ },
23
+ handler: async (ctx) => {
24
+ const response = ctx.context.returned as Response;
25
+ if (response.status === 302) {
26
+ const location = response.headers.get("location");
27
+ if (!location) {
28
+ return;
29
+ }
30
+ const trustedOrigins = ctx.context.trustedOrigins.filter(
31
+ (origin) => !origin.startsWith("http"),
32
+ );
33
+ const isTrustedOrigin = trustedOrigins.some((origin) =>
34
+ location?.startsWith(origin),
35
+ );
36
+ if (!isTrustedOrigin) {
37
+ return;
38
+ }
39
+ const cookie = response.headers.get("set-cookie");
40
+ if (!cookie) {
41
+ return;
42
+ }
43
+ const url = new URL(location);
44
+ url.searchParams.set("cookie", cookie);
45
+ response.headers.set("location", url.toString());
46
+ return {
47
+ response,
48
+ };
49
+ }
50
+ },
51
+ },
52
+ ],
53
+ },
54
+ } satisfies BetterAuthPlugin;
55
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "esModuleInterop": true,
4
+ "skipLibCheck": true,
5
+ "target": "es2022",
6
+ "allowJs": true,
7
+ "resolveJsonModule": true,
8
+ "module": "ESNext",
9
+ "noEmit": true,
10
+ "moduleResolution": "Bundler",
11
+ "moduleDetection": "force",
12
+ "isolatedModules": true,
13
+ "verbatimModuleSyntax": true,
14
+ "strict": true,
15
+ "noImplicitOverride": true,
16
+ "noFallthroughCasesInSwitch": true
17
+ },
18
+ "exclude": ["node_modules"],
19
+ "include": ["src"]
20
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig((env) => {
4
+ return {
5
+ entry: {
6
+ index: "src/index.ts",
7
+ client: "src/client.ts",
8
+ },
9
+ format: ["esm", "cjs"],
10
+ bundle: true,
11
+ skipNodeModulesBundle: true,
12
+ };
13
+ });