@missionmind/auth-front 0.0.0 → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MissionMind
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # @missionmind/auth-front
2
+
3
+ Better Auth in an Expo app whose users start anonymous on a device and sign in
4
+ later. The server side is [`@missionmind/auth-api`](https://www.npmjs.com/package/@missionmind/auth-api).
5
+
6
+ ```sh
7
+ npx expo install @missionmind/auth-front expo-secure-store expo-crypto \
8
+ expo-constants expo-web-browser expo-apple-authentication \
9
+ @react-native-google-signin/google-signin @legendapp/state
10
+ ```
11
+
12
+ The native modules, `@legendapp/state` and `react` are peer dependencies the
13
+ app installs itself.
14
+
15
+ ## What it does
16
+
17
+ - **One account per device, created once.** `ensureAccount()` signs the
18
+ device in anonymously (at the end of onboarding, say); `ensureSession()` and
19
+ `recoverSession()` only ever bring back the account it holds, never create
20
+ one. Concurrent callers share one run, with retries and backoff.
21
+ - **A device token in the keychain** (`<prefix>.device-token`, issued per
22
+ device id) gets a new session whenever the last one died. If the account is
23
+ gone: an anonymous one starts over (`onStartOver`), a registered one sets
24
+ `isSignInRequired$` for the app to ask the user to sign in.
25
+ - **Sign-in** with email, Apple (native sheet on iOS, the API's web flow on
26
+ Android) or Google replaces an anonymous account; **linking** Apple or Google
27
+ keeps it. The device token is reissued for the new account.
28
+ - The session itself is better-auth's: the Expo plugin keeps the cookie in the
29
+ keychain and caches `/get-session`, so the user is known offline.
30
+ `holdsNoAccount()` and `readStoredCookie()` answer synchronously, before the
31
+ first render.
32
+
33
+ ## Usage
34
+
35
+ ```ts
36
+ import { createAuthSession, createExpoAuthClient } from "@missionmind/auth-front";
37
+
38
+ export const expoAuthClient = createExpoAuthClient({
39
+ baseURL: API_URL,
40
+ scheme: "myapp",
41
+ storagePrefix: "myapp", // keychain: myapp_cookie, myapp.device-token…
42
+ });
43
+ export const { authClient } = expoAuthClient;
44
+
45
+ export const {
46
+ ensureAccount, ensureSession, recoverSession, initializeAuth, holdsNoAccount,
47
+ isSignInRequired$, useAuthSession, signInWithEmail, signInWithApple,
48
+ signInWithGoogle, linkWithApple, linkWithGoogle, requestPasswordReset,
49
+ resetPassword, signOut, rememberAccountKind, rememberAccountUser,
50
+ } = createAuthSession(expoAuthClient, {
51
+ signInOrigin: "myapp://", // sent as expo-origin: one of the API's trusted origins
52
+ passwordResetUrl: `${API_URL}/reset-password`,
53
+ apple: { webSignInUrl: `${API_URL}/auth/apple/start` },
54
+ google: { webClientId: GOOGLE_WEB_CLIENT_ID, iosClientId: GOOGLE_IOS_CLIENT_ID },
55
+ // All optional:
56
+ onStartOver: () => clearLocalData(),
57
+ onAccountSignedIn: ({ isSameAccount }) => (isSameAccount ? syncLocalData() : clearLocalData()),
58
+ onAnonymousAccountReplaced: (userId) => transferPurchases(userId),
59
+ trackAction: (action, props, run) => run(),
60
+ onPasswordReset: () => {},
61
+ onSignOut: () => {},
62
+ reportError: (error) => {},
63
+ legacyStorageKeys: [],
64
+ });
65
+ ```
66
+
67
+ - Call `initializeAuth()` once at launch, and `recoverSession()` when the API
68
+ answers 401.
69
+ - Sign-ins throw `AuthenticationError` (`code` is Better Auth's error code) and
70
+ resolve false when the user backs out of the Apple or Google sheet.
71
+ - Android's Apple sign-in answers on `<scheme>://apple-auth`
72
+ (`APPLE_WEB_SIGN_IN_APP_PATH`): let `+native-intent` consume it rather than
73
+ navigate.
@@ -0,0 +1,7 @@
1
+ export declare function getAppleIdToken({ webSignInUrl, }: {
2
+ webSignInUrl: string;
3
+ }): Promise<{
4
+ token: string;
5
+ nonce: string;
6
+ } | null>;
7
+ //# sourceMappingURL=apple-id-token.android.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apple-id-token.android.d.ts","sourceRoot":"","sources":["../src/apple-id-token.android.ts"],"names":[],"mappings":"AAWA,wBAAsB,eAAe,CAAC,EACpC,YAAY,GACb,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;CACtB;;;UA0CA"}
@@ -0,0 +1,39 @@
1
+ import Constants from "expo-constants";
2
+ import { CryptoDigestAlgorithm, digestStringAsync, randomUUID, } from "expo-crypto";
3
+ import { openAuthSessionAsync } from "expo-web-browser";
4
+ import { APPLE_WEB_SIGN_IN_APP_PATH } from "./apple-web-sign-in";
5
+ const USER_CANCELLED = "user_cancelled_authorize";
6
+ export async function getAppleIdToken({ webSignInUrl, }) {
7
+ const scheme = Constants.expoConfig?.scheme;
8
+ if (typeof scheme !== "string") {
9
+ throw new Error("No app scheme");
10
+ }
11
+ const state = randomUUID();
12
+ const nonce = randomUUID();
13
+ // Apple embeds the hash; the server checks it against the raw nonce, so a
14
+ // token caught on its way through the deep link cannot be replayed.
15
+ const hashedNonce = await digestStringAsync(CryptoDigestAlgorithm.SHA256, nonce);
16
+ const startUrl = `${webSignInUrl}?${new URLSearchParams({
17
+ scheme,
18
+ state,
19
+ nonce: hashedNonce,
20
+ }).toString()}`;
21
+ const result = await openAuthSessionAsync(startUrl, `${scheme}://${APPLE_WEB_SIGN_IN_APP_PATH}`);
22
+ if (result.type !== "success") {
23
+ return null;
24
+ }
25
+ const params = new URL(result.url).searchParams;
26
+ if (params.get("state") !== state) {
27
+ throw new Error("Apple sign-in state mismatch");
28
+ }
29
+ const error = params.get("error");
30
+ if (error === USER_CANCELLED) {
31
+ return null;
32
+ }
33
+ const token = params.get("id_token");
34
+ if (!token) {
35
+ throw new Error(`Apple sign-in failed: ${error ?? "no identity token"}`);
36
+ }
37
+ return { token, nonce };
38
+ }
39
+ //# sourceMappingURL=apple-id-token.android.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apple-id-token.android.js","sourceRoot":"","sources":["../src/apple-id-token.android.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,UAAU,GACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,MAAM,cAAc,GAAG,0BAA0B,CAAC;AAElD,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,EACpC,YAAY,GAGb;IACC,MAAM,MAAM,GAAG,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5C,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;IACnC,CAAC;IAED,MAAM,KAAK,GAAG,UAAU,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,UAAU,EAAE,CAAC;IAC3B,0EAA0E;IAC1E,oEAAoE;IACpE,MAAM,WAAW,GAAG,MAAM,iBAAiB,CACzC,qBAAqB,CAAC,MAAM,EAC5B,KAAK,CACN,CAAC;IACF,MAAM,QAAQ,GAAG,GAAG,YAAY,IAAI,IAAI,eAAe,CAAC;QACtD,MAAM;QACN,KAAK;QACL,KAAK,EAAE,WAAW;KACnB,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC;IAEhB,MAAM,MAAM,GAAG,MAAM,oBAAoB,CACvC,QAAQ,EACR,GAAG,MAAM,MAAM,0BAA0B,EAAE,CAC5C,CAAC;IACF,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC;IAChD,IAAI,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAClD,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAClC,IAAI,KAAK,KAAK,cAAc,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACrC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,yBAAyB,KAAK,IAAI,mBAAmB,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * An Apple identity token for `authClient.signIn.social` / `linkSocial`:
3
+ * from the native sheet on iOS (`.ios.ts`), from Apple's web sign-in in a
4
+ * browser tab on Android (`.android.ts`, opening `webSignInUrl`: the API's
5
+ * `createAppleWebSignInRouter` `/start`). Resolves null when the user backs out.
6
+ */
7
+ export declare function getAppleIdToken(options: {
8
+ webSignInUrl: string;
9
+ }): Promise<{
10
+ nonce?: string;
11
+ token: string;
12
+ } | null>;
@@ -0,0 +1,7 @@
1
+ /** The native sheet: no web sign-in on iOS. */
2
+ export declare function getAppleIdToken(_options: {
3
+ webSignInUrl: string;
4
+ }): Promise<{
5
+ token: string;
6
+ } | null>;
7
+ //# sourceMappingURL=apple-id-token.ios.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apple-id-token.ios.d.ts","sourceRoot":"","sources":["../src/apple-id-token.ios.ts"],"names":[],"mappings":"AAGA,+CAA+C;AAC/C,wBAAsB,eAAe,CAAC,QAAQ,EAAE;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE;;UAsBvE"}
@@ -0,0 +1,25 @@
1
+ // biome-ignore lint/performance/noNamespaceImport: needed for expo-apple-authentication
2
+ import * as AppleAuthentication from "expo-apple-authentication";
3
+ /** The native sheet: no web sign-in on iOS. */
4
+ export async function getAppleIdToken(_options) {
5
+ try {
6
+ const credential = await AppleAuthentication.signInAsync({
7
+ requestedScopes: [
8
+ AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
9
+ AppleAuthentication.AppleAuthenticationScope.EMAIL,
10
+ ],
11
+ });
12
+ if (!credential.identityToken) {
13
+ throw new Error("No identity token");
14
+ }
15
+ return { token: credential.identityToken };
16
+ // biome-ignore lint/suspicious/noExplicitAny: tmp fix
17
+ }
18
+ catch (err) {
19
+ if (err.code === "ERR_REQUEST_CANCELED") {
20
+ return null;
21
+ }
22
+ throw err;
23
+ }
24
+ }
25
+ //# sourceMappingURL=apple-id-token.ios.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apple-id-token.ios.js","sourceRoot":"","sources":["../src/apple-id-token.ios.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,OAAO,KAAK,mBAAmB,MAAM,2BAA2B,CAAC;AAEjE,+CAA+C;AAC/C,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAkC;IACtE,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,MAAM,mBAAmB,CAAC,WAAW,CAAC;YACvD,eAAe,EAAE;gBACf,mBAAmB,CAAC,wBAAwB,CAAC,SAAS;gBACtD,mBAAmB,CAAC,wBAAwB,CAAC,KAAK;aACnD;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,UAAU,CAAC,aAAa,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;QACvC,CAAC;QAED,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,aAAa,EAAE,CAAC;QAC3C,sDAAsD;IACxD,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,IAAI,GAAG,CAAC,IAAI,KAAK,sBAAsB,EAAE,CAAC;YACxC,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The deep link path Apple's web sign-in (Android) answers on:
3
+ * `<scheme>://apple-auth`, as the API's `createAppleWebSignInRouter` sends it.
4
+ * The app should consume it rather than navigate to it (Expo Router's
5
+ * `+native-intent`).
6
+ */
7
+ export declare const APPLE_WEB_SIGN_IN_APP_PATH = "apple-auth";
8
+ //# sourceMappingURL=apple-web-sign-in.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apple-web-sign-in.d.ts","sourceRoot":"","sources":["../src/apple-web-sign-in.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,eAAe,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The deep link path Apple's web sign-in (Android) answers on:
3
+ * `<scheme>://apple-auth`, as the API's `createAppleWebSignInRouter` sends it.
4
+ * The app should consume it rather than navigate to it (Expo Router's
5
+ * `+native-intent`).
6
+ */
7
+ export const APPLE_WEB_SIGN_IN_APP_PATH = "apple-auth";
8
+ //# sourceMappingURL=apple-web-sign-in.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apple-web-sign-in.js","sourceRoot":"","sources":["../src/apple-web-sign-in.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,YAAY,CAAC"}
@@ -0,0 +1,130 @@
1
+ export interface ExpoAuthClientOptions {
2
+ /** The API's URL, where Better Auth is served (`/api/auth`). */
3
+ baseURL: string;
4
+ /** This build's deep link scheme, for the Expo plugin's browser flows. */
5
+ scheme: string;
6
+ /**
7
+ * Prefix of what is kept in the keychain: the session cookie
8
+ * (`<prefix>_cookie`) and, with `createAuthSession`, the device token and
9
+ * the account records (`<prefix>.device-token`…).
10
+ */
11
+ storagePrefix: string;
12
+ }
13
+ /**
14
+ * The Better Auth client of an Expo app: the anonymous plugin, and the Expo
15
+ * plugin keeping the session cookie in the keychain, which also caches each
16
+ * `/get-session` reply there so the user is known before the first one comes
17
+ * back, and while offline.
18
+ */
19
+ export declare function createExpoAuthClient(options: ExpoAuthClientOptions): {
20
+ authClient: import("better-auth/react").ReactAuthClient<{
21
+ baseURL: string;
22
+ sessionOptions: {
23
+ refetchInterval: number;
24
+ refetchOnWindowFocus: false;
25
+ refetchWhenOffline: false;
26
+ };
27
+ plugins: ({
28
+ id: "anonymous";
29
+ version: string;
30
+ $InferServerPlugin: ReturnType<typeof import("better-auth/plugins").anonymous>;
31
+ pathMethods: {
32
+ "/sign-in/anonymous": "POST";
33
+ "/delete-anonymous-user": "POST";
34
+ };
35
+ atomListeners: {
36
+ matcher: (path: string) => path is "/sign-in/anonymous";
37
+ signal: "$sessionSignal";
38
+ }[];
39
+ $ERROR_CODES: {
40
+ FAILED_TO_CREATE_USER: import("better-auth").RawError<"FAILED_TO_CREATE_USER">;
41
+ INVALID_EMAIL_FORMAT: import("better-auth").RawError<"INVALID_EMAIL_FORMAT">;
42
+ COULD_NOT_CREATE_SESSION: import("better-auth").RawError<"COULD_NOT_CREATE_SESSION">;
43
+ ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY: import("better-auth").RawError<"ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY">;
44
+ FAILED_TO_DELETE_ANONYMOUS_USER: import("better-auth").RawError<"FAILED_TO_DELETE_ANONYMOUS_USER">;
45
+ FAILED_TO_DELETE_ANONYMOUS_USER_SESSIONS: import("better-auth").RawError<"FAILED_TO_DELETE_ANONYMOUS_USER_SESSIONS">;
46
+ USER_IS_NOT_ANONYMOUS: import("better-auth").RawError<"USER_IS_NOT_ANONYMOUS">;
47
+ DELETE_ANONYMOUS_USER_DISABLED: import("better-auth").RawError<"DELETE_ANONYMOUS_USER_DISABLED">;
48
+ };
49
+ } | {
50
+ id: "expo";
51
+ version: string;
52
+ getActions(_fetch: unknown, $store: import("better-auth").ClientStore): {
53
+ getCookie: () => Promise<string>;
54
+ };
55
+ fetchPlugins: {
56
+ id: string;
57
+ name: string;
58
+ hooks: {
59
+ onSuccess(context: import("@better-fetch/fetch").SuccessContext<any>): Promise<void>;
60
+ };
61
+ init(url: string, options: ({
62
+ method?: string | undefined;
63
+ window?: null | undefined;
64
+ redirect?: RequestRedirect | undefined;
65
+ cache?: RequestCache | undefined;
66
+ credentials?: RequestCredentials | undefined;
67
+ integrity?: string | undefined;
68
+ keepalive?: boolean | undefined;
69
+ mode?: RequestMode | undefined;
70
+ priority?: RequestPriority | undefined;
71
+ referrer?: string | undefined;
72
+ referrerPolicy?: ReferrerPolicy | undefined;
73
+ signal?: (AbortSignal | null) | undefined;
74
+ } & {
75
+ onRequest?: (<T extends Record<string, any>>(context: import("@better-fetch/fetch").RequestContext<T>) => Promise<import("@better-fetch/fetch").RequestContext | void> | import("@better-fetch/fetch").RequestContext | void) | undefined;
76
+ onResponse?: ((context: import("@better-fetch/fetch").ResponseContext) => Promise<Response | void | import("@better-fetch/fetch").ResponseContext> | Response | import("@better-fetch/fetch").ResponseContext | void) | undefined;
77
+ onSuccess?: ((context: import("@better-fetch/fetch").SuccessContext<any>) => Promise<void> | void) | undefined;
78
+ onError?: ((context: import("@better-fetch/fetch").ErrorContext) => Promise<void> | void) | undefined;
79
+ onRetry?: ((response: import("@better-fetch/fetch").ResponseContext) => Promise<void> | void) | undefined;
80
+ hookOptions?: {
81
+ cloneResponse?: boolean;
82
+ } | undefined;
83
+ timeout?: number | undefined;
84
+ customFetchImpl?: import("@better-fetch/fetch").FetchEsque | undefined;
85
+ plugins?: import("@better-fetch/fetch").BetterFetchPlugin[] | undefined;
86
+ baseURL?: string | undefined;
87
+ throw?: boolean | undefined;
88
+ auth?: ({
89
+ type: "Bearer";
90
+ token: string | Promise<string | undefined> | (() => string | Promise<string | undefined> | undefined) | undefined;
91
+ } | {
92
+ type: "Basic";
93
+ username: string | (() => string | undefined) | undefined;
94
+ password: string | (() => string | undefined) | undefined;
95
+ } | {
96
+ type: "Custom";
97
+ prefix: string | (() => string | undefined) | undefined;
98
+ value: string | (() => string | undefined) | undefined;
99
+ }) | undefined;
100
+ headers?: {} | {
101
+ [x: string]: string | undefined;
102
+ accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
103
+ "content-type"?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream" | "application/x-www-form-urlencoded" | "multipart/form-data") | undefined;
104
+ authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
105
+ } | undefined;
106
+ body?: any;
107
+ query?: any;
108
+ params?: any;
109
+ duplex?: "full" | "half" | undefined;
110
+ jsonParser?: ((text: string) => Promise<any> | any) | undefined;
111
+ retry?: import("@better-fetch/fetch").RetryOptions | undefined;
112
+ retryAttempt?: number | undefined;
113
+ method?: ("POST" | "PUT" | "DELETE" | "PATCH" | "GET" | "HEAD" | "OPTIONS" | (string & {})) | undefined;
114
+ output?: (import("@better-fetch/fetch").StandardSchemaV1 | typeof Blob | typeof File) | undefined;
115
+ errorSchema?: import("@better-fetch/fetch").StandardSchemaV1 | undefined;
116
+ disableValidation?: boolean | undefined;
117
+ signal?: (AbortSignal | null) | undefined;
118
+ } & Record<string, any>) | undefined): Promise<{
119
+ url: string;
120
+ options: import("better-auth").ClientFetchOption;
121
+ }>;
122
+ }[];
123
+ })[];
124
+ }>;
125
+ readStoredCookie: () => string;
126
+ storagePrefix: string;
127
+ };
128
+ export type ExpoAuthClient = ReturnType<typeof createExpoAuthClient>;
129
+ export type AuthClient = ExpoAuthClient["authClient"];
130
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BA0Cy6E,CAAC;0BAAkC,CAAC;4BAAkC,CAAC;yBAA0C,CAAC;+BAA6C,CAAC;6BAAiD,CAAC;6BAAqC,CAAC;wBAAiC,CAAC;4BAAyC,CAAC;4BAA6C,CAAC;kCAA0C,CAAC;0BAA0C,CAAC;;6BAA6D,CAAC;8BAAiP,CAAC;6BAAuO,CAAC;2BAAmH,CAAC;2BAA4G,CAAC;+BAAoH,CAAC;qCAAyB,CAAC;;2BAA6C,CAAC;mCAA2C,CAAC;2BAAqE,CAAC;2BAA8E,CAAC;yBAAiC,CAAC;wBAAiC,CAAC;;;;;;;;;;;;2BAA0gB,CAAC;;8BAAgE,CAAC;sCAAsH,CAAC;qCAAmL,CAAC;;wBAAuG,CAAC;yBAAkB,CAAC;0BAAmB,CAAC;0BAAmB,CAAC;8BAA+C,CAAC;yBAAiE,CAAC;gCAA4E,CAAC;0BAAkC,CAAC;0BAA8G,CAAC;+BAA6G,CAAC;qCAAqF,CAAC;0BAAmC,CAAC;;;;;;;;4BAbnqL,MAAM;;EASpC;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACrE,MAAM,MAAM,UAAU,GAAG,cAAc,CAAC,YAAY,CAAC,CAAC"}
package/dist/client.js ADDED
@@ -0,0 +1,48 @@
1
+ import { expoClient, getCookie, storageAdapter, } from "@better-auth/expo/client";
2
+ import { anonymousClient } from "better-auth/client/plugins";
3
+ import { createAuthClient } from "better-auth/react";
4
+ // biome-ignore lint/performance/noNamespaceImport: needed for better-auth/expo
5
+ import * as SecureStore from "expo-secure-store";
6
+ /**
7
+ * The Better Auth client of an Expo app: the anonymous plugin, and the Expo
8
+ * plugin keeping the session cookie in the keychain, which also caches each
9
+ * `/get-session` reply there so the user is known before the first one comes
10
+ * back, and while offline.
11
+ */
12
+ export function createExpoAuthClient(options) {
13
+ const { storagePrefix } = options;
14
+ const authClient = createAuthClient({
15
+ baseURL: options.baseURL,
16
+ sessionOptions: {
17
+ refetchInterval: 60 * 60 * 24, // 24 hours
18
+ refetchOnWindowFocus: false,
19
+ refetchWhenOffline: false,
20
+ },
21
+ plugins: [
22
+ anonymousClient(),
23
+ expoClient({
24
+ scheme: options.scheme,
25
+ storagePrefix,
26
+ storage: SecureStore,
27
+ }),
28
+ ],
29
+ });
30
+ const cookieStorage = storageAdapter(SecureStore);
31
+ /**
32
+ * The session cookie the Expo plugin keeps in the keychain, read
33
+ * synchronously — for the root layout, which decides before the first
34
+ * render. Everything else reads it with `authClient.getCookie()`, which is
35
+ * asynchronous since better-auth 1.7. A keychain that cannot be read counts
36
+ * as empty.
37
+ */
38
+ function readStoredCookie() {
39
+ try {
40
+ return getCookie(cookieStorage.getItem(`${storagePrefix}_cookie`));
41
+ }
42
+ catch {
43
+ return "";
44
+ }
45
+ }
46
+ return { authClient, readStoredCookie, storagePrefix };
47
+ }
48
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,SAAS,EACT,cAAc,GACf,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,+EAA+E;AAC/E,OAAO,KAAK,WAAW,MAAM,mBAAmB,CAAC;AAejD;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAA8B;IACjE,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC;IAElC,MAAM,UAAU,GAAG,gBAAgB,CAAC;QAClC,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,cAAc,EAAE;YACd,eAAe,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,WAAW;YAC1C,oBAAoB,EAAE,KAAK;YAC3B,kBAAkB,EAAE,KAAK;SAC1B;QACD,OAAO,EAAE;YACP,eAAe,EAAE;YACjB,UAAU,CAAC;gBACT,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,aAAa;gBACb,OAAO,EAAE,WAAW;aACrB,CAAC;SACH;KACF,CAAC,CAAC;IAEH,MAAM,aAAa,GAAG,cAAc,CAAC,WAAW,CAAC,CAAC;IAElD;;;;;;OAMG;IACH,SAAS,gBAAgB;QACvB,IAAI,CAAC;YACH,OAAO,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,aAAa,SAAS,CAAC,CAAC,CAAC;QACrE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,aAAa,EAAE,CAAC;AACzD,CAAC"}
@@ -0,0 +1,5 @@
1
+ /** biome-ignore-all lint/performance/noBarrelFile: the package's entry point */
2
+ export { APPLE_WEB_SIGN_IN_APP_PATH } from "./apple-web-sign-in";
3
+ export { type AuthClient, createExpoAuthClient, type ExpoAuthClient, type ExpoAuthClientOptions, } from "./client";
4
+ export { type AuthAction, type AuthActionProps, AuthenticationError, type AuthProvider, type AuthSession, type AuthSessionOptions, createAuthSession, } from "./session";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EACL,KAAK,UAAU,EACf,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,qBAAqB,GAC3B,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,mBAAmB,EACnB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,iBAAiB,GAClB,MAAM,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ /** biome-ignore-all lint/performance/noBarrelFile: the package's entry point */
2
+ export { APPLE_WEB_SIGN_IN_APP_PATH } from "./apple-web-sign-in";
3
+ export { createExpoAuthClient, } from "./client";
4
+ export { AuthenticationError, createAuthSession, } from "./session";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAEL,oBAAoB,GAGrB,MAAM,UAAU,CAAC;AAClB,OAAO,EAGL,mBAAmB,EAInB,iBAAiB,GAClB,MAAM,WAAW,CAAC"}
@@ -0,0 +1,132 @@
1
+ import type { ExpoAuthClient } from "./client";
2
+ export type AuthProvider = "email" | "apple" | "google";
3
+ export type AuthAction = "sign_in" | "link_account";
4
+ /** Where a sign-in started (`sign_in` by default), or `settings` for a link. */
5
+ export interface AuthActionProps<EntryPoint extends string = string> {
6
+ entry_point: EntryPoint | "sign_in" | "settings";
7
+ provider: AuthProvider;
8
+ }
9
+ export interface AuthSessionOptions<EntryPoint extends string = string> {
10
+ apple: {
11
+ /**
12
+ * Apple's web sign-in on Android: the API's `createAppleWebSignInRouter`
13
+ * `/start` (`<api>/auth/apple/start`).
14
+ */
15
+ webSignInUrl: string;
16
+ };
17
+ google: {
18
+ /** The iOS OAuth client. */
19
+ iosClientId?: string;
20
+ /** The web OAuth client, whose id tokens the API verifies. */
21
+ webClientId?: string;
22
+ };
23
+ /**
24
+ * Keychain items an earlier version of the app wrote and nothing reads any
25
+ * more, deleted at launch.
26
+ */
27
+ legacyStorageKeys?: readonly string[];
28
+ /**
29
+ * The user's data on this device belongs to another account, or to none:
30
+ * a sign-in into another account (`isSameAccount` false) or back into the
31
+ * same one, or a device left without an account (`onStartOver`).
32
+ */
33
+ onAccountSignedIn?: (change: {
34
+ isSameAccount: boolean;
35
+ userId: string | undefined;
36
+ }) => Promise<void> | void;
37
+ /**
38
+ * An anonymous account was replaced by the account the user signed into,
39
+ * which the server deletes: hand over what is kept outside the API
40
+ * (purchases). Must not throw.
41
+ */
42
+ onAnonymousAccountReplaced?: (userId: string) => Promise<void> | void;
43
+ /** A password reset was requested or submitted (analytics). */
44
+ onPasswordReset?: () => void;
45
+ /** The user signs out (analytics). Called first. */
46
+ onSignOut?: () => void;
47
+ /**
48
+ * The device has no account any more (signed out, or its anonymous account
49
+ * is gone): empty what it kept of the old one.
50
+ */
51
+ onStartOver?: () => Promise<void> | void;
52
+ /**
53
+ * Where the password reset email's link leads: the API's
54
+ * `createResetPasswordRouter`.
55
+ */
56
+ passwordResetUrl: string;
57
+ /** Errors worth hearing about, beside the console (Sentry). */
58
+ reportError?: (error: unknown) => void;
59
+ /**
60
+ * The origin id-token sign-ins (Apple, Google) send as `expo-origin`: one of
61
+ * the API's trusted origins, such as `myapp://`.
62
+ */
63
+ signInOrigin: string;
64
+ /**
65
+ * Wraps each sign-in and link (analytics). `run` resolves false when the user
66
+ * backed out. Must return what `run` returned, and rethrow what it threw.
67
+ */
68
+ trackAction?: (action: AuthAction, props: AuthActionProps<EntryPoint>, run: () => Promise<boolean>) => Promise<boolean>;
69
+ }
70
+ export declare class AuthenticationError extends Error {
71
+ code: string;
72
+ constructor(status: number, code: string);
73
+ }
74
+ /**
75
+ * The account a device holds, and everything that signs it in and out.
76
+ *
77
+ * A device gets an anonymous account once (`ensureAccount`), and a long-lived
78
+ * device token in the keychain that brings that account back whenever its
79
+ * session dies (`ensureSession`, `recoverSession`). Signing in with an email,
80
+ * Apple or Google replaces an anonymous account (the server hands its data
81
+ * over), linking Apple or Google keeps it. A registered account that cannot be
82
+ * recovered asks the user to sign in (`isSignInRequired$`); an anonymous one
83
+ * starts over.
84
+ */
85
+ export declare function createAuthSession<EntryPoint extends string = string>(client: ExpoAuthClient, options: AuthSessionOptions<EntryPoint>): {
86
+ ensureAccount: () => Promise<boolean>;
87
+ ensureSession: () => Promise<boolean>;
88
+ hasSessionCookie: () => Promise<boolean>;
89
+ holdsNoAccount: () => boolean;
90
+ initializeAuth: () => Promise<void>;
91
+ isSignInRequired$: import("@legendapp/state").ObservableBoolean<boolean>;
92
+ linkWithApple: () => Promise<boolean>;
93
+ linkWithGoogle: () => Promise<boolean>;
94
+ recoverSession: () => Promise<boolean>;
95
+ rememberAccountKind: (isAnonymous: boolean) => Promise<void>;
96
+ rememberAccountUser: (userId: string) => Promise<void>;
97
+ requestPasswordReset: (email: string) => Promise<void>;
98
+ resetPassword: (newPassword: string, token: string) => Promise<void>;
99
+ signInWithApple: (entryPoint?: EntryPoint | "sign_in") => Promise<boolean>;
100
+ signInWithEmail: (email: string, password: string, entryPoint?: EntryPoint | "sign_in") => Promise<boolean>;
101
+ signInWithGoogle: (entryPoint?: EntryPoint | "sign_in") => Promise<boolean>;
102
+ signOut: () => Promise<void>;
103
+ useAuthSession: () => {
104
+ isAnonymous: boolean;
105
+ isPending: boolean;
106
+ session: {
107
+ user: import("better-auth").StripEmptyObjects<{
108
+ id: string;
109
+ createdAt: Date;
110
+ updatedAt: Date;
111
+ email: string;
112
+ emailVerified: boolean;
113
+ name: string;
114
+ image?: string | null | undefined;
115
+ } & {
116
+ isAnonymous: boolean | null | undefined;
117
+ } & {}>;
118
+ session: import("better-auth").StripEmptyObjects<{
119
+ id: string;
120
+ createdAt: Date;
121
+ updatedAt: Date;
122
+ userId: string;
123
+ expiresAt: Date;
124
+ token: string;
125
+ ipAddress?: string | null | undefined;
126
+ userAgent?: string | null | undefined;
127
+ }>;
128
+ } | null;
129
+ };
130
+ };
131
+ export type AuthSession = ReturnType<typeof createAuthSession>;
132
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAU/C,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;AACxD,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,cAAc,CAAC;AACpD,gFAAgF;AAChF,MAAM,WAAW,eAAe,CAAC,UAAU,SAAS,MAAM,GAAG,MAAM;IACjE,WAAW,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,CAAC;IACjD,QAAQ,EAAE,YAAY,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB,CAAC,UAAU,SAAS,MAAM,GAAG,MAAM;IACpE,KAAK,EAAE;QACL;;;WAGG;QACH,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,MAAM,EAAE;QACN,4BAA4B;QAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,8DAA8D;QAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IACF;;;OAGG;IACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE;QAC3B,aAAa,EAAE,OAAO,CAAC;QACvB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;KAC5B,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3B;;;;OAIG;IACH,0BAA0B,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACtE,+DAA+D;IAC/D,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IAC7B,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACzC;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,WAAW,CAAC,EAAE,CACZ,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,eAAe,CAAC,UAAU,CAAC,EAClC,GAAG,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,KACxB,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,IAAI,EAAE,MAAM,CAAC;gBAED,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAKzC;AA6CD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,SAAS,MAAM,GAAG,MAAM,EAClE,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,kBAAkB,CAAC,UAAU,CAAC;yBAkcb,OAAO,CAAC,OAAO,CAAC;yBAThB,OAAO,CAAC,OAAO,CAAC;4BA5XP,OAAO,CAAC,OAAO,CAAC;;;;;;0BAqZxB,OAAO,CAAC,OAAO,CAAC;uCAxTK,OAAO;kCAeZ,MAAM;kCA2mBN,MAAM;iCAWP,MAAM,SAAS,MAAM;mCA3IjD,UAAU,GAAG,SAAS;6BAvB3B,MAAM,YACH,MAAM,eACJ,UAAU,GAAG,SAAS;oCAsDtB,UAAU,GAAG,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0JrC;AAED,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC"}