@better-auth/electron 1.5.0-beta.12
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.md +20 -0
- package/README.md +14 -0
- package/dist/authenticate-CWAVJ4W8.d.mts +129 -0
- package/dist/client-BBp9yCmE.d.mts +224 -0
- package/dist/client.d.mts +3 -0
- package/dist/client.mjs +476 -0
- package/dist/index.d.mts +239 -0
- package/dist/index.mjs +201 -0
- package/dist/proxy.d.mts +27 -0
- package/dist/proxy.mjs +39 -0
- package/dist/storage.d.mts +8 -0
- package/dist/storage.mjs +28 -0
- package/dist/utils-C3fLmbAT.mjs +17 -0
- package/package.json +104 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import * as better_auth0 from "better-auth";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { HookEndpointContext } from "@better-auth/core";
|
|
4
|
+
import * as better_call0 from "better-call";
|
|
5
|
+
|
|
6
|
+
//#region src/types/options.d.ts
|
|
7
|
+
interface ElectronOptions {
|
|
8
|
+
/**
|
|
9
|
+
* The duration (in seconds) for which the authorization code remains valid.
|
|
10
|
+
*
|
|
11
|
+
* @default 300 (5 minutes)
|
|
12
|
+
*/
|
|
13
|
+
codeExpiresIn?: number | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* The duration (in seconds) for which the redirect cookie remains valid.
|
|
16
|
+
*
|
|
17
|
+
* @default 120 (2 minutes)
|
|
18
|
+
*/
|
|
19
|
+
redirectCookieExpiresIn?: number | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* The prefix to use for cookies set by the plugin.
|
|
22
|
+
*
|
|
23
|
+
* @default "better-auth"
|
|
24
|
+
*/
|
|
25
|
+
cookiePrefix?: string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Client ID to use for identifying the Electron client during authorization.
|
|
28
|
+
*
|
|
29
|
+
* @default "electron"
|
|
30
|
+
*/
|
|
31
|
+
clientID?: string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Override the origin for Electron API routes.
|
|
34
|
+
* Enable this if you're facing cors origin issues with Electron API routes.
|
|
35
|
+
*
|
|
36
|
+
* @default false
|
|
37
|
+
*/
|
|
38
|
+
disableOriginOverride?: boolean | undefined;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/index.d.ts
|
|
42
|
+
declare module "@better-auth/core" {
|
|
43
|
+
interface BetterAuthPluginRegistry<AuthOptions, Options> {
|
|
44
|
+
electron: {
|
|
45
|
+
creator: typeof electron;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
declare const electron: (options?: ElectronOptions | undefined) => {
|
|
50
|
+
id: "electron";
|
|
51
|
+
onRequest(request: Request, _ctx: better_auth0.AuthContext): Promise<{
|
|
52
|
+
request: Request;
|
|
53
|
+
} | undefined>;
|
|
54
|
+
hooks: {
|
|
55
|
+
after: ({
|
|
56
|
+
matcher: (ctx: HookEndpointContext) => boolean;
|
|
57
|
+
handler: (inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<void>;
|
|
58
|
+
} | {
|
|
59
|
+
matcher: (ctx: HookEndpointContext) => boolean;
|
|
60
|
+
handler: (inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<better_call0.MiddlewareContext<better_call0.MiddlewareOptions, {
|
|
61
|
+
returned?: unknown | undefined;
|
|
62
|
+
responseHeaders?: Headers | undefined;
|
|
63
|
+
} & better_auth0.PluginContext<better_auth0.BetterAuthOptions> & better_auth0.InfoContext & {
|
|
64
|
+
options: better_auth0.BetterAuthOptions;
|
|
65
|
+
trustedOrigins: string[];
|
|
66
|
+
isTrustedOrigin: (url: string, settings?: {
|
|
67
|
+
allowRelativePaths: boolean;
|
|
68
|
+
}) => boolean;
|
|
69
|
+
oauthConfig: {
|
|
70
|
+
skipStateCookieCheck?: boolean | undefined;
|
|
71
|
+
storeStateStrategy: "database" | "cookie";
|
|
72
|
+
};
|
|
73
|
+
newSession: {
|
|
74
|
+
session: {
|
|
75
|
+
id: string;
|
|
76
|
+
createdAt: Date;
|
|
77
|
+
updatedAt: Date;
|
|
78
|
+
userId: string;
|
|
79
|
+
expiresAt: Date;
|
|
80
|
+
token: string;
|
|
81
|
+
ipAddress?: string | null | undefined;
|
|
82
|
+
userAgent?: string | null | undefined;
|
|
83
|
+
} & Record<string, any>;
|
|
84
|
+
user: {
|
|
85
|
+
id: string;
|
|
86
|
+
createdAt: Date;
|
|
87
|
+
updatedAt: Date;
|
|
88
|
+
email: string;
|
|
89
|
+
emailVerified: boolean;
|
|
90
|
+
name: string;
|
|
91
|
+
image?: string | null | undefined;
|
|
92
|
+
} & Record<string, any>;
|
|
93
|
+
} | null;
|
|
94
|
+
session: {
|
|
95
|
+
session: {
|
|
96
|
+
id: string;
|
|
97
|
+
createdAt: Date;
|
|
98
|
+
updatedAt: Date;
|
|
99
|
+
userId: string;
|
|
100
|
+
expiresAt: Date;
|
|
101
|
+
token: string;
|
|
102
|
+
ipAddress?: string | null | undefined;
|
|
103
|
+
userAgent?: string | null | undefined;
|
|
104
|
+
} & Record<string, any>;
|
|
105
|
+
user: {
|
|
106
|
+
id: string;
|
|
107
|
+
createdAt: Date;
|
|
108
|
+
updatedAt: Date;
|
|
109
|
+
email: string;
|
|
110
|
+
emailVerified: boolean;
|
|
111
|
+
name: string;
|
|
112
|
+
image?: string | null | undefined;
|
|
113
|
+
} & Record<string, any>;
|
|
114
|
+
} | null;
|
|
115
|
+
setNewSession: (session: {
|
|
116
|
+
session: {
|
|
117
|
+
id: string;
|
|
118
|
+
createdAt: Date;
|
|
119
|
+
updatedAt: Date;
|
|
120
|
+
userId: string;
|
|
121
|
+
expiresAt: Date;
|
|
122
|
+
token: string;
|
|
123
|
+
ipAddress?: string | null | undefined;
|
|
124
|
+
userAgent?: string | null | undefined;
|
|
125
|
+
} & Record<string, any>;
|
|
126
|
+
user: {
|
|
127
|
+
id: string;
|
|
128
|
+
createdAt: Date;
|
|
129
|
+
updatedAt: Date;
|
|
130
|
+
email: string;
|
|
131
|
+
emailVerified: boolean;
|
|
132
|
+
name: string;
|
|
133
|
+
image?: string | null | undefined;
|
|
134
|
+
} & Record<string, any>;
|
|
135
|
+
} | null) => void;
|
|
136
|
+
socialProviders: better_auth0.OAuthProvider[];
|
|
137
|
+
authCookies: better_auth0.BetterAuthCookies;
|
|
138
|
+
logger: ReturnType<typeof better_auth0.createLogger>;
|
|
139
|
+
rateLimit: {
|
|
140
|
+
enabled: boolean;
|
|
141
|
+
window: number;
|
|
142
|
+
max: number;
|
|
143
|
+
storage: "memory" | "database" | "secondary-storage";
|
|
144
|
+
} & Omit<better_auth0.BetterAuthRateLimitOptions, "enabled" | "window" | "max" | "storage">;
|
|
145
|
+
adapter: better_auth0.DBAdapter<better_auth0.BetterAuthOptions>;
|
|
146
|
+
internalAdapter: better_auth0.InternalAdapter<better_auth0.BetterAuthOptions>;
|
|
147
|
+
createAuthCookie: (cookieName: string, overrideAttributes?: Partial<better_call0.CookieOptions> | undefined) => better_auth0.BetterAuthCookie;
|
|
148
|
+
secret: string;
|
|
149
|
+
sessionConfig: {
|
|
150
|
+
updateAge: number;
|
|
151
|
+
expiresIn: number;
|
|
152
|
+
freshAge: number;
|
|
153
|
+
cookieRefreshCache: false | {
|
|
154
|
+
enabled: true;
|
|
155
|
+
updateAge: number;
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
generateId: (options: {
|
|
159
|
+
model: better_auth0.ModelNames;
|
|
160
|
+
size?: number | undefined;
|
|
161
|
+
}) => string | false;
|
|
162
|
+
secondaryStorage: better_auth0.SecondaryStorage | undefined;
|
|
163
|
+
password: {
|
|
164
|
+
hash: (password: string) => Promise<string>;
|
|
165
|
+
verify: (data: {
|
|
166
|
+
password: string;
|
|
167
|
+
hash: string;
|
|
168
|
+
}) => Promise<boolean>;
|
|
169
|
+
config: {
|
|
170
|
+
minPasswordLength: number;
|
|
171
|
+
maxPasswordLength: number;
|
|
172
|
+
};
|
|
173
|
+
checkPassword: (userId: string, ctx: better_auth0.GenericEndpointContext<better_auth0.BetterAuthOptions>) => Promise<boolean>;
|
|
174
|
+
};
|
|
175
|
+
tables: better_auth0.BetterAuthDBSchema;
|
|
176
|
+
runMigrations: () => Promise<void>;
|
|
177
|
+
publishTelemetry: (event: {
|
|
178
|
+
type: string;
|
|
179
|
+
anonymousId?: string | undefined;
|
|
180
|
+
payload: Record<string, any>;
|
|
181
|
+
}) => Promise<void>;
|
|
182
|
+
skipOriginCheck: boolean | string[];
|
|
183
|
+
skipCSRFCheck: boolean;
|
|
184
|
+
runInBackground: (promise: Promise<unknown>) => void;
|
|
185
|
+
runInBackgroundOrAwait: (promise: Promise<unknown> | void) => better_auth0.Awaitable<unknown>;
|
|
186
|
+
}> | undefined>;
|
|
187
|
+
})[];
|
|
188
|
+
};
|
|
189
|
+
endpoints: {
|
|
190
|
+
electronToken: better_call0.StrictEndpoint<"/electron/token", {
|
|
191
|
+
method: "POST";
|
|
192
|
+
body: z.ZodObject<{
|
|
193
|
+
token: z.ZodString;
|
|
194
|
+
state: z.ZodString;
|
|
195
|
+
code_verifier: z.ZodString;
|
|
196
|
+
}, z.core.$strip>;
|
|
197
|
+
metadata: {
|
|
198
|
+
scope: "http";
|
|
199
|
+
};
|
|
200
|
+
}, {
|
|
201
|
+
token: string;
|
|
202
|
+
user: better_auth0.User & Record<string, any>;
|
|
203
|
+
}>;
|
|
204
|
+
electronInitOAuthProxy: better_call0.StrictEndpoint<"/electron/init-oauth-proxy", {
|
|
205
|
+
method: "GET";
|
|
206
|
+
query: z.ZodObject<{
|
|
207
|
+
provider: z.ZodString;
|
|
208
|
+
state: z.ZodString;
|
|
209
|
+
code_challenge: z.ZodString;
|
|
210
|
+
code_challenge_method: z.ZodOptional<z.ZodString>;
|
|
211
|
+
}, z.core.$strip>;
|
|
212
|
+
metadata: {
|
|
213
|
+
scope: "http";
|
|
214
|
+
};
|
|
215
|
+
}, {
|
|
216
|
+
url: string | undefined;
|
|
217
|
+
redirect: boolean;
|
|
218
|
+
user?: better_auth0.User & Record<string, any>;
|
|
219
|
+
token?: string;
|
|
220
|
+
} | undefined>;
|
|
221
|
+
};
|
|
222
|
+
options: {
|
|
223
|
+
codeExpiresIn: number;
|
|
224
|
+
redirectCookieExpiresIn: number;
|
|
225
|
+
cookiePrefix: string;
|
|
226
|
+
clientID: string;
|
|
227
|
+
disableOriginOverride?: boolean | undefined;
|
|
228
|
+
};
|
|
229
|
+
$ERROR_CODES: {
|
|
230
|
+
INVALID_TOKEN: better_auth0.RawError<"INVALID_TOKEN">;
|
|
231
|
+
STATE_MISMATCH: better_auth0.RawError<"STATE_MISMATCH">;
|
|
232
|
+
MISSING_CODE_CHALLENGE: better_auth0.RawError<"MISSING_CODE_CHALLENGE">;
|
|
233
|
+
INVALID_CODE_VERIFIER: better_auth0.RawError<"INVALID_CODE_VERIFIER">;
|
|
234
|
+
MISSING_STATE: better_auth0.RawError<"MISSING_STATE">;
|
|
235
|
+
MISSING_PKCE: better_auth0.RawError<"MISSING_PKCE">;
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
//#endregion
|
|
239
|
+
export { ElectronOptions, electron };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { createAuthMiddleware } from "@better-auth/core/api";
|
|
2
|
+
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
3
|
+
import { defineErrorCodes, safeJSONParse } from "better-auth";
|
|
4
|
+
import { generateRandomString } from "better-auth/crypto";
|
|
5
|
+
import * as z from "zod";
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
import { timingSafeEqual } from "node:crypto";
|
|
8
|
+
import { SocialProviderListEnum } from "@better-auth/core/social-providers";
|
|
9
|
+
import { safeJSONParse as safeJSONParse$1 } from "@better-auth/core/utils/json";
|
|
10
|
+
import { base64Url } from "@better-auth/utils/base64";
|
|
11
|
+
import { createHash } from "@better-auth/utils/hash";
|
|
12
|
+
import { betterFetch } from "@better-fetch/fetch";
|
|
13
|
+
import { createAuthEndpoint } from "better-auth/api";
|
|
14
|
+
import { setSessionCookie } from "better-auth/cookies";
|
|
15
|
+
import { parseUserOutput } from "better-auth/db";
|
|
16
|
+
|
|
17
|
+
//#region src/error-codes.ts
|
|
18
|
+
const ELECTRON_ERROR_CODES = defineErrorCodes({
|
|
19
|
+
INVALID_TOKEN: "Invalid or expired token.",
|
|
20
|
+
STATE_MISMATCH: "state mismatch",
|
|
21
|
+
MISSING_CODE_CHALLENGE: "missing code challenge",
|
|
22
|
+
INVALID_CODE_VERIFIER: "Invalid code verifier",
|
|
23
|
+
MISSING_STATE: "state is required",
|
|
24
|
+
MISSING_PKCE: "pkce is required"
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/routes.ts
|
|
29
|
+
const electronTokenBodySchema = z.object({
|
|
30
|
+
token: z.string().nonempty(),
|
|
31
|
+
state: z.string().nonempty(),
|
|
32
|
+
code_verifier: z.string().nonempty()
|
|
33
|
+
});
|
|
34
|
+
const electronToken = (opts) => createAuthEndpoint("/electron/token", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
body: electronTokenBodySchema,
|
|
37
|
+
metadata: { scope: "http" }
|
|
38
|
+
}, async (ctx) => {
|
|
39
|
+
const token = await ctx.context.internalAdapter.findVerificationValue(`electron:${ctx.body.token}`);
|
|
40
|
+
if (!token || token.expiresAt < /* @__PURE__ */ new Date()) throw APIError.from("NOT_FOUND", ELECTRON_ERROR_CODES.INVALID_TOKEN);
|
|
41
|
+
const tokenRecord = safeJSONParse$1(token.value);
|
|
42
|
+
if (!tokenRecord) throw APIError.from("INTERNAL_SERVER_ERROR", ELECTRON_ERROR_CODES.INVALID_TOKEN);
|
|
43
|
+
if (tokenRecord.state !== ctx.body.state) throw APIError.from("BAD_REQUEST", ELECTRON_ERROR_CODES.STATE_MISMATCH);
|
|
44
|
+
if (!tokenRecord.codeChallenge) throw APIError.from("BAD_REQUEST", ELECTRON_ERROR_CODES.MISSING_CODE_CHALLENGE);
|
|
45
|
+
if (tokenRecord.codeChallengeMethod === "s256") {
|
|
46
|
+
const codeChallenge = Buffer.from(base64Url.decode(tokenRecord.codeChallenge));
|
|
47
|
+
const codeVerifier = Buffer.from(await createHash("SHA-256").digest(ctx.body.code_verifier));
|
|
48
|
+
if (codeChallenge.length !== codeVerifier.length || !timingSafeEqual(codeChallenge, codeVerifier)) throw APIError.from("BAD_REQUEST", ELECTRON_ERROR_CODES.INVALID_CODE_VERIFIER);
|
|
49
|
+
} else if (tokenRecord.codeChallenge !== ctx.body.code_verifier) throw APIError.from("BAD_REQUEST", ELECTRON_ERROR_CODES.INVALID_CODE_VERIFIER);
|
|
50
|
+
await ctx.context.internalAdapter.deleteVerificationValue(token.id);
|
|
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 electronInitOAuthProxyQuerySchema = z.object({
|
|
65
|
+
provider: z.string().nonempty(),
|
|
66
|
+
state: z.string(),
|
|
67
|
+
code_challenge: z.string(),
|
|
68
|
+
code_challenge_method: z.string().optional()
|
|
69
|
+
});
|
|
70
|
+
const electronInitOAuthProxy = (opts) => createAuthEndpoint("/electron/init-oauth-proxy", {
|
|
71
|
+
method: "GET",
|
|
72
|
+
query: electronInitOAuthProxyQuerySchema,
|
|
73
|
+
metadata: { scope: "http" }
|
|
74
|
+
}, async (ctx) => {
|
|
75
|
+
const isSocialProvider = SocialProviderListEnum.safeParse(ctx.query.provider);
|
|
76
|
+
if (!isSocialProvider && !ctx.context.getPlugin("generic-oauth")) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.PROVIDER_NOT_FOUND);
|
|
77
|
+
const headers = new Headers(ctx.request?.headers);
|
|
78
|
+
headers.set("origin", new URL(ctx.context.baseURL).origin);
|
|
79
|
+
let setCookie = null;
|
|
80
|
+
const searchParams = new URLSearchParams();
|
|
81
|
+
searchParams.set("client_id", opts.clientID || "electron");
|
|
82
|
+
searchParams.set("code_challenge", ctx.query.code_challenge);
|
|
83
|
+
searchParams.set("code_challenge_method", ctx.query.code_challenge_method || "plain");
|
|
84
|
+
searchParams.set("state", ctx.query.state);
|
|
85
|
+
const res = await betterFetch(`${isSocialProvider ? "/sign-in/social" : "/sign-in/oauth2"}?${searchParams.toString()}`, {
|
|
86
|
+
baseURL: ctx.context.baseURL,
|
|
87
|
+
method: "POST",
|
|
88
|
+
body: {
|
|
89
|
+
provider: ctx.query.provider,
|
|
90
|
+
disableRedirect: true
|
|
91
|
+
},
|
|
92
|
+
onResponse: (ctx) => {
|
|
93
|
+
setCookie = ctx.response.headers.get("set-cookie") ?? null;
|
|
94
|
+
},
|
|
95
|
+
headers
|
|
96
|
+
});
|
|
97
|
+
if (res.error) throw new APIError("INTERNAL_SERVER_ERROR", { message: res.error.message || "An unknown error occurred." });
|
|
98
|
+
if (setCookie) ctx.setHeader("set-cookie", setCookie);
|
|
99
|
+
if (res.data.url) {
|
|
100
|
+
ctx.setHeader("Location", res.data.url);
|
|
101
|
+
ctx.setStatus(302);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
return ctx.json(res.data);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/index.ts
|
|
109
|
+
const electron = (options) => {
|
|
110
|
+
const opts = {
|
|
111
|
+
codeExpiresIn: 300,
|
|
112
|
+
redirectCookieExpiresIn: 120,
|
|
113
|
+
cookiePrefix: "better-auth",
|
|
114
|
+
clientID: "electron",
|
|
115
|
+
...options || {}
|
|
116
|
+
};
|
|
117
|
+
const hookMatcher = (ctx) => {
|
|
118
|
+
return !!(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"));
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
id: "electron",
|
|
122
|
+
async onRequest(request, _ctx) {
|
|
123
|
+
if (opts.disableOriginOverride || request.headers.get("origin")) return;
|
|
124
|
+
const electronOrigin = request.headers.get("electron-origin");
|
|
125
|
+
if (!electronOrigin) return;
|
|
126
|
+
const req = request.clone();
|
|
127
|
+
req.headers.set("origin", electronOrigin);
|
|
128
|
+
return { request: req };
|
|
129
|
+
},
|
|
130
|
+
hooks: { after: [{
|
|
131
|
+
matcher: (ctx) => !hookMatcher(ctx),
|
|
132
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
133
|
+
const transferCookie = await ctx.getSignedCookie(`${opts.cookiePrefix}.transfer_token`, ctx.context.secret);
|
|
134
|
+
if (!ctx.context.newSession?.session || !transferCookie) return;
|
|
135
|
+
const cookie = ctx.context.createAuthCookie("transfer_token", { maxAge: opts.codeExpiresIn });
|
|
136
|
+
await ctx.setSignedCookie(cookie.name, transferCookie, ctx.context.secret, cookie.attributes);
|
|
137
|
+
})
|
|
138
|
+
}, {
|
|
139
|
+
matcher: hookMatcher,
|
|
140
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
141
|
+
const querySchema = z.object({
|
|
142
|
+
client_id: z.string(),
|
|
143
|
+
code_challenge: z.string().nonempty(),
|
|
144
|
+
code_challenge_method: z.string().optional().default("plain"),
|
|
145
|
+
state: z.string().nonempty()
|
|
146
|
+
});
|
|
147
|
+
const cookie = ctx.context.createAuthCookie("transfer_token", { maxAge: opts.codeExpiresIn });
|
|
148
|
+
if (ctx.query?.client_id === opts.clientID && (ctx.path.startsWith("/sign-in") || ctx.path.startsWith("/sign-up"))) {
|
|
149
|
+
const query = querySchema.safeParse(ctx.query);
|
|
150
|
+
if (query.success) await ctx.setSignedCookie(cookie.name, JSON.stringify(query.data), ctx.context.secret, cookie.attributes);
|
|
151
|
+
}
|
|
152
|
+
if (!ctx.context.newSession?.session) return;
|
|
153
|
+
const transferCookie = await ctx.getSignedCookie(cookie.name, ctx.context.secret);
|
|
154
|
+
ctx.setCookie(cookie.name, "", {
|
|
155
|
+
...cookie.attributes,
|
|
156
|
+
maxAge: 0
|
|
157
|
+
});
|
|
158
|
+
let transferPayload = null;
|
|
159
|
+
if (!!transferCookie) transferPayload = safeJSONParse(transferCookie);
|
|
160
|
+
else {
|
|
161
|
+
const query = querySchema.safeParse(ctx.query);
|
|
162
|
+
if (query.success) transferPayload = query.data;
|
|
163
|
+
}
|
|
164
|
+
if (!transferPayload) return;
|
|
165
|
+
const { client_id, code_challenge, code_challenge_method, state } = transferPayload;
|
|
166
|
+
if (client_id !== opts.clientID) return;
|
|
167
|
+
if (!state) throw APIError.from("BAD_REQUEST", ELECTRON_ERROR_CODES.MISSING_STATE);
|
|
168
|
+
if (!code_challenge) throw APIError.from("BAD_REQUEST", ELECTRON_ERROR_CODES.MISSING_PKCE);
|
|
169
|
+
const redirectCookieName = `${opts.cookiePrefix}.${opts.clientID}`;
|
|
170
|
+
const identifier = generateRandomString(32, "a-z", "A-Z", "0-9");
|
|
171
|
+
const codeExpiresInMs = opts.codeExpiresIn * 1e3;
|
|
172
|
+
const expiresAt = new Date(Date.now() + codeExpiresInMs);
|
|
173
|
+
await ctx.context.internalAdapter.createVerificationValue({
|
|
174
|
+
identifier: `electron:${identifier}`,
|
|
175
|
+
value: JSON.stringify({
|
|
176
|
+
userId: ctx.context.newSession.user.id,
|
|
177
|
+
codeChallenge: code_challenge,
|
|
178
|
+
codeChallengeMethod: code_challenge_method.toLowerCase(),
|
|
179
|
+
state
|
|
180
|
+
}),
|
|
181
|
+
expiresAt
|
|
182
|
+
});
|
|
183
|
+
ctx.setCookie(redirectCookieName, identifier, {
|
|
184
|
+
...ctx.context.authCookies.sessionToken.attributes,
|
|
185
|
+
maxAge: opts.redirectCookieExpiresIn,
|
|
186
|
+
httpOnly: false
|
|
187
|
+
});
|
|
188
|
+
return ctx;
|
|
189
|
+
})
|
|
190
|
+
}] },
|
|
191
|
+
endpoints: {
|
|
192
|
+
electronToken: electronToken(opts),
|
|
193
|
+
electronInitOAuthProxy: electronInitOAuthProxy(opts)
|
|
194
|
+
},
|
|
195
|
+
options: opts,
|
|
196
|
+
$ERROR_CODES: ELECTRON_ERROR_CODES
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
//#endregion
|
|
201
|
+
export { electron };
|
package/dist/proxy.d.mts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { r as ElectronProxyClientOptions } from "./authenticate-CWAVJ4W8.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/proxy.d.ts
|
|
4
|
+
declare const electronProxyClient: (options: ElectronProxyClientOptions) => {
|
|
5
|
+
id: "electron-proxy";
|
|
6
|
+
getActions: () => {
|
|
7
|
+
/**
|
|
8
|
+
* Ensures redirecting to the Electron app.
|
|
9
|
+
*
|
|
10
|
+
* Polls for a cookie set by the server to indicate that an authorization code is available.
|
|
11
|
+
*
|
|
12
|
+
* @returns The interval ID which can be used to clear the polling.
|
|
13
|
+
*/
|
|
14
|
+
ensureElectronRedirect: (cfg?: {
|
|
15
|
+
/**
|
|
16
|
+
* @default 10_000
|
|
17
|
+
*/
|
|
18
|
+
timeout?: number | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* @default 100
|
|
21
|
+
*/
|
|
22
|
+
interval?: number | undefined;
|
|
23
|
+
} | undefined) => NodeJS.Timeout;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { electronProxyClient };
|
package/dist/proxy.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { n as parseProtocolScheme } from "./utils-C3fLmbAT.mjs";
|
|
2
|
+
import { parseCookies } from "better-auth/cookies";
|
|
3
|
+
|
|
4
|
+
//#region src/proxy.ts
|
|
5
|
+
const electronProxyClient = (options) => {
|
|
6
|
+
const opts = {
|
|
7
|
+
clientID: "electron",
|
|
8
|
+
cookiePrefix: "better-auth",
|
|
9
|
+
callbackPath: "/auth/callback",
|
|
10
|
+
...options
|
|
11
|
+
};
|
|
12
|
+
const redirectCookieName = `${opts.cookiePrefix}.${opts.clientID}`;
|
|
13
|
+
const { scheme } = parseProtocolScheme(opts.protocol);
|
|
14
|
+
return {
|
|
15
|
+
id: "electron-proxy",
|
|
16
|
+
getActions: () => {
|
|
17
|
+
return { ensureElectronRedirect: (cfg) => {
|
|
18
|
+
const timeout = cfg?.timeout || 1e4;
|
|
19
|
+
const interval = cfg?.interval || 100;
|
|
20
|
+
const handleRedirect = () => {
|
|
21
|
+
if (typeof document === "undefined") return false;
|
|
22
|
+
const authorizationCode = parseCookies(document.cookie).get(redirectCookieName);
|
|
23
|
+
if (!authorizationCode) return false;
|
|
24
|
+
document.cookie = `${redirectCookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
|
|
25
|
+
window.location.replace(`${scheme}:/${opts.callbackPath}#token=${authorizationCode}`);
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
28
|
+
const start = Date.now();
|
|
29
|
+
const id = setInterval(() => {
|
|
30
|
+
if (handleRedirect() || Date.now() - start > timeout) clearInterval(id);
|
|
31
|
+
}, interval);
|
|
32
|
+
return id;
|
|
33
|
+
} };
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { electronProxyClient };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { i as Storage } from "./authenticate-CWAVJ4W8.mjs";
|
|
2
|
+
import "./client-BBp9yCmE.mjs";
|
|
3
|
+
import { Options } from "conf";
|
|
4
|
+
|
|
5
|
+
//#region src/storage.d.ts
|
|
6
|
+
declare const storage: (opts?: Options<Record<string, any>> | undefined) => Storage;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { storage };
|
package/dist/storage.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import electron from "electron";
|
|
2
|
+
import Conf from "conf";
|
|
3
|
+
|
|
4
|
+
//#region src/storage.ts
|
|
5
|
+
const { app } = electron;
|
|
6
|
+
const storage = (opts) => {
|
|
7
|
+
if (!app) return {
|
|
8
|
+
getItem: () => null,
|
|
9
|
+
setItem: () => {}
|
|
10
|
+
};
|
|
11
|
+
const config = new Conf({
|
|
12
|
+
cwd: app.getPath("userData"),
|
|
13
|
+
projectName: app.getName(),
|
|
14
|
+
projectVersion: app.getVersion(),
|
|
15
|
+
...opts
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
getItem: (key) => {
|
|
19
|
+
return config.get(key, null);
|
|
20
|
+
},
|
|
21
|
+
setItem: (key, value) => {
|
|
22
|
+
config.set(key, value);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
export { storage };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/utils.ts
|
|
2
|
+
function isProcessType(type) {
|
|
3
|
+
return typeof process !== "undefined" && process.type === type;
|
|
4
|
+
}
|
|
5
|
+
function parseProtocolScheme(protocolOption) {
|
|
6
|
+
if (typeof protocolOption === "string") return {
|
|
7
|
+
scheme: protocolOption,
|
|
8
|
+
privileges: {}
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
scheme: protocolOption.scheme,
|
|
12
|
+
privileges: protocolOption.privileges || {}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
export { parseProtocolScheme as n, isProcessType as t };
|
package/package.json
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@better-auth/electron",
|
|
3
|
+
"version": "1.5.0-beta.12",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Better Auth integration for Electron applications.",
|
|
6
|
+
"main": "dist/index.mjs",
|
|
7
|
+
"module": "dist/index.mjs",
|
|
8
|
+
"types": "dist/index.d.mts",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/better-auth/better-auth",
|
|
16
|
+
"directory": "packages/electron"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://www.better-auth.com/docs/integrations/electron",
|
|
19
|
+
"keywords": [
|
|
20
|
+
"electron",
|
|
21
|
+
"auth",
|
|
22
|
+
"better-auth",
|
|
23
|
+
"typescript"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"dev-source": "./src/index.ts",
|
|
28
|
+
"types": "./dist/index.d.mts",
|
|
29
|
+
"default": "./dist/index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"./client": {
|
|
32
|
+
"dev-source": "./src/client.ts",
|
|
33
|
+
"types": "./dist/client.d.mts",
|
|
34
|
+
"default": "./dist/client.mjs"
|
|
35
|
+
},
|
|
36
|
+
"./proxy": {
|
|
37
|
+
"dev-source": "./src/proxy.ts",
|
|
38
|
+
"types": "./dist/proxy.d.mts",
|
|
39
|
+
"default": "./dist/proxy.mjs"
|
|
40
|
+
},
|
|
41
|
+
"./storage": {
|
|
42
|
+
"dev-source": "./src/storage.ts",
|
|
43
|
+
"types": "./dist/storage.d.mts",
|
|
44
|
+
"default": "./dist/storage.mjs"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"typesVersions": {
|
|
48
|
+
"*": {
|
|
49
|
+
"*": [
|
|
50
|
+
"./dist/index.d.mts"
|
|
51
|
+
],
|
|
52
|
+
"client": [
|
|
53
|
+
"./dist/client.d.mts"
|
|
54
|
+
],
|
|
55
|
+
"proxy": [
|
|
56
|
+
"./dist/proxy.d.mts"
|
|
57
|
+
],
|
|
58
|
+
"storage": [
|
|
59
|
+
"./dist/storage.d.mts"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"electron": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"conf": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"peerDependencies": {
|
|
72
|
+
"@better-auth/utils": "0.3.1",
|
|
73
|
+
"@better-fetch/fetch": "1.1.21",
|
|
74
|
+
"better-call": "1.2.1",
|
|
75
|
+
"conf": "^15.0.2",
|
|
76
|
+
"@better-auth/core": "1.5.0-beta.12",
|
|
77
|
+
"better-auth": "1.5.0-beta.12"
|
|
78
|
+
},
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"zod": "^4.3.5"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
84
|
+
"better-sqlite3": "^12.6.2",
|
|
85
|
+
"tsdown": "^0.20.1",
|
|
86
|
+
"@better-auth/core": "1.5.0-beta.12",
|
|
87
|
+
"better-auth": "1.5.0-beta.12"
|
|
88
|
+
},
|
|
89
|
+
"engines": {
|
|
90
|
+
"node": ">=22.0.0"
|
|
91
|
+
},
|
|
92
|
+
"files": [
|
|
93
|
+
"dist"
|
|
94
|
+
],
|
|
95
|
+
"scripts": {
|
|
96
|
+
"test": "vitest",
|
|
97
|
+
"coverage": "vitest run --coverage --coverage.provider=istanbul",
|
|
98
|
+
"lint:package": "publint run --strict",
|
|
99
|
+
"lint:types": "attw --profile esm-only --pack .",
|
|
100
|
+
"build": "tsdown",
|
|
101
|
+
"dev": "tsdown --watch",
|
|
102
|
+
"typecheck": "tsc --project tsconfig.json"
|
|
103
|
+
}
|
|
104
|
+
}
|