tauri-plugin-better-auth 0.0.0-stage → 0.2.0-alpha.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.
@@ -0,0 +1,124 @@
1
+ import { i as CallbackTarget, t as tauri } from "./index-DfaAtWTX.js";
2
+ import { BetterFetch, BetterFetchOption } from "@better-fetch/fetch";
3
+ //#region src/proxy.d.ts
4
+ /** Options of {@link tauriProxyClient}. */
5
+ export interface TauriProxyClientOptions {
6
+ /** Custom URI scheme the app registers, e.g. `"com.example.app"`. */
7
+ scheme: string;
8
+ /**
9
+ * Must match the server plugin and the Rust plugin.
10
+ *
11
+ * @default "tauri"
12
+ */
13
+ clientID?: string | undefined;
14
+ /**
15
+ * Path of the app callback, `<scheme>://auth/callback`.
16
+ *
17
+ * @default "/auth/callback"
18
+ */
19
+ callbackPath?: string | undefined;
20
+ /**
21
+ * Social providers the page may start automatically when the app asks for
22
+ * one (`requestAuth({ provider })`). Other values are ignored.
23
+ */
24
+ providers?: string[] | undefined;
25
+ /**
26
+ * Seconds a captured request survives in `sessionStorage`, covering OAuth
27
+ * round trips in the same tab.
28
+ *
29
+ * @default 600
30
+ */
31
+ requestTTL?: number | undefined;
32
+ }
33
+ /** A desktop sign-in request captured from the hand-off URL. */
34
+ export interface TauriAuthRequest {
35
+ clientId: string;
36
+ state: string;
37
+ codeChallenge: string;
38
+ /** Loopback redirect chosen by the app, or `null` for the custom scheme. */
39
+ redirectUri: string | null;
40
+ /** Allowed social provider the app asked for, or `null`. */
41
+ provider: string | null;
42
+ /** Wire protocol version requested by the app. */
43
+ version: number;
44
+ /** Epoch milliseconds when the request was captured. */
45
+ capturedAt: number;
46
+ }
47
+ /** Why a hand-off URL was rejected. */
48
+ export type TauriRequestProblem = 'client_id' | 'state' | 'code_challenge' | 'redirect_uri' | 'version' | 'expired';
49
+ /** Result of {@link TauriProxyActions.getRequest}. */
50
+ export type TauriRequestResult = {
51
+ status: 'ok';
52
+ request: TauriAuthRequest;
53
+ } | {
54
+ status: 'none';
55
+ } | {
56
+ status: 'invalid';
57
+ problem: TauriRequestProblem;
58
+ };
59
+ /** Result of {@link TauriProxyActions.handoff}. */
60
+ export type TauriHandoffResult = {
61
+ data: {
62
+ code: string;
63
+ redirectTo: string;
64
+ };
65
+ error: null;
66
+ } | {
67
+ data: null;
68
+ error: {
69
+ status: number;
70
+ code?: string | undefined;
71
+ message: string;
72
+ };
73
+ };
74
+ /** Actions added under `authClient.tauri`. */
75
+ export interface TauriProxyActions {
76
+ /**
77
+ * Reads the desktop sign-in request from the current URL (and stores it in
78
+ * `sessionStorage`, then removes it from the address bar), or from storage
79
+ * on later page loads in the same tab.
80
+ */
81
+ getRequest(): TauriRequestResult;
82
+ /**
83
+ * After the user has signed in **and consented**, mints a single-use code
84
+ * for the app and returns the URL that hands it back. Call it from a click
85
+ * handler, then pass `redirectTo` to {@link openApp}.
86
+ */
87
+ handoff(fetchOptions?: BetterFetchOption | undefined): Promise<TauriHandoffResult>;
88
+ /**
89
+ * The URL that tells the app the user cancelled (`error=access_denied`),
90
+ * or `null` when there is no request. Clears the request.
91
+ */
92
+ cancelLink(): string | null;
93
+ /** Navigates to a URL returned by {@link handoff} or {@link cancelLink}. */
94
+ openApp(url: string): void;
95
+ /** Forgets the stored request. */
96
+ clear(): void;
97
+ }
98
+ /**
99
+ * Better Auth client plugin for the web page that hands a sign-in to a Tauri
100
+ * app. Add it to the auth client used by that page.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const authClient = createAuthClient({
105
+ * plugins: [tauriProxyClient({ scheme: 'com.example.app', clientID: 'com.example.app' })],
106
+ * });
107
+ * const result = authClient.tauri.getRequest();
108
+ * // …after sign-in and the user's click on "Continue":
109
+ * const { data } = await authClient.tauri.handoff();
110
+ * if (data) authClient.tauri.openApp(data.redirectTo);
111
+ * ```
112
+ */
113
+ export declare const tauriProxyClient: (options: TauriProxyClientOptions) => {
114
+ id: "tauri-proxy";
115
+ $InferServerPlugin: ReturnType<typeof tauri>;
116
+ pathMethods: {
117
+ '/tauri/authorize': "POST";
118
+ };
119
+ getActions: ($fetch: BetterFetch) => {
120
+ tauri: TauriProxyActions;
121
+ };
122
+ };
123
+ //#endregion
124
+ export type { CallbackTarget };
package/dist/proxy.js ADDED
@@ -0,0 +1,217 @@
1
+ import { _ as normalizeCallbackPath, f as assertClientId, h as isLoopbackRedirectUri, l as PROVIDER_PATTERN, m as buildCallbackUrl, n as CODE_CHALLENGE_PATTERN, p as assertScheme, t as CLIENT_ID_PATTERN, u as STATE_PATTERN } from "./protocol-Fcr7NzIh.js";
2
+ //#region src/proxy.ts
3
+ const QUERY_KEYS = [
4
+ "client_id",
5
+ "state",
6
+ "code_challenge",
7
+ "code_challenge_method",
8
+ "redirect_uri",
9
+ "provider",
10
+ "v"
11
+ ];
12
+ function resolve(options) {
13
+ const clientID = assertClientId(options.clientID ?? "tauri");
14
+ return {
15
+ scheme: assertScheme(options.scheme),
16
+ clientID,
17
+ callbackPath: normalizeCallbackPath(options.callbackPath ?? "/auth/callback"),
18
+ providers: new Set(options.providers ?? []),
19
+ requestTTL: options.requestTTL ?? 600,
20
+ storageKey: `tauri-plugin-better-auth:request:${clientID}`
21
+ };
22
+ }
23
+ /** Parses the hand-off query string. Pure: safe to call anywhere. */
24
+ function parseRequest(search, opts, now) {
25
+ const params = new URLSearchParams(search);
26
+ if (!params.has("client_id")) return { status: "none" };
27
+ const clientId = params.get("client_id") ?? "";
28
+ if (!CLIENT_ID_PATTERN.test(clientId) || clientId !== opts.clientID) return {
29
+ status: "invalid",
30
+ problem: "client_id"
31
+ };
32
+ const version = Number(params.get("v") ?? 1);
33
+ if (version !== 1) return {
34
+ status: "invalid",
35
+ problem: "version"
36
+ };
37
+ const state = params.get("state") ?? "";
38
+ if (!STATE_PATTERN.test(state)) return {
39
+ status: "invalid",
40
+ problem: "state"
41
+ };
42
+ const codeChallenge = params.get("code_challenge") ?? "";
43
+ const method = params.get("code_challenge_method");
44
+ if (!CODE_CHALLENGE_PATTERN.test(codeChallenge) || method !== null && method !== "S256") return {
45
+ status: "invalid",
46
+ problem: "code_challenge"
47
+ };
48
+ const redirectUri = params.get("redirect_uri");
49
+ if (redirectUri !== null && !isLoopbackRedirectUri(redirectUri, opts.callbackPath)) return {
50
+ status: "invalid",
51
+ problem: "redirect_uri"
52
+ };
53
+ const rawProvider = params.get("provider");
54
+ return {
55
+ status: "ok",
56
+ request: {
57
+ clientId,
58
+ state,
59
+ codeChallenge,
60
+ redirectUri,
61
+ provider: rawProvider !== null && PROVIDER_PATTERN.test(rawProvider) && opts.providers.has(rawProvider) ? rawProvider : null,
62
+ version,
63
+ capturedAt: now
64
+ }
65
+ };
66
+ }
67
+ function storage() {
68
+ try {
69
+ return typeof window === "undefined" ? null : window.sessionStorage;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+ function readStored(opts, now) {
75
+ const raw = storage()?.getItem(opts.storageKey);
76
+ if (!raw) return { status: "none" };
77
+ let request;
78
+ try {
79
+ request = JSON.parse(raw);
80
+ } catch {
81
+ storage()?.removeItem(opts.storageKey);
82
+ return { status: "none" };
83
+ }
84
+ if (now - request.capturedAt > opts.requestTTL * 1e3) {
85
+ storage()?.removeItem(opts.storageKey);
86
+ return {
87
+ status: "invalid",
88
+ problem: "expired"
89
+ };
90
+ }
91
+ const parsed = parseRequest(new URLSearchParams({
92
+ client_id: request.clientId,
93
+ state: request.state,
94
+ code_challenge: request.codeChallenge,
95
+ v: String(request.version),
96
+ ...request.redirectUri ? { redirect_uri: request.redirectUri } : {},
97
+ ...request.provider ? { provider: request.provider } : {}
98
+ }).toString(), opts, request.capturedAt);
99
+ if (parsed.status !== "ok") storage()?.removeItem(opts.storageKey);
100
+ return parsed;
101
+ }
102
+ function stripQuery() {
103
+ if (typeof window === "undefined") return;
104
+ const url = new URL(window.location.href);
105
+ for (const key of QUERY_KEYS) url.searchParams.delete(key);
106
+ window.history.replaceState(window.history.state, "", url.toString());
107
+ }
108
+ function callbackTarget(request, opts) {
109
+ return request.redirectUri ? {
110
+ kind: "loopback",
111
+ redirectUri: request.redirectUri
112
+ } : {
113
+ kind: "scheme",
114
+ scheme: opts.scheme,
115
+ callbackPath: opts.callbackPath
116
+ };
117
+ }
118
+ /**
119
+ * Better Auth client plugin for the web page that hands a sign-in to a Tauri
120
+ * app. Add it to the auth client used by that page.
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * const authClient = createAuthClient({
125
+ * plugins: [tauriProxyClient({ scheme: 'com.example.app', clientID: 'com.example.app' })],
126
+ * });
127
+ * const result = authClient.tauri.getRequest();
128
+ * // …after sign-in and the user's click on "Continue":
129
+ * const { data } = await authClient.tauri.handoff();
130
+ * if (data) authClient.tauri.openApp(data.redirectTo);
131
+ * ```
132
+ */
133
+ const tauriProxyClient = (options) => {
134
+ const opts = resolve(options);
135
+ return {
136
+ id: "tauri-proxy",
137
+ $InferServerPlugin: {},
138
+ pathMethods: { "/tauri/authorize": "POST" },
139
+ getActions: ($fetch) => {
140
+ const getRequest = () => {
141
+ if (typeof window === "undefined") return { status: "none" };
142
+ const now = Date.now();
143
+ const fromUrl = parseRequest(window.location.search, opts, now);
144
+ if (fromUrl.status === "none") return readStored(opts, now);
145
+ if (fromUrl.status === "ok") storage()?.setItem(opts.storageKey, JSON.stringify(fromUrl.request));
146
+ stripQuery();
147
+ return fromUrl;
148
+ };
149
+ return { tauri: {
150
+ getRequest,
151
+ async handoff(fetchOptions) {
152
+ const current = getRequest();
153
+ if (current.status !== "ok") return {
154
+ data: null,
155
+ error: {
156
+ status: 400,
157
+ code: "NO_DESKTOP_REQUEST",
158
+ message: current.status === "invalid" ? `The desktop sign-in request is invalid (${current.problem}). Start again from the app.` : "There is no desktop sign-in request. Start again from the app."
159
+ }
160
+ };
161
+ const { request } = current;
162
+ const res = await $fetch("/tauri/authorize", {
163
+ ...fetchOptions,
164
+ method: "POST",
165
+ body: {
166
+ client_id: request.clientId,
167
+ state: request.state,
168
+ code_challenge: request.codeChallenge,
169
+ code_challenge_method: "S256"
170
+ }
171
+ });
172
+ if (res.error || !res.data) {
173
+ const error = res.error;
174
+ return {
175
+ data: null,
176
+ error: {
177
+ status: error?.status ?? 500,
178
+ code: error?.code,
179
+ message: error?.message ?? "Authorization failed"
180
+ }
181
+ };
182
+ }
183
+ storage()?.removeItem(opts.storageKey);
184
+ return {
185
+ data: {
186
+ code: res.data.code,
187
+ redirectTo: buildCallbackUrl(callbackTarget(request, opts), {
188
+ code: res.data.code,
189
+ state: request.state
190
+ })
191
+ },
192
+ error: null
193
+ };
194
+ },
195
+ cancelLink() {
196
+ const current = getRequest();
197
+ if (current.status !== "ok") return null;
198
+ storage()?.removeItem(opts.storageKey);
199
+ return buildCallbackUrl(callbackTarget(current.request, opts), {
200
+ error: "access_denied",
201
+ state: current.request.state
202
+ });
203
+ },
204
+ openApp(url) {
205
+ if (typeof window === "undefined") return;
206
+ if (!(url.startsWith(`${opts.scheme}://`) || url.startsWith("http://127.0.0.1:") || url.startsWith("http://[::1]:"))) throw new Error("openApp only navigates to the app callback returned by handoff() or cancelLink().");
207
+ window.location.href = url;
208
+ },
209
+ clear() {
210
+ storage()?.removeItem(opts.storageKey);
211
+ }
212
+ } };
213
+ }
214
+ };
215
+ };
216
+ //#endregion
217
+ export { tauriProxyClient };
package/package.json CHANGED
@@ -1,6 +1,96 @@
1
1
  {
2
2
  "name": "tauri-plugin-better-auth",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0-alpha.0",
4
+ "description": "Better Auth for Tauri 3: browser sign-in with PKCE, a Rust-held session and a token-free webview.",
5
+ "keywords": [
6
+ "auth",
7
+ "authentication",
8
+ "better-auth",
9
+ "deep-link",
10
+ "desktop",
11
+ "pkce",
12
+ "tauri",
13
+ "tauri-plugin"
14
+ ],
15
+ "homepage": "https://github.com/laakal/tauri-plugin-better-auth#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/laakal/tauri-plugin-better-auth/issues"
18
+ },
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/laakal/tauri-plugin-better-auth.git",
23
+ "directory": "packages/tauri-plugin-better-auth"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "type": "module",
31
+ "sideEffects": false,
32
+ "main": "./dist/index.js",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "default": "./dist/index.js"
39
+ },
40
+ "./proxy": {
41
+ "types": "./dist/proxy.d.ts",
42
+ "default": "./dist/proxy.js"
43
+ },
44
+ "./client": {
45
+ "types": "./dist/client.d.ts",
46
+ "default": "./dist/client.js"
47
+ },
48
+ "./package.json": "./package.json"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public",
52
+ "provenance": true,
53
+ "tag": "next"
54
+ },
55
+ "scripts": {
56
+ "build": "tsdown",
57
+ "dev": "tsdown --watch",
58
+ "lint:package": "publint --strict && attw --pack . --profile esm-only",
59
+ "test": "vitest run",
60
+ "typecheck": "tsc --noEmit -p tsconfig.json"
61
+ },
62
+ "dependencies": {
63
+ "zod": "4.6.7"
64
+ },
65
+ "devDependencies": {
66
+ "@arethetypeswrong/cli": "0.18.5",
67
+ "@better-auth/core": "1.7.11",
68
+ "@better-fetch/fetch": "1.3.2",
69
+ "@tauri-apps/api": "3.0.0-alpha.3",
70
+ "@types/node": "24.19.2",
71
+ "@types/pg": "8.23.1",
72
+ "better-auth": "1.7.11",
73
+ "better-call": "1.4.2",
74
+ "happy-dom": "21.0.3",
75
+ "pg": "8.23.1",
76
+ "publint": "0.3.25",
77
+ "tsdown": "0.23.1",
78
+ "typescript": "7.0.2",
79
+ "vitest": "5.0.5"
80
+ },
81
+ "peerDependencies": {
82
+ "@better-auth/core": ">=1.7.4 <2",
83
+ "@better-fetch/fetch": "^1.3.1",
84
+ "@tauri-apps/api": ">=3.0.0-alpha.0 <4",
85
+ "better-auth": ">=1.7.4 <2",
86
+ "better-call": "^1.4.0"
87
+ },
88
+ "peerDependenciesMeta": {
89
+ "@tauri-apps/api": {
90
+ "optional": true
91
+ }
92
+ },
93
+ "engines": {
94
+ "node": ">=22"
95
+ }
96
+ }