@soorya-u/better-auth-desktop 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/README.md +257 -0
- package/dist/adapters/electrobun.d.mts +49 -0
- package/dist/adapters/electrobun.mjs +166 -0
- package/dist/adapters/electron.d.mts +55 -0
- package/dist/adapters/electron.mjs +205 -0
- package/dist/client-De_zQzE1.d.mts +34 -0
- package/dist/client-DhRcoap2.mjs +157 -0
- package/dist/client.d.mts +3 -0
- package/dist/client.mjs +2 -0
- package/dist/core/index.d.mts +6 -0
- package/dist/core/index.mjs +5 -0
- package/dist/exchange-DCZ0j_T5.mjs +217 -0
- package/dist/index-CP_2Wlut.d.mts +61 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +7 -0
- package/dist/loopback-ClkAoS2u.d.mts +13 -0
- package/dist/loopback-DTQgg1Za.mjs +35 -0
- package/dist/rpc/webview.d.mts +26 -0
- package/dist/rpc/webview.mjs +325 -0
- package/dist/server/index.d.mts +60 -0
- package/dist/server/index.mjs +213 -0
- package/dist/storage-BcwDmS-W.mjs +30 -0
- package/dist/storage-DtgX_vmE.d.mts +10 -0
- package/dist/types-h9AEfSnq.d.mts +75 -0
- package/dist/version-1hEssvcU.mjs +5 -0
- package/dist/web/index.d.mts +32 -0
- package/dist/web/index.mjs +41 -0
- package/package.json +153 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { t as PACKAGE_VERSION } from "../version-1hEssvcU.mjs";
|
|
2
|
+
import { a as parseLoopbackUrl } from "../loopback-DTQgg1Za.mjs";
|
|
3
|
+
import { setSessionCookie } from "better-auth/cookies";
|
|
4
|
+
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
5
|
+
import { base64Url } from "@better-auth/utils/base64";
|
|
6
|
+
import { createHash } from "@better-auth/utils/hash";
|
|
7
|
+
import { defineErrorCodes } from "better-auth";
|
|
8
|
+
import { generateRandomString } from "better-auth/crypto";
|
|
9
|
+
import { createAuthMiddleware } from "@better-auth/core/api";
|
|
10
|
+
import { Buffer } from "node:buffer";
|
|
11
|
+
import { timingSafeEqual } from "node:crypto";
|
|
12
|
+
import { SocialProviderListEnum } from "@better-auth/core/social-providers";
|
|
13
|
+
import { safeJSONParse as safeJSONParse$1 } from "@better-auth/core/utils/json";
|
|
14
|
+
import { betterFetch } from "@better-fetch/fetch";
|
|
15
|
+
import { createAuthEndpoint, sessionMiddleware } from "better-auth/api";
|
|
16
|
+
import { parseUserOutput } from "better-auth/db";
|
|
17
|
+
import * as z from "zod";
|
|
18
|
+
//#region src/server/error-codes.ts
|
|
19
|
+
const DESKTOP_ERROR_CODES = defineErrorCodes({
|
|
20
|
+
INVALID_CLIENT_ID: "Invalid client ID",
|
|
21
|
+
INVALID_TOKEN: "Invalid or expired token.",
|
|
22
|
+
STATE_MISMATCH: "state mismatch",
|
|
23
|
+
MISSING_CODE_CHALLENGE: "missing code challenge",
|
|
24
|
+
INVALID_CODE_VERIFIER: "Invalid code verifier",
|
|
25
|
+
MISSING_STATE: "state is required",
|
|
26
|
+
MISSING_PKCE: "pkce is required",
|
|
27
|
+
INVALID_PKCE_METHOD: "PKCE method must be S256",
|
|
28
|
+
INVALID_LOOPBACK_URL: "callbackURL must be a http://127.0.0.1 loopback URL"
|
|
29
|
+
});
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/server/routes.ts
|
|
32
|
+
const desktopToken = (_opts) => createAuthEndpoint("/desktop/token", {
|
|
33
|
+
method: "POST",
|
|
34
|
+
body: z.object({
|
|
35
|
+
token: z.string().nonempty(),
|
|
36
|
+
state: z.string().nonempty(),
|
|
37
|
+
code_verifier: z.string().nonempty()
|
|
38
|
+
}),
|
|
39
|
+
metadata: { scope: "http" }
|
|
40
|
+
}, async (ctx) => {
|
|
41
|
+
const token = await ctx.context.internalAdapter.consumeVerificationValue(`desktop:${ctx.body.token}`);
|
|
42
|
+
if (!token) throw APIError.from("NOT_FOUND", DESKTOP_ERROR_CODES.INVALID_TOKEN);
|
|
43
|
+
const tokenRecord = safeJSONParse$1(token.value);
|
|
44
|
+
if (!tokenRecord) throw APIError.from("INTERNAL_SERVER_ERROR", DESKTOP_ERROR_CODES.INVALID_TOKEN);
|
|
45
|
+
if (tokenRecord.state !== ctx.body.state) throw APIError.from("BAD_REQUEST", DESKTOP_ERROR_CODES.STATE_MISMATCH);
|
|
46
|
+
if (!tokenRecord.codeChallenge) throw APIError.from("BAD_REQUEST", DESKTOP_ERROR_CODES.MISSING_CODE_CHALLENGE);
|
|
47
|
+
if (tokenRecord.codeChallengeMethod !== "s256") throw APIError.from("BAD_REQUEST", DESKTOP_ERROR_CODES.INVALID_PKCE_METHOD);
|
|
48
|
+
const codeChallenge = Buffer.from(base64Url.decode(tokenRecord.codeChallenge));
|
|
49
|
+
const codeVerifier = Buffer.from(await createHash("SHA-256").digest(ctx.body.code_verifier));
|
|
50
|
+
if (codeChallenge.length !== codeVerifier.length || !timingSafeEqual(codeChallenge, codeVerifier)) throw APIError.from("BAD_REQUEST", DESKTOP_ERROR_CODES.INVALID_CODE_VERIFIER);
|
|
51
|
+
const user = await ctx.context.internalAdapter.findUserById(tokenRecord.userId);
|
|
52
|
+
if (!user) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
53
|
+
const session = await ctx.context.internalAdapter.createSession(user.id);
|
|
54
|
+
if (!session) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION);
|
|
55
|
+
await setSessionCookie(ctx, {
|
|
56
|
+
session,
|
|
57
|
+
user
|
|
58
|
+
});
|
|
59
|
+
return ctx.json({
|
|
60
|
+
token: session.token,
|
|
61
|
+
user: parseUserOutput(ctx.context.options, user)
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
const desktopInitOAuthProxy = (opts) => createAuthEndpoint("/desktop/init-oauth-proxy", {
|
|
65
|
+
method: "GET",
|
|
66
|
+
query: z.object({
|
|
67
|
+
provider: z.string().nonempty(),
|
|
68
|
+
state: z.string(),
|
|
69
|
+
code_challenge: z.string(),
|
|
70
|
+
code_challenge_method: z.string().optional(),
|
|
71
|
+
callbackURL: z.string().nonempty()
|
|
72
|
+
}),
|
|
73
|
+
metadata: { scope: "http" }
|
|
74
|
+
}, async (ctx) => {
|
|
75
|
+
const loopback = parseLoopbackUrl(ctx.query.callbackURL, opts.allowedLoopbackPorts);
|
|
76
|
+
if (!loopback) throw APIError.from("BAD_REQUEST", DESKTOP_ERROR_CODES.INVALID_LOOPBACK_URL);
|
|
77
|
+
const isSocialProvider = SocialProviderListEnum.safeParse(ctx.query.provider);
|
|
78
|
+
if (!isSocialProvider && !ctx.context.getPlugin("generic-oauth")) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PROVIDER_NOT_FOUND);
|
|
79
|
+
if (ctx.query.code_challenge_method && ctx.query.code_challenge_method.toLowerCase() !== "s256") throw APIError.from("BAD_REQUEST", DESKTOP_ERROR_CODES.INVALID_PKCE_METHOD);
|
|
80
|
+
const headers = new Headers(ctx.request?.headers);
|
|
81
|
+
headers.set("origin", new URL(ctx.context.baseURL).origin);
|
|
82
|
+
let setCookie = null;
|
|
83
|
+
const searchParams = new URLSearchParams();
|
|
84
|
+
searchParams.set("client_id", opts.clientID);
|
|
85
|
+
searchParams.set("code_challenge", ctx.query.code_challenge);
|
|
86
|
+
searchParams.set("code_challenge_method", "S256");
|
|
87
|
+
searchParams.set("state", ctx.query.state);
|
|
88
|
+
const completeURL = new URL(`${ctx.context.baseURL}/desktop/oauth-complete`);
|
|
89
|
+
completeURL.searchParams.set("redirect", loopback.toString());
|
|
90
|
+
const res = await betterFetch(`${isSocialProvider ? "/sign-in/social" : "/sign-in/oauth2"}?${searchParams.toString()}`, {
|
|
91
|
+
baseURL: ctx.context.baseURL,
|
|
92
|
+
method: "POST",
|
|
93
|
+
body: {
|
|
94
|
+
provider: ctx.query.provider,
|
|
95
|
+
callbackURL: completeURL.toString()
|
|
96
|
+
},
|
|
97
|
+
onResponse: (ctx) => {
|
|
98
|
+
setCookie = ctx.response.headers.get("set-cookie") ?? null;
|
|
99
|
+
},
|
|
100
|
+
headers
|
|
101
|
+
});
|
|
102
|
+
if (res.error) throw new APIError("INTERNAL_SERVER_ERROR", { message: res.error.message || "An unknown error occurred." });
|
|
103
|
+
if (setCookie) ctx.setHeader("set-cookie", setCookie);
|
|
104
|
+
const cookie = ctx.context.createAuthCookie("transfer_token", { maxAge: opts.codeExpiresIn });
|
|
105
|
+
await ctx.setSignedCookie(cookie.name, JSON.stringify({
|
|
106
|
+
client_id: opts.clientID,
|
|
107
|
+
state: ctx.query.state,
|
|
108
|
+
code_challenge: ctx.query.code_challenge,
|
|
109
|
+
code_challenge_method: ctx.query.code_challenge_method ?? "S256"
|
|
110
|
+
}), ctx.context.secret, cookie.attributes);
|
|
111
|
+
if (res.data.url && res.data.redirect) {
|
|
112
|
+
ctx.setHeader("Location", res.data.url);
|
|
113
|
+
ctx.setStatus(302);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
return ctx.json(res.data);
|
|
117
|
+
});
|
|
118
|
+
const desktopOAuthComplete = (opts, { handleTransfer }) => createAuthEndpoint("/desktop/oauth-complete", {
|
|
119
|
+
method: "GET",
|
|
120
|
+
query: z.object({ redirect: z.string().nonempty() }),
|
|
121
|
+
use: [sessionMiddleware],
|
|
122
|
+
requireHeaders: true,
|
|
123
|
+
metadata: { scope: "http" }
|
|
124
|
+
}, async (ctx) => {
|
|
125
|
+
const loopback = parseLoopbackUrl(ctx.query.redirect, opts.allowedLoopbackPorts);
|
|
126
|
+
if (!loopback) throw APIError.from("BAD_REQUEST", DESKTOP_ERROR_CODES.INVALID_LOOPBACK_URL);
|
|
127
|
+
const cookie = ctx.context.createAuthCookie("transfer_token", { maxAge: opts.codeExpiresIn });
|
|
128
|
+
const transferCookie = await ctx.getSignedCookie(cookie.name, ctx.context.secret);
|
|
129
|
+
ctx.setCookie(cookie.name, "", {
|
|
130
|
+
...cookie.attributes,
|
|
131
|
+
maxAge: 0
|
|
132
|
+
});
|
|
133
|
+
const payload = transferCookie ? safeJSONParse$1(transferCookie) : null;
|
|
134
|
+
if (!payload) throw APIError.from("BAD_REQUEST", DESKTOP_ERROR_CODES.MISSING_STATE);
|
|
135
|
+
const identifier = await handleTransfer(ctx, payload);
|
|
136
|
+
if (identifier === null) throw APIError.from("BAD_REQUEST", DESKTOP_ERROR_CODES.INVALID_CLIENT_ID);
|
|
137
|
+
const redirectToken = base64Url.encode(new TextEncoder().encode(JSON.stringify({
|
|
138
|
+
identifier,
|
|
139
|
+
state: payload.state
|
|
140
|
+
})));
|
|
141
|
+
if (opts.webCallbackUrl) {
|
|
142
|
+
ctx.setHeader("Location", `${opts.webCallbackUrl}#${opts.hashKey}=${redirectToken}&loopback=${encodeURIComponent(loopback.toString())}`);
|
|
143
|
+
ctx.setStatus(302);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
loopback.searchParams.set(opts.hashKey, redirectToken);
|
|
147
|
+
ctx.setHeader("Location", loopback.toString());
|
|
148
|
+
ctx.setStatus(302);
|
|
149
|
+
});
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/server/plugin.ts
|
|
152
|
+
const isAuthPath = (ctx) => !!(ctx.path?.startsWith("/sign-in") || ctx.path?.startsWith("/sign-up") || ctx.path?.startsWith("/callback") || ctx.path?.startsWith("/oauth2/callback") || ctx.path?.startsWith("/magic-link/verify") || ctx.path?.startsWith("/email-otp/verify-email") || ctx.path?.startsWith("/verify-email") || ctx.path?.startsWith("/one-tap/callback") || ctx.path?.startsWith("/passkey/verify-authentication") || ctx.path?.startsWith("/phone-number/verify"));
|
|
153
|
+
const betterAuthDesktop = (options) => {
|
|
154
|
+
const opts = {
|
|
155
|
+
clientID: options?.clientID ?? "desktop",
|
|
156
|
+
codeExpiresIn: options?.codeExpiresIn ?? 300,
|
|
157
|
+
hashKey: options?.hashKey ?? "token",
|
|
158
|
+
webCallbackUrl: options?.webCallbackUrl,
|
|
159
|
+
allowedLoopbackPorts: options?.allowedLoopbackPorts
|
|
160
|
+
};
|
|
161
|
+
const disableOriginOverride = options?.disableOriginOverride;
|
|
162
|
+
const handleTransfer = async (ctx, payload) => {
|
|
163
|
+
const { client_id, state, code_challenge, code_challenge_method } = payload;
|
|
164
|
+
const userId = ctx.context.session?.user.id || ctx.context.newSession?.user.id;
|
|
165
|
+
if (!userId || client_id !== opts.clientID) return null;
|
|
166
|
+
if (!state) throw APIError.from("BAD_REQUEST", DESKTOP_ERROR_CODES.MISSING_STATE);
|
|
167
|
+
if (!code_challenge) throw APIError.from("BAD_REQUEST", DESKTOP_ERROR_CODES.MISSING_PKCE);
|
|
168
|
+
if (code_challenge_method?.toLowerCase() !== "s256") throw APIError.from("BAD_REQUEST", DESKTOP_ERROR_CODES.INVALID_PKCE_METHOD);
|
|
169
|
+
const identifier = generateRandomString(32, "a-z", "A-Z", "0-9");
|
|
170
|
+
const expiresAt = new Date(Date.now() + opts.codeExpiresIn * 1e3);
|
|
171
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
172
|
+
identifier: `desktop:${identifier}`,
|
|
173
|
+
value: JSON.stringify({
|
|
174
|
+
userId,
|
|
175
|
+
codeChallenge: code_challenge,
|
|
176
|
+
codeChallengeMethod: "s256",
|
|
177
|
+
state
|
|
178
|
+
}),
|
|
179
|
+
expiresAt
|
|
180
|
+
});
|
|
181
|
+
return identifier;
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
id: "desktop",
|
|
185
|
+
version: PACKAGE_VERSION,
|
|
186
|
+
async onRequest(request, _ctx) {
|
|
187
|
+
if (disableOriginOverride || request.headers.get("origin")) return;
|
|
188
|
+
const desktopOrigin = request.headers.get("desktop-origin");
|
|
189
|
+
if (!desktopOrigin) return;
|
|
190
|
+
const headers = new Headers(request.headers);
|
|
191
|
+
headers.set("origin", desktopOrigin);
|
|
192
|
+
return { request: new Request(request, { headers }) };
|
|
193
|
+
},
|
|
194
|
+
hooks: { after: [{
|
|
195
|
+
matcher: (ctx) => !isAuthPath(ctx),
|
|
196
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
197
|
+
const cookie = ctx.context.createAuthCookie("transfer_token", { maxAge: opts.codeExpiresIn });
|
|
198
|
+
const transferCookie = await ctx.getSignedCookie(cookie.name, ctx.context.secret);
|
|
199
|
+
if (!ctx.context.newSession?.session || !transferCookie) return;
|
|
200
|
+
await ctx.setSignedCookie(cookie.name, transferCookie, ctx.context.secret, cookie.attributes);
|
|
201
|
+
})
|
|
202
|
+
}] },
|
|
203
|
+
endpoints: {
|
|
204
|
+
desktopToken: desktopToken(opts),
|
|
205
|
+
desktopInitOAuthProxy: desktopInitOAuthProxy(opts),
|
|
206
|
+
desktopOAuthComplete: desktopOAuthComplete(opts, { handleTransfer })
|
|
207
|
+
},
|
|
208
|
+
options: opts,
|
|
209
|
+
$ERROR_CODES: DESKTOP_ERROR_CODES
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
//#endregion
|
|
213
|
+
export { DESKTOP_ERROR_CODES, betterAuthDesktop };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/core/storage.ts
|
|
2
|
+
async function keychainStorage(opts = {}) {
|
|
3
|
+
const { service = "better-auth-desktop", account = "session" } = opts;
|
|
4
|
+
let cache = {};
|
|
5
|
+
try {
|
|
6
|
+
const raw = await Bun.secrets.get({
|
|
7
|
+
service,
|
|
8
|
+
name: account
|
|
9
|
+
});
|
|
10
|
+
if (raw) cache = JSON.parse(raw);
|
|
11
|
+
} catch {
|
|
12
|
+
cache = {};
|
|
13
|
+
}
|
|
14
|
+
const persist = () => {
|
|
15
|
+
Bun.secrets.set({
|
|
16
|
+
service,
|
|
17
|
+
name: account,
|
|
18
|
+
value: JSON.stringify(cache)
|
|
19
|
+
}).catch(() => void 0);
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
getItem: (name) => cache[name] ?? null,
|
|
23
|
+
setItem: (name, value) => {
|
|
24
|
+
cache[name] = value;
|
|
25
|
+
persist();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { keychainStorage as t };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { l as Storage } from "./types-h9AEfSnq.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/core/storage.d.ts
|
|
4
|
+
type KeychainStorageOptions = {
|
|
5
|
+
service?: string | undefined;
|
|
6
|
+
account?: string | undefined;
|
|
7
|
+
};
|
|
8
|
+
declare function keychainStorage(opts?: KeychainStorageOptions): Promise<Storage>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { keychainStorage as n, KeychainStorageOptions as t };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { BetterFetchError } from "@better-fetch/fetch";
|
|
2
|
+
import { Awaitable } from "@better-auth/core";
|
|
3
|
+
import { User } from "@better-auth/core/db";
|
|
4
|
+
|
|
5
|
+
//#region src/core/types.d.ts
|
|
6
|
+
type AuthUser = User & Record<string, any>;
|
|
7
|
+
/** Synchronous key/value store, typically keychain-backed. */
|
|
8
|
+
type Storage = {
|
|
9
|
+
getItem: (name: string) => unknown | null;
|
|
10
|
+
setItem: (name: string, value: unknown) => void;
|
|
11
|
+
};
|
|
12
|
+
/** A single hit on the loopback listener, normalized across runtimes. */
|
|
13
|
+
type LoopbackRequest = {
|
|
14
|
+
path: string;
|
|
15
|
+
query: Record<string, string>;
|
|
16
|
+
};
|
|
17
|
+
type LoopbackResponse = {
|
|
18
|
+
status: number;
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
body: string;
|
|
21
|
+
};
|
|
22
|
+
type LoopbackServer = {
|
|
23
|
+
port: number;
|
|
24
|
+
close: () => void;
|
|
25
|
+
};
|
|
26
|
+
/** Events the core pushes to the renderer through the adapter. */
|
|
27
|
+
type AuthEvent = {
|
|
28
|
+
type: "authenticated";
|
|
29
|
+
user: AuthUser;
|
|
30
|
+
} | {
|
|
31
|
+
type: "user-updated";
|
|
32
|
+
user: AuthUser | null;
|
|
33
|
+
} | {
|
|
34
|
+
type: "error";
|
|
35
|
+
error: BetterFetchError | Error;
|
|
36
|
+
path?: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The only per-runtime surface. An adapter binds a 127.0.0.1 socket, opens the
|
|
40
|
+
* system browser, pushes events to the renderer, and provides keychain storage;
|
|
41
|
+
* the loopback flow itself lives in framework-agnostic core.
|
|
42
|
+
*/
|
|
43
|
+
type DesktopAdapter = {
|
|
44
|
+
openExternal: (url: string) => Awaitable<void>;
|
|
45
|
+
serveLoopback: (onRequest: (req: LoopbackRequest) => Promise<LoopbackResponse>, opts?: {
|
|
46
|
+
port?: number;
|
|
47
|
+
}) => Promise<LoopbackServer>;
|
|
48
|
+
notifyRenderer: (event: AuthEvent) => void;
|
|
49
|
+
storage: Storage;
|
|
50
|
+
};
|
|
51
|
+
/** Renderer-initiated sign-in request. `provider` is required for loopback. */
|
|
52
|
+
type RequestAuthOptions = {
|
|
53
|
+
provider: string;
|
|
54
|
+
[key: string]: unknown;
|
|
55
|
+
};
|
|
56
|
+
type DesktopClientOptions = {
|
|
57
|
+
clientID?: string; /** Loopback path the browser is redirected back to. @default "/callback" */
|
|
58
|
+
loopbackPath?: string; /** Fixed loopback port; omit to bind 127.0.0.1:0 (OS-assigned). */
|
|
59
|
+
loopbackPort?: number; /** Milliseconds to keep the loopback open awaiting the redirect. @default 300000 */
|
|
60
|
+
loopbackTimeout?: number;
|
|
61
|
+
storagePrefix?: string;
|
|
62
|
+
cookiePrefix?: string | string[];
|
|
63
|
+
disableCache?: boolean;
|
|
64
|
+
sanitizeUser?: (user: AuthUser) => Awaitable<AuthUser>;
|
|
65
|
+
/**
|
|
66
|
+
* What the loopback shows after a successful exchange. A string is used as the
|
|
67
|
+
* HTML body; an object can redirect the browser to your own page instead.
|
|
68
|
+
* Defaults to a built-in "you can close this tab" page.
|
|
69
|
+
*/
|
|
70
|
+
loopbackSuccess?: string | {
|
|
71
|
+
redirectTo: string;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
export { LoopbackRequest as a, RequestAuthOptions as c, DesktopClientOptions as i, Storage as l, AuthUser as n, LoopbackResponse as o, DesktopAdapter as r, LoopbackServer as s, AuthEvent as t };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/web/forward-to-desktop.d.ts
|
|
2
|
+
type ForwardToDesktopOptions = {
|
|
3
|
+
/** Must match the server plugin's `hashKey`. @default "token" */hashKey?: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Opt-in successor to the old scheme-based `forwardCallback`. On a branded
|
|
7
|
+
* `webCallbackUrl` page, reads `#token=…&loopback=…` from the fragment and
|
|
8
|
+
* performs a **top-level navigation** to the desktop loopback
|
|
9
|
+
* (`http://127.0.0.1:<port>/…?token=…`) — no CORS / PNA / mixed-content.
|
|
10
|
+
*
|
|
11
|
+
* Returns `false` (a no-op) outside a browser or when the fragment is absent,
|
|
12
|
+
* so it is safe to call unconditionally on the callback page.
|
|
13
|
+
*/
|
|
14
|
+
declare function forwardToDesktop(options?: ForwardToDesktopOptions): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Better Auth client plugin wrapping {@link forwardToDesktop}, for consumers
|
|
17
|
+
* that prefer the `createAuthClient({ plugins: [...] })` pattern:
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* const authClient = createAuthClient({ plugins: [webDesktop()] });
|
|
21
|
+
* authClient.forwardToDesktop();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
declare const webDesktop: (options?: ForwardToDesktopOptions) => {
|
|
25
|
+
id: "desktop-web";
|
|
26
|
+
version: string;
|
|
27
|
+
getActions: () => {
|
|
28
|
+
forwardToDesktop: () => boolean;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { type ForwardToDesktopOptions, forwardToDesktop, webDesktop };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { t as PACKAGE_VERSION } from "../version-1hEssvcU.mjs";
|
|
2
|
+
import { a as parseLoopbackUrl } from "../loopback-DTQgg1Za.mjs";
|
|
3
|
+
//#region src/web/forward-to-desktop.ts
|
|
4
|
+
/**
|
|
5
|
+
* Opt-in successor to the old scheme-based `forwardCallback`. On a branded
|
|
6
|
+
* `webCallbackUrl` page, reads `#token=…&loopback=…` from the fragment and
|
|
7
|
+
* performs a **top-level navigation** to the desktop loopback
|
|
8
|
+
* (`http://127.0.0.1:<port>/…?token=…`) — no CORS / PNA / mixed-content.
|
|
9
|
+
*
|
|
10
|
+
* Returns `false` (a no-op) outside a browser or when the fragment is absent,
|
|
11
|
+
* so it is safe to call unconditionally on the callback page.
|
|
12
|
+
*/
|
|
13
|
+
function forwardToDesktop(options) {
|
|
14
|
+
if (typeof window === "undefined" || !window) return false;
|
|
15
|
+
const hashKey = options?.hashKey ?? "token";
|
|
16
|
+
const params = new URLSearchParams(window.location.hash.replace(/^#/, ""));
|
|
17
|
+
const token = params.get(hashKey);
|
|
18
|
+
const loopback = params.get("loopback");
|
|
19
|
+
if (!token || !loopback) return false;
|
|
20
|
+
const target = parseLoopbackUrl(decodeURIComponent(loopback));
|
|
21
|
+
if (!target) return false;
|
|
22
|
+
target.searchParams.set(hashKey, token);
|
|
23
|
+
window.location.replace(target.toString());
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Better Auth client plugin wrapping {@link forwardToDesktop}, for consumers
|
|
28
|
+
* that prefer the `createAuthClient({ plugins: [...] })` pattern:
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* const authClient = createAuthClient({ plugins: [webDesktop()] });
|
|
32
|
+
* authClient.forwardToDesktop();
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
const webDesktop = (options) => ({
|
|
36
|
+
id: "desktop-web",
|
|
37
|
+
version: PACKAGE_VERSION,
|
|
38
|
+
getActions: () => ({ forwardToDesktop: () => forwardToDesktop(options) })
|
|
39
|
+
});
|
|
40
|
+
//#endregion
|
|
41
|
+
export { forwardToDesktop, webDesktop };
|
package/package.json
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@soorya-u/better-auth-desktop",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Loopback-based desktop auth for Better Auth. Framework-agnostic server + core with thin Electrobun and Electron adapters; OAuth hand-off via a 127.0.0.1 loopback navigation (no custom URL scheme).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/soorya-u/better-auth-electrobun",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/soorya-u/better-auth-electrobun.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"electrobun",
|
|
14
|
+
"electron",
|
|
15
|
+
"auth",
|
|
16
|
+
"better-auth",
|
|
17
|
+
"desktop",
|
|
18
|
+
"loopback",
|
|
19
|
+
"typescript"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsdown",
|
|
27
|
+
"dev": "tsdown --watch",
|
|
28
|
+
"lint:package": "publint run --strict --pack false",
|
|
29
|
+
"lint:types": "attw --profile esm-only --pack .",
|
|
30
|
+
"typecheck": "tsc --project tsconfig.typecheck.json --noEmit",
|
|
31
|
+
"test": "vitest",
|
|
32
|
+
"coverage": "vitest run --coverage --coverage.provider=istanbul",
|
|
33
|
+
"check": "biome check",
|
|
34
|
+
"fix": "biome check --fix --assist-enabled=true"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"main": "./dist/index.mjs",
|
|
41
|
+
"module": "./dist/index.mjs",
|
|
42
|
+
"types": "./dist/index.d.mts",
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"dev-source": "./src/index.ts",
|
|
46
|
+
"types": "./dist/index.d.mts",
|
|
47
|
+
"default": "./dist/index.mjs"
|
|
48
|
+
},
|
|
49
|
+
"./server": {
|
|
50
|
+
"dev-source": "./src/server/index.ts",
|
|
51
|
+
"types": "./dist/server/index.d.mts",
|
|
52
|
+
"default": "./dist/server/index.mjs"
|
|
53
|
+
},
|
|
54
|
+
"./client": {
|
|
55
|
+
"dev-source": "./src/client.ts",
|
|
56
|
+
"types": "./dist/client.d.mts",
|
|
57
|
+
"default": "./dist/client.mjs"
|
|
58
|
+
},
|
|
59
|
+
"./core": {
|
|
60
|
+
"dev-source": "./src/core/index.ts",
|
|
61
|
+
"types": "./dist/core/index.d.mts",
|
|
62
|
+
"default": "./dist/core/index.mjs"
|
|
63
|
+
},
|
|
64
|
+
"./electrobun": {
|
|
65
|
+
"dev-source": "./src/adapters/electrobun.ts",
|
|
66
|
+
"types": "./dist/adapters/electrobun.d.mts",
|
|
67
|
+
"default": "./dist/adapters/electrobun.mjs"
|
|
68
|
+
},
|
|
69
|
+
"./electron": {
|
|
70
|
+
"dev-source": "./src/adapters/electron.ts",
|
|
71
|
+
"types": "./dist/adapters/electron.d.mts",
|
|
72
|
+
"default": "./dist/adapters/electron.mjs"
|
|
73
|
+
},
|
|
74
|
+
"./web": {
|
|
75
|
+
"dev-source": "./src/web/index.ts",
|
|
76
|
+
"types": "./dist/web/index.d.mts",
|
|
77
|
+
"default": "./dist/web/index.mjs"
|
|
78
|
+
},
|
|
79
|
+
"./rpc/webview": {
|
|
80
|
+
"dev-source": "./src/rpc/webview.ts",
|
|
81
|
+
"types": "./dist/rpc/webview.d.mts",
|
|
82
|
+
"default": "./dist/rpc/webview.mjs"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"typesVersions": {
|
|
86
|
+
"*": {
|
|
87
|
+
"*": [
|
|
88
|
+
"./dist/index.d.mts"
|
|
89
|
+
],
|
|
90
|
+
"server": [
|
|
91
|
+
"./dist/server/index.d.mts"
|
|
92
|
+
],
|
|
93
|
+
"client": [
|
|
94
|
+
"./dist/client.d.mts"
|
|
95
|
+
],
|
|
96
|
+
"core": [
|
|
97
|
+
"./dist/core/index.d.mts"
|
|
98
|
+
],
|
|
99
|
+
"electrobun": [
|
|
100
|
+
"./dist/adapters/electrobun.d.mts"
|
|
101
|
+
],
|
|
102
|
+
"electron": [
|
|
103
|
+
"./dist/adapters/electron.d.mts"
|
|
104
|
+
],
|
|
105
|
+
"web": [
|
|
106
|
+
"./dist/web/index.d.mts"
|
|
107
|
+
],
|
|
108
|
+
"rpc/webview": [
|
|
109
|
+
"./dist/rpc/webview.d.mts"
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"dependencies": {
|
|
114
|
+
"zod": "^4.3.6"
|
|
115
|
+
},
|
|
116
|
+
"devDependencies": {
|
|
117
|
+
"@biomejs/biome": "^2.5.1",
|
|
118
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
119
|
+
"@types/bun": "latest",
|
|
120
|
+
"better-auth": "^1.6.21",
|
|
121
|
+
"better-call": "1.3.7",
|
|
122
|
+
"better-sqlite3": "^12.11.1",
|
|
123
|
+
"electrobun": "^1.18.1",
|
|
124
|
+
"publint": "^0.3.12",
|
|
125
|
+
"tsdown": "^0.21.1",
|
|
126
|
+
"typescript": "^5.9.3",
|
|
127
|
+
"vitest": "^4.1.5"
|
|
128
|
+
},
|
|
129
|
+
"peerDependencies": {
|
|
130
|
+
"@better-auth/core": "^1.6.21",
|
|
131
|
+
"@better-auth/utils": "^0.4.2",
|
|
132
|
+
"@better-fetch/fetch": "^1.3.1",
|
|
133
|
+
"better-auth": "^1.6.21",
|
|
134
|
+
"better-call": "^1.3.7",
|
|
135
|
+
"electron": ">=28.0.0",
|
|
136
|
+
"electron-store": ">=8.2.0",
|
|
137
|
+
"electrobun": ">=1.18.1"
|
|
138
|
+
},
|
|
139
|
+
"peerDependenciesMeta": {
|
|
140
|
+
"electrobun": {
|
|
141
|
+
"optional": true
|
|
142
|
+
},
|
|
143
|
+
"electron": {
|
|
144
|
+
"optional": true
|
|
145
|
+
},
|
|
146
|
+
"electron-store": {
|
|
147
|
+
"optional": true
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"engines": {
|
|
151
|
+
"node": ">=22.0.0"
|
|
152
|
+
}
|
|
153
|
+
}
|