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.
@@ -0,0 +1,292 @@
1
+ import "zod";
2
+ import { User } from "better-auth";
3
+ //#region src/types.d.ts
4
+ /** Options of the {@link tauri} server plugin. */
5
+ interface TauriOptions {
6
+ /**
7
+ * Identifies the desktop app. Must match the Rust plugin and the hand-off
8
+ * page client.
9
+ *
10
+ * @default "tauri"
11
+ */
12
+ clientID?: string | undefined;
13
+ /**
14
+ * Seconds an authorization code stays valid. Codes are single-use.
15
+ *
16
+ * @default 300
17
+ */
18
+ codeExpiresIn?: number | undefined;
19
+ /**
20
+ * Origins allowed to call `POST /tauri/authorize`, normally just the origin
21
+ * of your hand-off page. When unset, any origin Better Auth trusts may call
22
+ * it, which widens the attack surface to every trusted site.
23
+ */
24
+ authorizeOrigins?: string[] | undefined;
25
+ /**
26
+ * Rate limit for both `/tauri/*` endpoints, per client IP. Better Auth only
27
+ * enforces rate limits when its own `rateLimit` is enabled (by default in
28
+ * production). Pass `false` to rely on your global rules instead.
29
+ *
30
+ * @default { window: 60, max: 10 }
31
+ */
32
+ rateLimit?: {
33
+ window: number;
34
+ max: number;
35
+ } | false | undefined;
36
+ }
37
+ /** Fully resolved options, exposed as `plugin.options`. */
38
+ interface ResolvedTauriOptions {
39
+ clientID: string;
40
+ codeExpiresIn: number;
41
+ authorizeOrigins: string[] | undefined;
42
+ rateLimit: {
43
+ window: number;
44
+ max: number;
45
+ } | false;
46
+ }
47
+ //#endregion
48
+ //#region src/error-codes.d.ts
49
+ /**
50
+ * Error codes returned by the `/tauri/*` endpoints. Stable within protocol v1:
51
+ * the Rust plugin and apps switch on `code`, never on `message`.
52
+ */
53
+ declare const TAURI_ERROR_CODES: {
54
+ INVALID_CLIENT_ID: import("better-auth").RawError<"INVALID_CLIENT_ID">;
55
+ INVALID_CODE_CHALLENGE: import("better-auth").RawError<"INVALID_CODE_CHALLENGE">;
56
+ INVALID_CODE_VERIFIER: import("better-auth").RawError<"INVALID_CODE_VERIFIER">;
57
+ INVALID_OR_EXPIRED_CODE: import("better-auth").RawError<"INVALID_OR_EXPIRED_CODE">;
58
+ INVALID_STATE: import("better-auth").RawError<"INVALID_STATE">;
59
+ ORIGIN_NOT_ALLOWED: import("better-auth").RawError<"ORIGIN_NOT_ALLOWED">;
60
+ PKCE_VERIFICATION_FAILED: import("better-auth").RawError<"PKCE_VERIFICATION_FAILED">;
61
+ SESSION_REJECTED: import("better-auth").RawError<"SESSION_REJECTED">;
62
+ STATE_MISMATCH: import("better-auth").RawError<"STATE_MISMATCH">;
63
+ USER_NOT_FOUND: import("better-auth").RawError<"USER_NOT_FOUND">;
64
+ };
65
+ //#endregion
66
+ //#region src/protocol.d.ts
67
+ /**
68
+ * Wire protocol v1, shared by the server plugin, the web hand-off client and
69
+ * the Rust plugin. Documented in PROTOCOL.md. Within v1, changes are additive
70
+ * only: installed desktop apps lag behind servers and must keep working.
71
+ *
72
+ * This module has no runtime dependencies so every entry point can import it.
73
+ */
74
+ /** Protocol version sent as `v` on the sign-in URL and returned by `/tauri/token`. */
75
+ declare const PROTOCOL_VERSION = 1;
76
+ /** Thrown when a configuration value breaks a protocol rule. */
77
+ declare class TauriConfigError extends Error {
78
+ name: string;
79
+ }
80
+ /** Returns true for a custom URI scheme the app can register. */
81
+ declare function isValidScheme(scheme: string): boolean;
82
+ /**
83
+ * Returns true when `uri` is a loopback redirect the hand-off page may follow:
84
+ * exactly `http://127.0.0.1:<port><callbackPath>` or
85
+ * `http://[::1]:<port><callbackPath>`, with no userinfo, query or fragment.
86
+ *
87
+ * Anything else is rejected so that a crafted sign-in link cannot send an
88
+ * authorization code to a remote server (RFC 8252 §7.3, §8.3).
89
+ */
90
+ declare function isLoopbackRedirectUri(uri: string, callbackPath: string): boolean;
91
+ /** Where the hand-off page sends the browser after authorizing. */
92
+ type CallbackTarget = {
93
+ kind: 'scheme';
94
+ scheme: string;
95
+ callbackPath: string;
96
+ } | {
97
+ kind: 'loopback';
98
+ redirectUri: string;
99
+ };
100
+ //#endregion
101
+ //#region src/routes.d.ts
102
+ /** Response body of `POST /tauri/token`. */
103
+ interface TauriTokenResponse {
104
+ /** Session token, used as `Authorization: Bearer` by the app. */
105
+ token: string;
106
+ /** The signed-in user, filtered by `parseUserOutput`. */
107
+ user: User & Record<string, unknown>;
108
+ /** Wire protocol version (see PROTOCOL.md). */
109
+ protocol: typeof PROTOCOL_VERSION;
110
+ }
111
+ //#endregion
112
+ //#region src/version.d.ts
113
+ /**
114
+ * Package version. Released in lockstep with the Rust crate; CI fails when this
115
+ * constant, `package.json` and `Cargo.toml` disagree.
116
+ */
117
+ declare const VERSION = "0.1.0";
118
+ //#endregion
119
+ //#region src/index.d.ts
120
+ /** What `init` needs from Better Auth's context. */
121
+ interface InitContext {
122
+ hasPlugin(pluginId: string): boolean;
123
+ logger: {
124
+ warn(message: string, ...args: unknown[]): void;
125
+ };
126
+ }
127
+ /**
128
+ * Better Auth server plugin for Tauri desktop apps.
129
+ *
130
+ * Adds two endpoints:
131
+ * - `POST /tauri/authorize`: called by your hand-off web page for the signed-in
132
+ * user; returns a single-use code bound to the app's PKCE challenge.
133
+ * - `POST /tauri/token`: called by the app's Rust plugin; exchanges the code
134
+ * and PKCE verifier for a new session created in the app's own request.
135
+ *
136
+ * Pair it with the `bearer()` plugin: the app authenticates with
137
+ * `Authorization: Bearer`, never with cookies.
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * import { betterAuth } from 'better-auth';
142
+ * import { bearer } from 'better-auth/plugins';
143
+ * import { tauri } from 'tauri-plugin-better-auth';
144
+ *
145
+ * export const auth = betterAuth({
146
+ * plugins: [
147
+ * bearer(),
148
+ * tauri({ clientID: 'com.example.app', authorizeOrigins: ['https://example.com'] }),
149
+ * ],
150
+ * });
151
+ * ```
152
+ */
153
+ declare const tauri: (options?: TauriOptions) => {
154
+ id: "tauri";
155
+ version: string;
156
+ init(ctx: InitContext): void;
157
+ endpoints: {
158
+ tauriAuthorize: import("better-call").StrictEndpoint<"/tauri/authorize", {
159
+ method: "POST";
160
+ body: import("zod").ZodObject<{
161
+ client_id: import("zod").ZodString;
162
+ state: import("zod").ZodString;
163
+ code_challenge: import("zod").ZodString;
164
+ code_challenge_method: import("zod").ZodOptional<import("zod").ZodString>;
165
+ }, import("zod/v4/core").$strip>;
166
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
167
+ session: {
168
+ session: Record<string, any> & {
169
+ id: string;
170
+ createdAt: Date;
171
+ updatedAt: Date;
172
+ userId: string;
173
+ expiresAt: Date;
174
+ token: string;
175
+ ipAddress?: string | null | undefined;
176
+ userAgent?: string | null | undefined;
177
+ };
178
+ user: Record<string, any> & {
179
+ id: string;
180
+ createdAt: Date;
181
+ updatedAt: Date;
182
+ email: string;
183
+ emailVerified: boolean;
184
+ name: string;
185
+ image?: string | null | undefined;
186
+ };
187
+ };
188
+ }>>[];
189
+ allowedMediaTypes: string[];
190
+ metadata: {
191
+ noStore: boolean;
192
+ openapi: {
193
+ operationId: string;
194
+ description: string;
195
+ responses: {
196
+ 200: {
197
+ description: string;
198
+ content: {
199
+ 'application/json': {
200
+ schema: {
201
+ type: "object";
202
+ properties: {
203
+ code: {
204
+ type: string;
205
+ };
206
+ };
207
+ required: string[];
208
+ };
209
+ };
210
+ };
211
+ };
212
+ };
213
+ };
214
+ };
215
+ }, {
216
+ code: string;
217
+ }>;
218
+ tauriToken: import("better-call").StrictEndpoint<"/tauri/token", {
219
+ method: "POST";
220
+ body: import("zod").ZodObject<{
221
+ client_id: import("zod").ZodString;
222
+ code: import("zod").ZodString;
223
+ state: import("zod").ZodString;
224
+ code_verifier: import("zod").ZodString;
225
+ }, import("zod/v4/core").$strip>;
226
+ allowedMediaTypes: string[];
227
+ metadata: {
228
+ scope: "http";
229
+ noStore: boolean;
230
+ openapi: {
231
+ operationId: string;
232
+ description: string;
233
+ responses: {
234
+ 200: {
235
+ description: string;
236
+ content: {
237
+ 'application/json': {
238
+ schema: {
239
+ type: "object";
240
+ properties: {
241
+ token: {
242
+ type: string;
243
+ };
244
+ user: {
245
+ $ref: string;
246
+ };
247
+ protocol: {
248
+ type: string;
249
+ };
250
+ };
251
+ required: string[];
252
+ };
253
+ };
254
+ };
255
+ };
256
+ };
257
+ };
258
+ };
259
+ }, {
260
+ token: string;
261
+ user: import("better-auth").User & Record<string, unknown>;
262
+ protocol: 1;
263
+ }>;
264
+ };
265
+ rateLimit: {
266
+ window: number;
267
+ max: number;
268
+ pathMatcher: (path: string) => boolean;
269
+ }[] | undefined;
270
+ options: ResolvedTauriOptions;
271
+ $ERROR_CODES: {
272
+ INVALID_CLIENT_ID: import("better-auth").RawError<"INVALID_CLIENT_ID">;
273
+ INVALID_CODE_CHALLENGE: import("better-auth").RawError<"INVALID_CODE_CHALLENGE">;
274
+ INVALID_CODE_VERIFIER: import("better-auth").RawError<"INVALID_CODE_VERIFIER">;
275
+ INVALID_OR_EXPIRED_CODE: import("better-auth").RawError<"INVALID_OR_EXPIRED_CODE">;
276
+ INVALID_STATE: import("better-auth").RawError<"INVALID_STATE">;
277
+ ORIGIN_NOT_ALLOWED: import("better-auth").RawError<"ORIGIN_NOT_ALLOWED">;
278
+ PKCE_VERIFICATION_FAILED: import("better-auth").RawError<"PKCE_VERIFICATION_FAILED">;
279
+ SESSION_REJECTED: import("better-auth").RawError<"SESSION_REJECTED">;
280
+ STATE_MISMATCH: import("better-auth").RawError<"STATE_MISMATCH">;
281
+ USER_NOT_FOUND: import("better-auth").RawError<"USER_NOT_FOUND">;
282
+ };
283
+ };
284
+ declare module '@better-auth/core' {
285
+ interface BetterAuthPluginRegistry<AuthOptions, Options> {
286
+ tauri: {
287
+ creator: typeof tauri;
288
+ };
289
+ }
290
+ }
291
+ //#endregion
292
+ export { PROTOCOL_VERSION as a, isValidScheme as c, TauriOptions as d, CallbackTarget as i, TAURI_ERROR_CODES as l, VERSION as n, TauriConfigError as o, TauriTokenResponse as r, isLoopbackRedirectUri as s, tauri as t, ResolvedTauriOptions as u };
@@ -0,0 +1,2 @@
1
+ import { a as PROTOCOL_VERSION, c as isValidScheme, d as TauriOptions, l as TAURI_ERROR_CODES, n as VERSION, o as TauriConfigError, r as TauriTokenResponse, s as isLoopbackRedirectUri, t as tauri, u as ResolvedTauriOptions } from "./index-DYZxnm2f.js";
2
+ export { PROTOCOL_VERSION, type ResolvedTauriOptions, TAURI_ERROR_CODES, TauriConfigError, type TauriOptions, type TauriTokenResponse, VERSION, isLoopbackRedirectUri, isValidScheme, tauri };
package/dist/index.js ADDED
@@ -0,0 +1,235 @@
1
+ import { a as CODE_VERIFIER_PATTERN, c as PROTOCOL_VERSION, d as TauriConfigError, f as assertClientId, g as isValidScheme, h as isLoopbackRedirectUri, i as CODE_PATTERN, n as CODE_CHALLENGE_PATTERN, r as CODE_IDENTIFIER_PREFIX, u as STATE_PATTERN } from "./protocol-Fcr7NzIh.js";
2
+ import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
3
+ import { APIError } from "@better-auth/core/error";
4
+ import { createAuthEndpoint, sensitiveSessionMiddleware } from "better-auth/api";
5
+ import { setSessionCookie } from "better-auth/cookies";
6
+ import { constantTimeEqual, generateRandomString } from "better-auth/crypto";
7
+ import { parseUserOutput } from "better-auth/db";
8
+ import { generateCodeChallenge } from "better-auth/oauth2";
9
+ import * as z from "zod";
10
+ //#region src/error-codes.ts
11
+ /**
12
+ * Error codes returned by the `/tauri/*` endpoints. Stable within protocol v1:
13
+ * the Rust plugin and apps switch on `code`, never on `message`.
14
+ */
15
+ const TAURI_ERROR_CODES = defineErrorCodes({
16
+ INVALID_CLIENT_ID: "Unknown client id",
17
+ ORIGIN_NOT_ALLOWED: "This origin may not authorize a desktop sign-in",
18
+ INVALID_STATE: "Invalid state parameter",
19
+ INVALID_CODE_CHALLENGE: "Invalid PKCE code challenge (S256 required)",
20
+ INVALID_CODE_VERIFIER: "Invalid PKCE code verifier",
21
+ INVALID_OR_EXPIRED_CODE: "Invalid or expired authorization code",
22
+ STATE_MISMATCH: "State does not match the authorization request",
23
+ PKCE_VERIFICATION_FAILED: "PKCE verification failed",
24
+ USER_NOT_FOUND: "The user for this authorization no longer exists",
25
+ SESSION_REJECTED: "The server rejected the new session"
26
+ });
27
+ //#endregion
28
+ //#region src/routes.ts
29
+ const authorizeBody = z.object({
30
+ client_id: z.string().max(64),
31
+ state: z.string().max(256),
32
+ code_challenge: z.string().max(256),
33
+ code_challenge_method: z.string().max(16).optional()
34
+ });
35
+ const tokenBody = z.object({
36
+ client_id: z.string().max(64),
37
+ code: z.string().max(256),
38
+ state: z.string().max(256),
39
+ code_verifier: z.string().max(256)
40
+ });
41
+ /** What `/tauri/authorize` stores under `tauri:<code>`. */
42
+ const storedGrant = z.object({
43
+ userId: z.string().min(1),
44
+ clientId: z.string().min(1),
45
+ codeChallenge: z.string().regex(CODE_CHALLENGE_PATTERN),
46
+ state: z.string().regex(STATE_PATTERN)
47
+ });
48
+ function parseStoredGrant(value) {
49
+ try {
50
+ return storedGrant.safeParse(JSON.parse(value));
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ const createAuthorizeEndpoint = (opts) => createAuthEndpoint("/tauri/authorize", {
56
+ method: "POST",
57
+ body: authorizeBody,
58
+ use: [sensitiveSessionMiddleware],
59
+ allowedMediaTypes: ["application/json"],
60
+ metadata: {
61
+ noStore: true,
62
+ openapi: {
63
+ operationId: "tauriAuthorize",
64
+ description: "Mint a single-use code that hands the signed-in user's sign-in to a Tauri app. Called by the hand-off page after the user consents.",
65
+ responses: { 200: {
66
+ description: "A single-use authorization code",
67
+ content: { "application/json": { schema: {
68
+ type: "object",
69
+ properties: { code: { type: "string" } },
70
+ required: ["code"]
71
+ } } }
72
+ } }
73
+ }
74
+ }
75
+ }, async (ctx) => {
76
+ if (opts.authorizeOrigins && ctx.request) {
77
+ const origin = ctx.request.headers.get("origin");
78
+ if (!origin || !opts.authorizeOrigins.includes(origin)) throw APIError.from("FORBIDDEN", TAURI_ERROR_CODES.ORIGIN_NOT_ALLOWED);
79
+ }
80
+ const { client_id, state, code_challenge, code_challenge_method } = ctx.body;
81
+ if (client_id !== opts.clientID) throw APIError.from("BAD_REQUEST", TAURI_ERROR_CODES.INVALID_CLIENT_ID);
82
+ if (!STATE_PATTERN.test(state)) throw APIError.from("BAD_REQUEST", TAURI_ERROR_CODES.INVALID_STATE);
83
+ if (!CODE_CHALLENGE_PATTERN.test(code_challenge) || code_challenge_method !== void 0 && code_challenge_method !== "S256") throw APIError.from("BAD_REQUEST", TAURI_ERROR_CODES.INVALID_CODE_CHALLENGE);
84
+ const code = generateRandomString(32, "a-z", "A-Z", "0-9");
85
+ await ctx.context.internalAdapter.createVerificationValue({
86
+ identifier: `${CODE_IDENTIFIER_PREFIX}${code}`,
87
+ value: JSON.stringify({
88
+ userId: ctx.context.session.user.id,
89
+ clientId: client_id,
90
+ codeChallenge: code_challenge,
91
+ state
92
+ }),
93
+ expiresAt: new Date(Date.now() + opts.codeExpiresIn * 1e3)
94
+ });
95
+ return ctx.json({ code });
96
+ });
97
+ const createTokenEndpoint = (opts) => createAuthEndpoint("/tauri/token", {
98
+ method: "POST",
99
+ body: tokenBody,
100
+ allowedMediaTypes: ["application/json"],
101
+ metadata: {
102
+ scope: "http",
103
+ noStore: true,
104
+ openapi: {
105
+ operationId: "tauriToken",
106
+ description: "Exchange a single-use authorization code and PKCE verifier for a new session. Called by the Tauri app, never by a browser.",
107
+ responses: { 200: {
108
+ description: "The new session token and the user",
109
+ content: { "application/json": { schema: {
110
+ type: "object",
111
+ properties: {
112
+ token: { type: "string" },
113
+ user: { $ref: "#/components/schemas/User" },
114
+ protocol: { type: "number" }
115
+ },
116
+ required: [
117
+ "token",
118
+ "user",
119
+ "protocol"
120
+ ]
121
+ } } }
122
+ } }
123
+ }
124
+ }
125
+ }, async (ctx) => {
126
+ const { client_id, code, state, code_verifier } = ctx.body;
127
+ if (client_id !== opts.clientID) throw APIError.from("BAD_REQUEST", TAURI_ERROR_CODES.INVALID_CLIENT_ID);
128
+ if (!CODE_PATTERN.test(code)) throw APIError.from("BAD_REQUEST", TAURI_ERROR_CODES.INVALID_OR_EXPIRED_CODE);
129
+ if (!STATE_PATTERN.test(state)) throw APIError.from("BAD_REQUEST", TAURI_ERROR_CODES.INVALID_STATE);
130
+ if (!CODE_VERIFIER_PATTERN.test(code_verifier)) throw APIError.from("BAD_REQUEST", TAURI_ERROR_CODES.INVALID_CODE_VERIFIER);
131
+ const record = await ctx.context.internalAdapter.consumeVerificationValue(`${CODE_IDENTIFIER_PREFIX}${code}`);
132
+ if (!record) throw APIError.from("BAD_REQUEST", TAURI_ERROR_CODES.INVALID_OR_EXPIRED_CODE);
133
+ const grant = parseStoredGrant(record.value);
134
+ if (!grant?.success) throw APIError.from("BAD_REQUEST", TAURI_ERROR_CODES.INVALID_OR_EXPIRED_CODE);
135
+ if (!constantTimeEqual(grant.data.clientId, client_id)) throw APIError.from("BAD_REQUEST", TAURI_ERROR_CODES.INVALID_CLIENT_ID);
136
+ if (!constantTimeEqual(grant.data.state, state)) throw APIError.from("BAD_REQUEST", TAURI_ERROR_CODES.STATE_MISMATCH);
137
+ const challenge = await generateCodeChallenge(code_verifier);
138
+ if (!constantTimeEqual(challenge, grant.data.codeChallenge)) throw APIError.from("BAD_REQUEST", TAURI_ERROR_CODES.PKCE_VERIFICATION_FAILED);
139
+ const user = await ctx.context.internalAdapter.findUserById(grant.data.userId);
140
+ if (!user) throw APIError.from("BAD_REQUEST", TAURI_ERROR_CODES.USER_NOT_FOUND);
141
+ const session = await ctx.context.internalAdapter.createSession(user.id);
142
+ if (!session) throw APIError.from("FORBIDDEN", TAURI_ERROR_CODES.SESSION_REJECTED);
143
+ await setSessionCookie(ctx, {
144
+ session,
145
+ user
146
+ });
147
+ return ctx.json({
148
+ token: session.token,
149
+ user: parseUserOutput(ctx.context.options, user),
150
+ protocol: 1
151
+ });
152
+ });
153
+ //#endregion
154
+ //#region src/version.ts
155
+ /**
156
+ * Package version. Released in lockstep with the Rust crate; CI fails when this
157
+ * constant, `package.json` and `Cargo.toml` disagree.
158
+ */
159
+ const VERSION = "0.1.0";
160
+ //#endregion
161
+ //#region src/index.ts
162
+ function resolveOptions(options = {}) {
163
+ const clientID = assertClientId(options.clientID ?? "tauri");
164
+ const codeExpiresIn = options.codeExpiresIn ?? 300;
165
+ if (!Number.isInteger(codeExpiresIn) || codeExpiresIn < 30 || codeExpiresIn > 3600) throw new TauriConfigError(`codeExpiresIn must be a whole number of seconds between 30 and 3600, got ${codeExpiresIn}.`);
166
+ return {
167
+ clientID,
168
+ codeExpiresIn,
169
+ authorizeOrigins: options.authorizeOrigins?.map((origin) => {
170
+ let parsed;
171
+ try {
172
+ parsed = new URL(origin);
173
+ } catch {
174
+ throw new TauriConfigError(`authorizeOrigins entry "${origin}" is not a valid URL origin.`);
175
+ }
176
+ if (parsed.origin === "null" || parsed.origin !== origin.replace(/\/$/, "")) throw new TauriConfigError(`authorizeOrigins entry "${origin}" must be a bare origin such as "https://example.com".`);
177
+ return parsed.origin;
178
+ }),
179
+ rateLimit: options.rateLimit ?? {
180
+ window: 60,
181
+ max: 10
182
+ }
183
+ };
184
+ }
185
+ /**
186
+ * Better Auth server plugin for Tauri desktop apps.
187
+ *
188
+ * Adds two endpoints:
189
+ * - `POST /tauri/authorize`: called by your hand-off web page for the signed-in
190
+ * user; returns a single-use code bound to the app's PKCE challenge.
191
+ * - `POST /tauri/token`: called by the app's Rust plugin; exchanges the code
192
+ * and PKCE verifier for a new session created in the app's own request.
193
+ *
194
+ * Pair it with the `bearer()` plugin: the app authenticates with
195
+ * `Authorization: Bearer`, never with cookies.
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * import { betterAuth } from 'better-auth';
200
+ * import { bearer } from 'better-auth/plugins';
201
+ * import { tauri } from 'tauri-plugin-better-auth';
202
+ *
203
+ * export const auth = betterAuth({
204
+ * plugins: [
205
+ * bearer(),
206
+ * tauri({ clientID: 'com.example.app', authorizeOrigins: ['https://example.com'] }),
207
+ * ],
208
+ * });
209
+ * ```
210
+ */
211
+ const tauri = (options) => {
212
+ const opts = resolveOptions(options);
213
+ const endpoints = {
214
+ tauriAuthorize: createAuthorizeEndpoint(opts),
215
+ tauriToken: createTokenEndpoint(opts)
216
+ };
217
+ const isTauriPath = (path) => path === "/tauri/authorize" || path === "/tauri/token";
218
+ return {
219
+ id: "tauri",
220
+ version: VERSION,
221
+ init(ctx) {
222
+ if (!ctx.hasPlugin("bearer")) ctx.logger.warn("[tauri-plugin-better-auth] The bearer() plugin is not registered. Tauri apps authenticate with \"Authorization: Bearer\"; add bearer() from \"better-auth/plugins\" to your plugins.");
223
+ },
224
+ endpoints,
225
+ rateLimit: opts.rateLimit === false ? void 0 : [{
226
+ window: opts.rateLimit.window,
227
+ max: opts.rateLimit.max,
228
+ pathMatcher: isTauriPath
229
+ }],
230
+ options: opts,
231
+ $ERROR_CODES: TAURI_ERROR_CODES
232
+ };
233
+ };
234
+ //#endregion
235
+ export { PROTOCOL_VERSION, TAURI_ERROR_CODES, TauriConfigError, VERSION, isLoopbackRedirectUri, isValidScheme, tauri };
@@ -0,0 +1,111 @@
1
+ //#region src/protocol.ts
2
+ /**
3
+ * Wire protocol v1, shared by the server plugin, the web hand-off client and
4
+ * the Rust plugin. Documented in PROTOCOL.md. Within v1, changes are additive
5
+ * only: installed desktop apps lag behind servers and must keep working.
6
+ *
7
+ * This module has no runtime dependencies so every entry point can import it.
8
+ */
9
+ /** Protocol version sent as `v` on the sign-in URL and returned by `/tauri/token`. */
10
+ const PROTOCOL_VERSION = 1;
11
+ /** Default client id when none is configured. */
12
+ const DEFAULT_CLIENT_ID = "tauri";
13
+ /** Default path of the app callback (`<scheme>://auth/callback`). */
14
+ const DEFAULT_CALLBACK_PATH = "/auth/callback";
15
+ /** Prefix of the verification identifier that stores an authorization code. */
16
+ const CODE_IDENTIFIER_PREFIX = "tauri:";
17
+ /** OAuth `state`: 16–128 unreserved characters (RFC 3986). */
18
+ const STATE_PATTERN = /^[A-Za-z0-9._~-]{16,128}$/;
19
+ /** PKCE S256 challenge: base64url of a SHA-256 digest, no padding. */
20
+ const CODE_CHALLENGE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
21
+ /** PKCE verifier: 43–128 unreserved characters (RFC 7636 §4.1). */
22
+ const CODE_VERIFIER_PATTERN = /^[A-Za-z0-9._~-]{43,128}$/;
23
+ /** Authorization code minted by `/tauri/authorize`. */
24
+ const CODE_PATTERN = /^[A-Za-z0-9]{32}$/;
25
+ /** Client id shared by the server, the hand-off page and the app. */
26
+ const CLIENT_ID_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
27
+ /** Social provider id forwarded from the app to the hand-off page. */
28
+ const PROVIDER_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
29
+ const SCHEME_PATTERN = /^[a-z][a-z0-9+.-]{1,62}$/;
30
+ const CALLBACK_PATH_PATTERN = /^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/;
31
+ /**
32
+ * Schemes that must never be used for the app callback: they are either
33
+ * web schemes, browser-internal, or Tauri's own.
34
+ */
35
+ const RESERVED_SCHEMES = /* @__PURE__ */ new Set([
36
+ "about",
37
+ "asset",
38
+ "blob",
39
+ "data",
40
+ "file",
41
+ "ftp",
42
+ "http",
43
+ "https",
44
+ "ipc",
45
+ "javascript",
46
+ "mailto",
47
+ "tauri",
48
+ "ws",
49
+ "wss"
50
+ ]);
51
+ /** Thrown when a configuration value breaks a protocol rule. */
52
+ var TauriConfigError = class extends Error {
53
+ name = "TauriConfigError";
54
+ };
55
+ /** Returns true for a custom URI scheme the app can register. */
56
+ function isValidScheme(scheme) {
57
+ return SCHEME_PATTERN.test(scheme) && !RESERVED_SCHEMES.has(scheme);
58
+ }
59
+ /** Validates a scheme, throwing {@link TauriConfigError} with the reason. */
60
+ function assertScheme(scheme) {
61
+ if (!isValidScheme(scheme)) throw new TauriConfigError(`Invalid callback scheme "${scheme}". Use a lowercase custom scheme such as "com.example.app" (RFC 8252 §7.1); web and Tauri schemes are not allowed.`);
62
+ return scheme;
63
+ }
64
+ /** Normalizes and validates the callback path (`/auth/callback`). */
65
+ function normalizeCallbackPath(path) {
66
+ const normalized = path.startsWith("/") ? path : `/${path}`;
67
+ if (!CALLBACK_PATH_PATTERN.test(normalized) || normalized.length > 64) throw new TauriConfigError(`Invalid callback path "${path}". Use 1–64 characters of letters, digits, "_", "-" and "/" segments, like "/auth/callback".`);
68
+ return normalized;
69
+ }
70
+ /** Validates a client id, throwing {@link TauriConfigError} with the reason. */
71
+ function assertClientId(clientId) {
72
+ if (!CLIENT_ID_PATTERN.test(clientId)) throw new TauriConfigError(`Invalid client id "${clientId}". Use 1–64 characters of letters, digits, ".", "_" and "-".`);
73
+ return clientId;
74
+ }
75
+ /**
76
+ * Returns true when `uri` is a loopback redirect the hand-off page may follow:
77
+ * exactly `http://127.0.0.1:<port><callbackPath>` or
78
+ * `http://[::1]:<port><callbackPath>`, with no userinfo, query or fragment.
79
+ *
80
+ * Anything else is rejected so that a crafted sign-in link cannot send an
81
+ * authorization code to a remote server (RFC 8252 §7.3, §8.3).
82
+ */
83
+ function isLoopbackRedirectUri(uri, callbackPath) {
84
+ if (!uri.startsWith("http://127.0.0.1:") && !uri.startsWith("http://[::1]:")) return false;
85
+ let url;
86
+ try {
87
+ url = new URL(uri);
88
+ } catch {
89
+ return false;
90
+ }
91
+ if (url.protocol !== "http:") return false;
92
+ if (url.username !== "" || url.password !== "") return false;
93
+ if (url.search !== "" || url.hash !== "") return false;
94
+ if (uri.includes("?") || uri.includes("#")) return false;
95
+ if (url.hostname !== "127.0.0.1" && url.hostname !== "[::1]") return false;
96
+ if (url.port === "") return false;
97
+ const port = Number(url.port);
98
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) return false;
99
+ return url.pathname === callbackPath;
100
+ }
101
+ /**
102
+ * Builds the URL the browser opens to hand the result back to the app:
103
+ * `<scheme>://auth/callback?code=…&state=…` or the loopback redirect URI.
104
+ */
105
+ function buildCallbackUrl(target, params) {
106
+ const search = new URLSearchParams(params).toString();
107
+ if (target.kind === "loopback") return `${target.redirectUri}?${search}`;
108
+ return `${target.scheme}://${target.callbackPath.slice(1)}?${search}`;
109
+ }
110
+ //#endregion
111
+ export { normalizeCallbackPath as _, CODE_VERIFIER_PATTERN as a, PROTOCOL_VERSION as c, TauriConfigError as d, assertClientId as f, isValidScheme as g, isLoopbackRedirectUri as h, CODE_PATTERN as i, PROVIDER_PATTERN as l, buildCallbackUrl as m, CODE_CHALLENGE_PATTERN as n, DEFAULT_CALLBACK_PATH as o, assertScheme as p, CODE_IDENTIFIER_PREFIX as r, DEFAULT_CLIENT_ID as s, CLIENT_ID_PATTERN as t, STATE_PATTERN as u };