tauri-plugin-better-auth 0.0.0-stage → 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 +21 -0
- package/README.md +49 -2
- package/dist/client.d.ts +232 -0
- package/dist/client.js +244 -0
- package/dist/index-DYZxnm2f.d.ts +292 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +235 -0
- package/dist/protocol-Fcr7NzIh.js +111 -0
- package/dist/proxy.d.ts +124 -0
- package/dist/proxy.js +217 -0
- package/package.json +93 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Muhammed Koç and tauri-plugin-better-auth contributors
|
|
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
CHANGED
|
@@ -1,3 +1,50 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tauri-plugin-better-auth
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Better Auth](https://better-auth.com) for [Tauri 2](https://tauri.app). Users
|
|
4
|
+
sign in through the system browser with PKCE. The session lives in Rust, and
|
|
5
|
+
webview code never sees a token.
|
|
6
|
+
|
|
7
|
+
This npm package is one half of the plugin. The other half is the Rust crate
|
|
8
|
+
[`tauri-plugin-better-auth`](https://crates.io/crates/tauri-plugin-better-auth).
|
|
9
|
+
Full documentation:
|
|
10
|
+
[github.com/laakal/tauri-plugin-better-auth](https://github.com/laakal/tauri-plugin-better-auth#readme).
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install tauri-plugin-better-auth
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Import | Where | What |
|
|
17
|
+
| --------------------------------- | ----------------------- | --------------------------------------------------------------------- |
|
|
18
|
+
| `tauri-plugin-better-auth` | Your Better Auth server | `tauri()` server plugin: `/tauri/authorize`, `/tauri/token` |
|
|
19
|
+
| `tauri-plugin-better-auth/proxy` | Your hand-off web page | `tauriProxyClient()`: request capture, consent hand-off, cancel |
|
|
20
|
+
| `tauri-plugin-better-auth/client` | The Tauri webview | `withTauri()`, `tauriFetch`, `requestAuth()`, events, tokenless types |
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// Server
|
|
24
|
+
import { bearer } from 'better-auth/plugins';
|
|
25
|
+
import { tauri } from 'tauri-plugin-better-auth';
|
|
26
|
+
betterAuth({
|
|
27
|
+
plugins: [
|
|
28
|
+
bearer(),
|
|
29
|
+
tauri({
|
|
30
|
+
clientID: 'com.example.app',
|
|
31
|
+
authorizeOrigins: ['https://example.com'],
|
|
32
|
+
}),
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Tauri webview
|
|
37
|
+
import { createAuthClient } from 'better-auth/react';
|
|
38
|
+
import {
|
|
39
|
+
asTauriClient,
|
|
40
|
+
requestAuth,
|
|
41
|
+
withTauri,
|
|
42
|
+
} from 'tauri-plugin-better-auth/client';
|
|
43
|
+
const authClient = asTauriClient(
|
|
44
|
+
createAuthClient(withTauri({ baseURL: 'https://api.example.com/api/auth' })),
|
|
45
|
+
);
|
|
46
|
+
await requestAuth();
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Requires `better-auth` >= 1.7.4 and, for `/client`, `@tauri-apps/api` >= 2.11.
|
|
50
|
+
MIT licensed.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { UnlistenFn } from "@tauri-apps/api/event";
|
|
2
|
+
import { BetterAuthClientOptions } from "@better-auth/core";
|
|
3
|
+
//#region src/client/commands.d.ts
|
|
4
|
+
/** How the browser hands the sign-in back to the app. */
|
|
5
|
+
type Transport = 'auto' | 'deep-link' | 'loopback';
|
|
6
|
+
/** The signed-in user (Better Auth user object). */
|
|
7
|
+
type TauriUser = Record<string, unknown> & {
|
|
8
|
+
id: string;
|
|
9
|
+
email?: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
image?: string | null;
|
|
12
|
+
};
|
|
13
|
+
/** The current session as the webview sees it: never contains a token. */
|
|
14
|
+
interface TauriSessionSnapshot<U = TauriUser, S = Record<string, unknown>> {
|
|
15
|
+
user: U;
|
|
16
|
+
/** Better Auth session object without `token`. */
|
|
17
|
+
session: S;
|
|
18
|
+
/** True when the server could not be reached and this is the cached copy. */
|
|
19
|
+
offline: boolean;
|
|
20
|
+
}
|
|
21
|
+
interface RequestAuthOptions {
|
|
22
|
+
/** Social provider the hand-off page should start (if it allows it). */
|
|
23
|
+
provider?: string;
|
|
24
|
+
/** Overrides the transport configured in Rust for this sign-in. */
|
|
25
|
+
transport?: Transport;
|
|
26
|
+
}
|
|
27
|
+
interface RequestAuthResult {
|
|
28
|
+
/** The sign-in URL: show it when the browser could not be opened. */
|
|
29
|
+
url: string;
|
|
30
|
+
/** Whether the system browser was opened. */
|
|
31
|
+
opened: boolean;
|
|
32
|
+
/** The transport chosen for this sign-in. */
|
|
33
|
+
transport: Transport;
|
|
34
|
+
}
|
|
35
|
+
/** Error raised by the plugin commands; `code` is stable (see the README). */
|
|
36
|
+
export declare class TauriAuthError extends Error {
|
|
37
|
+
readonly code: string;
|
|
38
|
+
name: string;
|
|
39
|
+
constructor(code: string, message: string);
|
|
40
|
+
}
|
|
41
|
+
/** True for errors raised by this plugin. */
|
|
42
|
+
export declare function isTauriAuthError(error: unknown): error is TauriAuthError;
|
|
43
|
+
/**
|
|
44
|
+
* Opens the system browser on your sign-in page. The result arrives later as
|
|
45
|
+
* an `authenticated` event (deep link or loopback), or through
|
|
46
|
+
* {@link authenticate} when the user pastes the code.
|
|
47
|
+
*/
|
|
48
|
+
export declare function requestAuth(options?: RequestAuthOptions): Promise<RequestAuthResult>;
|
|
49
|
+
/** Completes a sign-in with a pasted code or callback link. */
|
|
50
|
+
export declare function authenticate<U = TauriUser>(codeOrLink: string): Promise<TauriSessionSnapshot<U>>;
|
|
51
|
+
/** Cancels the sign-in in progress. */
|
|
52
|
+
export declare function cancelAuth(): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* The current session, verified with the server. With `cachedOnly`, returns
|
|
55
|
+
* the last known session without a network call (fast, for startup guards).
|
|
56
|
+
* When the server is unreachable the cached copy comes back with `offline`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function getSession<U = TauriUser>(options?: {
|
|
59
|
+
cachedOnly?: boolean;
|
|
60
|
+
}): Promise<TauriSessionSnapshot<U> | null>;
|
|
61
|
+
/** Signs out locally (always) and on the server (best effort). */
|
|
62
|
+
export declare function signOut(): Promise<void>;
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/client/events.d.ts
|
|
65
|
+
/** Why the user is no longer signed in. */
|
|
66
|
+
type SignOutReason = 'user' | 'revoked' | 'expired';
|
|
67
|
+
/** Payload of the error event. */
|
|
68
|
+
interface TauriAuthErrorEvent {
|
|
69
|
+
code: string;
|
|
70
|
+
message: string;
|
|
71
|
+
}
|
|
72
|
+
export declare const EVENT_AUTHENTICATED = "better-auth://authenticated";
|
|
73
|
+
export declare const EVENT_SIGNED_OUT = "better-auth://signed-out";
|
|
74
|
+
export declare const EVENT_ERROR = "better-auth://error";
|
|
75
|
+
/** Fires after every successful sign-in (browser or in-app). */
|
|
76
|
+
export declare function onAuthenticated<U = TauriUser>(callback: (user: U) => void): Promise<UnlistenFn>;
|
|
77
|
+
/** Fires when the session ends: signed out, revoked by the server, or expired. */
|
|
78
|
+
export declare function onSignedOut(callback: (event: {
|
|
79
|
+
reason: SignOutReason;
|
|
80
|
+
}) => void): Promise<UnlistenFn>;
|
|
81
|
+
/** Fires when a background sign-in (deep link or loopback) fails. */
|
|
82
|
+
export declare function onAuthError(callback: (error: TauriAuthErrorEvent) => void): Promise<UnlistenFn>;
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/client/fetch.d.ts
|
|
85
|
+
/**
|
|
86
|
+
* A `fetch` that sends the request through the plugin's Rust bridge.
|
|
87
|
+
*
|
|
88
|
+
* Rust adds the session's bearer token and your app headers, refuses anything
|
|
89
|
+
* outside the configured auth operations and API routes, never follows
|
|
90
|
+
* redirects, and strips `set-cookie`/`set-auth-token` from responses. The
|
|
91
|
+
* webview never sees a token.
|
|
92
|
+
*
|
|
93
|
+
* Use it as Better Auth's `customFetchImpl` (see `withTauri`) and as the
|
|
94
|
+
* `fetch` of your API client. Bodies are buffered (no streaming).
|
|
95
|
+
*/
|
|
96
|
+
export declare const tauriFetch: typeof fetch;
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/client/plugin.d.ts
|
|
99
|
+
/**
|
|
100
|
+
* Better Auth client plugin for the Tauri webview. Refreshes `useSession()`
|
|
101
|
+
* when a browser sign-in completes or the session ends, and adds
|
|
102
|
+
* `authClient.tauri.*` actions. Added for you by {@link withTauri}.
|
|
103
|
+
*/
|
|
104
|
+
export declare const tauriClient: () => {
|
|
105
|
+
id: "tauri";
|
|
106
|
+
getActions: (_$fetch: import("@better-fetch/fetch").BetterFetch, $store: import("better-auth").ClientStore) => {
|
|
107
|
+
tauri: {
|
|
108
|
+
/** Opens the system browser on the sign-in page. */
|
|
109
|
+
requestAuth: typeof requestAuth;
|
|
110
|
+
/** Completes a sign-in with a pasted code or link. */
|
|
111
|
+
authenticate: typeof authenticate;
|
|
112
|
+
/** Cancels the sign-in in progress. */
|
|
113
|
+
cancelAuth: typeof cancelAuth;
|
|
114
|
+
/** The current session from Rust (with offline fallback). */
|
|
115
|
+
getSession: typeof getSession;
|
|
116
|
+
/** Signs out locally and on the server. */
|
|
117
|
+
signOut: typeof signOut;
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
type PluginsOf<O> = O extends {
|
|
122
|
+
plugins: infer P extends readonly unknown[];
|
|
123
|
+
} ? P : [];
|
|
124
|
+
/** Options returned by {@link withTauri}. */
|
|
125
|
+
type WithTauri<O extends BetterAuthClientOptions> = Omit<O, 'plugins' | 'fetchOptions' | 'disableDefaultFetchPlugins'> & {
|
|
126
|
+
plugins: [...PluginsOf<O>, ReturnType<typeof tauriClient>];
|
|
127
|
+
fetchOptions: Omit<NonNullable<O['fetchOptions']>, 'customFetchImpl'> & {
|
|
128
|
+
customFetchImpl: typeof tauriFetch;
|
|
129
|
+
};
|
|
130
|
+
disableDefaultFetchPlugins: true;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Adapts Better Auth client options for the Tauri webview. Pass the result to
|
|
134
|
+
* the `createAuthClient` of your framework.
|
|
135
|
+
*
|
|
136
|
+
* - every request goes through the Rust bridge (`tauriFetch`): the token is
|
|
137
|
+
* added in Rust and never reaches the page;
|
|
138
|
+
* - Better Auth's redirect plugin is disabled, so no response can navigate
|
|
139
|
+
* the webview away from the app (start social sign-in with
|
|
140
|
+
* `authClient.tauri.requestAuth({ provider })` instead);
|
|
141
|
+
* - `useSession()` refreshes after browser sign-ins and sign-outs.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```ts
|
|
145
|
+
* import { createAuthClient } from 'better-auth/react';
|
|
146
|
+
* import { emailOTPClient } from 'better-auth/client/plugins';
|
|
147
|
+
* import { asTauriClient, withTauri } from 'tauri-plugin-better-auth/client';
|
|
148
|
+
*
|
|
149
|
+
* export const authClient = asTauriClient(
|
|
150
|
+
* createAuthClient(withTauri({ baseURL: 'https://api.example.com/api/auth', plugins: [emailOTPClient()] })),
|
|
151
|
+
* );
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export declare function withTauri<O extends BetterAuthClientOptions>(options: O): WithTauri<O>;
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/client/types.d.ts
|
|
157
|
+
/**
|
|
158
|
+
* Types that describe a Better Auth client as it behaves inside a Tauri
|
|
159
|
+
* webview: sessions carry no token, and only the operations the Rust bridge
|
|
160
|
+
* allows are present.
|
|
161
|
+
*/
|
|
162
|
+
/** A `{ session, user }` pair without `session.token`. */
|
|
163
|
+
type Tokenless<T> = T extends {
|
|
164
|
+
session: infer S;
|
|
165
|
+
user: infer U;
|
|
166
|
+
} ? {
|
|
167
|
+
session: Omit<S, 'token'>;
|
|
168
|
+
user: U;
|
|
169
|
+
} : T;
|
|
170
|
+
type TokenlessData<D> = D extends {
|
|
171
|
+
session: unknown;
|
|
172
|
+
user: unknown;
|
|
173
|
+
} ? Tokenless<D> : D;
|
|
174
|
+
/** Maps `{ data: … }` (a hook result or a fetch response) to tokenless data. */
|
|
175
|
+
type MapData<T> = T extends {
|
|
176
|
+
data: infer D;
|
|
177
|
+
} ? Omit<T, 'data'> & {
|
|
178
|
+
data: TokenlessData<D>;
|
|
179
|
+
} : T;
|
|
180
|
+
/** React/Vue/Svelte hooks are functions; the vanilla client exposes an atom. */
|
|
181
|
+
type MapUseSession<U> = U extends ((...args: infer A) => infer R) ? (...args: A) => MapData<R> : U extends {
|
|
182
|
+
get(): infer V;
|
|
183
|
+
} ? Omit<U, 'get' | 'value' | 'subscribe' | 'listen'> & {
|
|
184
|
+
get(): MapData<V>;
|
|
185
|
+
readonly value: MapData<V> | undefined;
|
|
186
|
+
subscribe(listener: (value: MapData<V>) => void): () => void;
|
|
187
|
+
listen(listener: (value: MapData<V>) => void): () => void;
|
|
188
|
+
} : U;
|
|
189
|
+
type MapGetSession<G> = G extends ((...args: infer A) => Promise<infer R>) ? (...args: A) => Promise<MapData<R>> : G;
|
|
190
|
+
type Restrict<T, K extends string> = T extends object ? Pick<T, Extract<keyof T, K>> : never;
|
|
191
|
+
/** Sign-in methods the bridge accepts by default. */
|
|
192
|
+
type TauriSignInMethod = 'email' | 'emailOtp';
|
|
193
|
+
/**
|
|
194
|
+
* Client actions the bridge refuses: they read or mint credentials, or need
|
|
195
|
+
* browser redirects. Use `authClient.tauri.requestAuth()` for browser flows.
|
|
196
|
+
*/
|
|
197
|
+
type TauriUnsupportedAction = 'listSessions' | 'revokeSession' | 'revokeSessions' | 'revokeOtherSessions' | 'oneTimeToken' | 'linkSocial' | 'unlinkAccount' | 'getAccessToken' | 'refreshToken' | 'accountInfo' | 'multiSession' | 'token';
|
|
198
|
+
/**
|
|
199
|
+
* A Better Auth client as seen in the Tauri webview: tokenless sessions,
|
|
200
|
+
* in-app sign-in limited to `email` and `emailOtp` (plus `ExtraSignIn`, for
|
|
201
|
+
* routes you allowed in Rust with `Builder::auth_route`).
|
|
202
|
+
*/
|
|
203
|
+
type TauriAuthClient<C, ExtraSignIn extends string = never> = Omit<C, 'useSession' | 'getSession' | 'hydrateSession' | '$Infer' | 'signIn' | 'signUp' | TauriUnsupportedAction> & {
|
|
204
|
+
useSession: C extends {
|
|
205
|
+
useSession: infer U;
|
|
206
|
+
} ? MapUseSession<U> : never;
|
|
207
|
+
getSession: C extends {
|
|
208
|
+
getSession: infer G;
|
|
209
|
+
} ? MapGetSession<G> : never;
|
|
210
|
+
signIn: C extends {
|
|
211
|
+
signIn: infer S;
|
|
212
|
+
} ? Restrict<S, TauriSignInMethod | ExtraSignIn> : never;
|
|
213
|
+
signUp: C extends {
|
|
214
|
+
signUp: infer S;
|
|
215
|
+
} ? Restrict<S, 'email'> : never;
|
|
216
|
+
$Infer: (C extends {
|
|
217
|
+
$Infer: infer I;
|
|
218
|
+
} ? Omit<I, 'Session'> : {}) & {
|
|
219
|
+
Session: C extends {
|
|
220
|
+
$Infer: {
|
|
221
|
+
Session: infer S;
|
|
222
|
+
};
|
|
223
|
+
} ? Tokenless<S> : never;
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Narrows a client created with `withTauri` to {@link TauriAuthClient}.
|
|
228
|
+
* Runtime no-op: the bridge already enforces all of this.
|
|
229
|
+
*/
|
|
230
|
+
export declare function asTauriClient<C, ExtraSignIn extends string = never>(client: C): TauriAuthClient<C, ExtraSignIn>;
|
|
231
|
+
//#endregion
|
|
232
|
+
export type { RequestAuthOptions, RequestAuthResult, SignOutReason, TauriAuthClient, TauriAuthErrorEvent, TauriSessionSnapshot, TauriSignInMethod, TauriUnsupportedAction, TauriUser, Tokenless, Transport, WithTauri };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { invoke, isTauri } from "@tauri-apps/api/core";
|
|
2
|
+
import { listen } from "@tauri-apps/api/event";
|
|
3
|
+
//#region src/client/commands.ts
|
|
4
|
+
/** Error raised by the plugin commands; `code` is stable (see the README). */
|
|
5
|
+
var TauriAuthError = class extends Error {
|
|
6
|
+
code;
|
|
7
|
+
name = "TauriAuthError";
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
/** True for errors raised by this plugin. */
|
|
14
|
+
function isTauriAuthError(error) {
|
|
15
|
+
return error instanceof TauriAuthError;
|
|
16
|
+
}
|
|
17
|
+
function toError(error) {
|
|
18
|
+
if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") {
|
|
19
|
+
const message = "message" in error && typeof error.message === "string" ? error.message : error.code;
|
|
20
|
+
return new TauriAuthError(error.code, message);
|
|
21
|
+
}
|
|
22
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
23
|
+
}
|
|
24
|
+
/** `invoke` for this plugin, with errors mapped to {@link TauriAuthError}. */
|
|
25
|
+
async function call(command, args) {
|
|
26
|
+
try {
|
|
27
|
+
return await invoke(`plugin:better-auth|${command}`, args);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw toError(error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Opens the system browser on your sign-in page. The result arrives later as
|
|
34
|
+
* an `authenticated` event (deep link or loopback), or through
|
|
35
|
+
* {@link authenticate} when the user pastes the code.
|
|
36
|
+
*/
|
|
37
|
+
function requestAuth(options = {}) {
|
|
38
|
+
return call("request_auth", { options });
|
|
39
|
+
}
|
|
40
|
+
/** Completes a sign-in with a pasted code or callback link. */
|
|
41
|
+
function authenticate(codeOrLink) {
|
|
42
|
+
return call("authenticate", { code: codeOrLink });
|
|
43
|
+
}
|
|
44
|
+
/** Cancels the sign-in in progress. */
|
|
45
|
+
function cancelAuth() {
|
|
46
|
+
return call("cancel_auth");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The current session, verified with the server. With `cachedOnly`, returns
|
|
50
|
+
* the last known session without a network call (fast, for startup guards).
|
|
51
|
+
* When the server is unreachable the cached copy comes back with `offline`.
|
|
52
|
+
*/
|
|
53
|
+
function getSession(options = {}) {
|
|
54
|
+
return call("get_session", { cachedOnly: options.cachedOnly ?? false });
|
|
55
|
+
}
|
|
56
|
+
/** Signs out locally (always) and on the server (best effort). */
|
|
57
|
+
function signOut() {
|
|
58
|
+
return call("sign_out");
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/client/events.ts
|
|
62
|
+
const EVENT_AUTHENTICATED = "better-auth://authenticated";
|
|
63
|
+
const EVENT_SIGNED_OUT = "better-auth://signed-out";
|
|
64
|
+
const EVENT_ERROR = "better-auth://error";
|
|
65
|
+
/** Fires after every successful sign-in (browser or in-app). */
|
|
66
|
+
function onAuthenticated(callback) {
|
|
67
|
+
return listen(EVENT_AUTHENTICATED, (event) => callback(event.payload.user));
|
|
68
|
+
}
|
|
69
|
+
/** Fires when the session ends: signed out, revoked by the server, or expired. */
|
|
70
|
+
function onSignedOut(callback) {
|
|
71
|
+
return listen(EVENT_SIGNED_OUT, (event) => callback(event.payload));
|
|
72
|
+
}
|
|
73
|
+
/** Fires when a background sign-in (deep link or loopback) fails. */
|
|
74
|
+
function onAuthError(callback) {
|
|
75
|
+
return listen(EVENT_ERROR, (event) => callback(event.payload));
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/client/fetch.ts
|
|
79
|
+
const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([
|
|
80
|
+
101,
|
|
81
|
+
204,
|
|
82
|
+
205,
|
|
83
|
+
304
|
|
84
|
+
]);
|
|
85
|
+
const MAX_ID = 4294967295;
|
|
86
|
+
let nextId = 1;
|
|
87
|
+
function takeId() {
|
|
88
|
+
const id = nextId;
|
|
89
|
+
nextId = nextId >= MAX_ID ? 1 : nextId + 1;
|
|
90
|
+
return id;
|
|
91
|
+
}
|
|
92
|
+
function isTextual(contentType) {
|
|
93
|
+
if (!contentType) return false;
|
|
94
|
+
const value = contentType.toLowerCase();
|
|
95
|
+
return value.startsWith("text/") || value.includes("json") || value.includes("xml") || value.includes("javascript") || value.includes("x-www-form-urlencoded");
|
|
96
|
+
}
|
|
97
|
+
function toBase64(bytes) {
|
|
98
|
+
let binary = "";
|
|
99
|
+
for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
100
|
+
return btoa(binary);
|
|
101
|
+
}
|
|
102
|
+
function fromBase64(data) {
|
|
103
|
+
const binary = atob(data);
|
|
104
|
+
const bytes = new Uint8Array(binary.length);
|
|
105
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
106
|
+
return bytes;
|
|
107
|
+
}
|
|
108
|
+
async function encodeBody(request) {
|
|
109
|
+
if (request.method === "GET" || request.method === "HEAD") return null;
|
|
110
|
+
const bytes = new Uint8Array(await request.arrayBuffer());
|
|
111
|
+
if (bytes.byteLength === 0) return null;
|
|
112
|
+
if (isTextual(request.headers.get("content-type"))) try {
|
|
113
|
+
return {
|
|
114
|
+
encoding: "text",
|
|
115
|
+
data: new TextDecoder("utf-8", { fatal: true }).decode(bytes)
|
|
116
|
+
};
|
|
117
|
+
} catch {}
|
|
118
|
+
return {
|
|
119
|
+
encoding: "base64",
|
|
120
|
+
data: toBase64(bytes)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function abortError(signal) {
|
|
124
|
+
return signal?.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A `fetch` that sends the request through the plugin's Rust bridge.
|
|
128
|
+
*
|
|
129
|
+
* Rust adds the session's bearer token and your app headers, refuses anything
|
|
130
|
+
* outside the configured auth operations and API routes, never follows
|
|
131
|
+
* redirects, and strips `set-cookie`/`set-auth-token` from responses. The
|
|
132
|
+
* webview never sees a token.
|
|
133
|
+
*
|
|
134
|
+
* Use it as Better Auth's `customFetchImpl` (see `withTauri`) and as the
|
|
135
|
+
* `fetch` of your API client. Bodies are buffered (no streaming).
|
|
136
|
+
*/
|
|
137
|
+
const tauriFetch = async (input, init) => {
|
|
138
|
+
const request = new Request(input, init);
|
|
139
|
+
const signal = init?.signal ?? request.signal;
|
|
140
|
+
if (signal?.aborted) throw abortError(signal);
|
|
141
|
+
const id = takeId();
|
|
142
|
+
const headers = [];
|
|
143
|
+
request.headers.forEach((value, name) => headers.push([name, value]));
|
|
144
|
+
const body = await encodeBody(request);
|
|
145
|
+
const onAbort = () => {
|
|
146
|
+
invoke("plugin:better-auth|fetch_cancel", { id }).catch(() => {});
|
|
147
|
+
};
|
|
148
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
149
|
+
try {
|
|
150
|
+
const response = await invoke("plugin:better-auth|fetch", { request: {
|
|
151
|
+
id,
|
|
152
|
+
url: request.url,
|
|
153
|
+
method: request.method,
|
|
154
|
+
headers,
|
|
155
|
+
body
|
|
156
|
+
} });
|
|
157
|
+
const payload = response.body === null || NULL_BODY_STATUSES.has(response.status) ? null : response.body.encoding === "text" ? response.body.data : fromBase64(response.body.data);
|
|
158
|
+
return new Response(payload, {
|
|
159
|
+
status: response.status,
|
|
160
|
+
statusText: response.statusText,
|
|
161
|
+
headers: response.headers
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (signal?.aborted) throw abortError(signal);
|
|
165
|
+
if ((typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0) === "ABORTED") throw abortError(signal);
|
|
166
|
+
const message = typeof error === "object" && error !== null && "message" in error ? String(error.message) : String(error);
|
|
167
|
+
throw new TypeError(`Failed to fetch: ${message}`, { cause: error });
|
|
168
|
+
} finally {
|
|
169
|
+
signal?.removeEventListener("abort", onAbort);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/client/plugin.ts
|
|
174
|
+
/**
|
|
175
|
+
* Better Auth client plugin for the Tauri webview. Refreshes `useSession()`
|
|
176
|
+
* when a browser sign-in completes or the session ends, and adds
|
|
177
|
+
* `authClient.tauri.*` actions. Added for you by {@link withTauri}.
|
|
178
|
+
*/
|
|
179
|
+
const tauriClient = () => ({
|
|
180
|
+
id: "tauri",
|
|
181
|
+
getActions: (_$fetch, $store) => {
|
|
182
|
+
if (typeof window !== "undefined" && isTauri()) {
|
|
183
|
+
const refresh = () => $store.notify("$sessionSignal");
|
|
184
|
+
onAuthenticated(refresh);
|
|
185
|
+
onSignedOut(refresh);
|
|
186
|
+
}
|
|
187
|
+
return { tauri: {
|
|
188
|
+
/** Opens the system browser on the sign-in page. */
|
|
189
|
+
requestAuth,
|
|
190
|
+
/** Completes a sign-in with a pasted code or link. */
|
|
191
|
+
authenticate,
|
|
192
|
+
/** Cancels the sign-in in progress. */
|
|
193
|
+
cancelAuth,
|
|
194
|
+
/** The current session from Rust (with offline fallback). */
|
|
195
|
+
getSession,
|
|
196
|
+
/** Signs out locally and on the server. */
|
|
197
|
+
signOut
|
|
198
|
+
} };
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
/**
|
|
202
|
+
* Adapts Better Auth client options for the Tauri webview. Pass the result to
|
|
203
|
+
* the `createAuthClient` of your framework.
|
|
204
|
+
*
|
|
205
|
+
* - every request goes through the Rust bridge (`tauriFetch`): the token is
|
|
206
|
+
* added in Rust and never reaches the page;
|
|
207
|
+
* - Better Auth's redirect plugin is disabled, so no response can navigate
|
|
208
|
+
* the webview away from the app (start social sign-in with
|
|
209
|
+
* `authClient.tauri.requestAuth({ provider })` instead);
|
|
210
|
+
* - `useSession()` refreshes after browser sign-ins and sign-outs.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* import { createAuthClient } from 'better-auth/react';
|
|
215
|
+
* import { emailOTPClient } from 'better-auth/client/plugins';
|
|
216
|
+
* import { asTauriClient, withTauri } from 'tauri-plugin-better-auth/client';
|
|
217
|
+
*
|
|
218
|
+
* export const authClient = asTauriClient(
|
|
219
|
+
* createAuthClient(withTauri({ baseURL: 'https://api.example.com/api/auth', plugins: [emailOTPClient()] })),
|
|
220
|
+
* );
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
function withTauri(options) {
|
|
224
|
+
return {
|
|
225
|
+
...options,
|
|
226
|
+
disableDefaultFetchPlugins: true,
|
|
227
|
+
fetchOptions: {
|
|
228
|
+
...options.fetchOptions,
|
|
229
|
+
customFetchImpl: tauriFetch
|
|
230
|
+
},
|
|
231
|
+
plugins: [...options.plugins ?? [], tauriClient()]
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/client/types.ts
|
|
236
|
+
/**
|
|
237
|
+
* Narrows a client created with `withTauri` to {@link TauriAuthClient}.
|
|
238
|
+
* Runtime no-op: the bridge already enforces all of this.
|
|
239
|
+
*/
|
|
240
|
+
function asTauriClient(client) {
|
|
241
|
+
return client;
|
|
242
|
+
}
|
|
243
|
+
//#endregion
|
|
244
|
+
export { EVENT_AUTHENTICATED, EVENT_ERROR, EVENT_SIGNED_OUT, TauriAuthError, asTauriClient, authenticate, cancelAuth, getSession, isTauriAuthError, onAuthError, onAuthenticated, onSignedOut, requestAuth, signOut, tauriClient, tauriFetch, withTauri };
|