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.
- package/LICENSE +21 -0
- package/README.md +50 -2
- package/dist/client.d.ts +232 -0
- package/dist/client.js +244 -0
- package/dist/index-DfaAtWTX.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 +94 -4
package/dist/proxy.d.ts
ADDED
|
@@ -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
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|