@chat-js/cli 0.4.0 → 0.6.1
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/dist/index.js +1548 -969
- package/package.json +4 -3
- package/templates/chat-app/app/(auth)/device-login/page.tsx +37 -0
- package/templates/chat-app/app/(auth)/login/page.tsx +26 -2
- package/templates/chat-app/app/(auth)/register/page.tsx +0 -12
- package/templates/chat-app/app/(chat)/api/chat/filter-reasoning-parts.ts +1 -1
- package/templates/chat-app/app/(chat)/api/chat/route.ts +13 -5
- package/templates/chat-app/app/(chat)/layout.tsx +4 -1
- package/templates/chat-app/app/api/trpc/[trpc]/route.ts +1 -0
- package/templates/chat-app/app/globals.css +9 -9
- package/templates/chat-app/app/layout.tsx +4 -2
- package/templates/chat-app/biome.jsonc +3 -3
- package/templates/chat-app/chat.config.ts +144 -141
- package/templates/chat-app/components/ai-elements/prompt-input.tsx +1 -1
- package/templates/chat-app/components/anonymous-session-init.tsx +10 -6
- package/templates/chat-app/components/artifact-actions.tsx +81 -18
- package/templates/chat-app/components/artifact-panel.tsx +142 -41
- package/templates/chat-app/components/attachment-list.tsx +1 -1
- package/templates/chat-app/components/{social-auth-providers.tsx → auth-providers.tsx} +49 -4
- package/templates/chat-app/components/chat/chat-welcome.tsx +3 -3
- package/templates/chat-app/components/chat-menu-items.tsx +1 -1
- package/templates/chat-app/components/chat-sync.tsx +3 -8
- package/templates/chat-app/components/console.tsx +9 -9
- package/templates/chat-app/components/context-usage.tsx +2 -2
- package/templates/chat-app/components/create-artifact.tsx +15 -5
- package/templates/chat-app/components/data-stream-handler.tsx +57 -16
- package/templates/chat-app/components/device-login-page.tsx +191 -0
- package/templates/chat-app/components/diffview.tsx +8 -2
- package/templates/chat-app/components/electron-auth-handler.tsx +184 -0
- package/templates/chat-app/components/electron-auth-ui.tsx +121 -0
- package/templates/chat-app/components/favicon-group.tsx +1 -1
- package/templates/chat-app/components/feedback-actions.tsx +1 -1
- package/templates/chat-app/components/greeting.tsx +1 -1
- package/templates/chat-app/components/interactive-chart-impl.tsx +3 -4
- package/templates/chat-app/components/interactive-charts.tsx +1 -1
- package/templates/chat-app/components/login-form.tsx +52 -10
- package/templates/chat-app/components/message-editor.tsx +4 -5
- package/templates/chat-app/components/model-selector.tsx +661 -655
- package/templates/chat-app/components/multimodal-input.tsx +13 -10
- package/templates/chat-app/components/parallel-response-cards.tsx +53 -35
- package/templates/chat-app/components/part/code-execution.tsx +8 -2
- package/templates/chat-app/components/part/document-common.tsx +1 -1
- package/templates/chat-app/components/part/document-preview.tsx +5 -5
- package/templates/chat-app/components/part/retrieve-url.tsx +12 -12
- package/templates/chat-app/components/part/text-message-part.tsx +13 -9
- package/templates/chat-app/components/project-chat-item.tsx +1 -1
- package/templates/chat-app/components/project-menu-items.tsx +1 -1
- package/templates/chat-app/components/research-task.tsx +1 -1
- package/templates/chat-app/components/research-tasks.tsx +1 -1
- package/templates/chat-app/components/retry-button.tsx +1 -1
- package/templates/chat-app/components/sandbox.tsx +1 -1
- package/templates/chat-app/components/sheet-editor.tsx +7 -7
- package/templates/chat-app/components/sidebar-chats-list.tsx +1 -1
- package/templates/chat-app/components/sidebar-toggle.tsx +15 -2
- package/templates/chat-app/components/sidebar-top-row.tsx +27 -12
- package/templates/chat-app/components/sidebar-user-nav.tsx +10 -1
- package/templates/chat-app/components/signup-form.tsx +49 -10
- package/templates/chat-app/components/sources.tsx +4 -4
- package/templates/chat-app/components/text-editor.tsx +5 -2
- package/templates/chat-app/components/toolbar.tsx +3 -3
- package/templates/chat-app/components/ui/sidebar.tsx +0 -1
- package/templates/chat-app/components/upgrade-cta/limit-display.tsx +1 -1
- package/templates/chat-app/components/user-message.tsx +135 -134
- package/templates/chat-app/electron.d.ts +41 -0
- package/templates/chat-app/evals/my-eval.eval.ts +3 -1
- package/templates/chat-app/hooks/use-artifact.tsx +13 -13
- package/templates/chat-app/lib/ai/gateways/provider-types.ts +19 -10
- package/templates/chat-app/lib/ai/stream-errors.test.ts +72 -0
- package/templates/chat-app/lib/ai/stream-errors.ts +94 -0
- package/templates/chat-app/lib/ai/tools/code-execution.javascript.ts +171 -0
- package/templates/chat-app/lib/ai/tools/code-execution.python.ts +336 -0
- package/templates/chat-app/lib/ai/tools/code-execution.shared.test.ts +71 -0
- package/templates/chat-app/lib/ai/tools/code-execution.shared.ts +59 -0
- package/templates/chat-app/lib/ai/tools/code-execution.ts +62 -391
- package/templates/chat-app/lib/ai/tools/code-execution.types.ts +24 -0
- package/templates/chat-app/lib/ai/tools/steps/multi-query-web-search.ts +3 -2
- package/templates/chat-app/lib/anonymous-session-client.ts +0 -3
- package/templates/chat-app/lib/artifacts/code/client.tsx +35 -5
- package/templates/chat-app/lib/artifacts/sheet/client.tsx +11 -3
- package/templates/chat-app/lib/auth-client.ts +23 -1
- package/templates/chat-app/lib/auth.ts +18 -1
- package/templates/chat-app/lib/blob.ts +1 -1
- package/templates/chat-app/lib/clone-messages.ts +1 -1
- package/templates/chat-app/lib/config-schema.ts +13 -1
- package/templates/chat-app/lib/constants.ts +3 -4
- package/templates/chat-app/lib/db/migrations/meta/0044_snapshot.json +42 -129
- package/templates/chat-app/lib/db/migrations/meta/_journal.json +1 -1
- package/templates/chat-app/lib/editor/config.ts +4 -4
- package/templates/chat-app/lib/electron-auth.ts +96 -0
- package/templates/chat-app/lib/env-schema.ts +33 -4
- package/templates/chat-app/lib/message-conversion.ts +1 -1
- package/templates/chat-app/lib/playwright-test-environment.ts +18 -0
- package/templates/chat-app/lib/social-auth.ts +5 -0
- package/templates/chat-app/lib/stores/hooks-threads.ts +2 -1
- package/templates/chat-app/lib/stores/with-threads.test.ts +1 -1
- package/templates/chat-app/lib/stores/with-threads.ts +5 -6
- package/templates/chat-app/lib/stores/with-tracing.ts +1 -1
- package/templates/chat-app/lib/thread-utils.ts +19 -21
- package/templates/chat-app/lib/utils/download-assets.ts +6 -7
- package/templates/chat-app/lib/utils/rate-limit.ts +9 -3
- package/templates/chat-app/package.json +22 -19
- package/templates/chat-app/playwright.config.ts +0 -19
- package/templates/chat-app/providers/chat-input-provider.tsx +1 -1
- package/templates/chat-app/proxy.ts +28 -3
- package/templates/chat-app/scripts/check-env.ts +10 -0
- package/templates/chat-app/trpc/server.tsx +7 -2
- package/templates/chat-app/tsconfig.json +2 -1
- package/templates/chat-app/vercel.json +0 -10
- package/templates/electron/CHANGELOG.md +7 -0
- package/templates/electron/README.md +54 -0
- package/templates/electron/entitlements.mac.plist +10 -0
- package/templates/electron/forge.config.ts +152 -0
- package/templates/electron/icon.png +0 -0
- package/templates/electron/package.json +53 -0
- package/templates/electron/scripts/generate-icons.test.js +37 -0
- package/templates/electron/scripts/generate-icons.ts +29 -0
- package/templates/electron/scripts/run-forge.cjs +28 -0
- package/templates/electron/scripts/write-branding.ts +18 -0
- package/templates/electron/src/config.ts +16 -0
- package/templates/electron/src/lib/auth-client.ts +64 -0
- package/templates/electron/src/main.ts +670 -0
- package/templates/electron/src/preload.d.ts +27 -0
- package/templates/electron/src/preload.ts +25 -0
- package/templates/electron/tsconfig.json +18 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
app,
|
|
4
|
+
BrowserWindow,
|
|
5
|
+
ipcMain,
|
|
6
|
+
Menu,
|
|
7
|
+
type MenuItemConstructorOptions,
|
|
8
|
+
nativeImage,
|
|
9
|
+
shell,
|
|
10
|
+
Tray,
|
|
11
|
+
} from "electron";
|
|
12
|
+
import { ELECTRON_AUTH_COOKIE_PREFIX } from "@/lib/electron-auth";
|
|
13
|
+
import { APP_NAME, APP_SCHEME, APP_URL, WINDOW_DEFAULTS } from "./config";
|
|
14
|
+
import { electronAuthClient } from "./lib/auth-client";
|
|
15
|
+
|
|
16
|
+
function isSquirrelStartupEvent(): boolean {
|
|
17
|
+
if (process.platform !== "win32") {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return process.argv.some((arg) => arg.startsWith("--squirrel-"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (isSquirrelStartupEvent()) {
|
|
25
|
+
app.quit();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Disable GPU acceleration in WSL / headless environments to prevent D3D12 crashes.
|
|
29
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSLENV) {
|
|
30
|
+
app.disableHardwareAcceleration();
|
|
31
|
+
app.commandLine.appendSwitch("disable-gpu");
|
|
32
|
+
app.commandLine.appendSwitch("disable-software-rasterizer");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let isQuitting = false;
|
|
36
|
+
let mainWindow: BrowserWindow | null = null;
|
|
37
|
+
let tray: Tray | null = null;
|
|
38
|
+
let pendingAuthRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
39
|
+
let currentAuthOverlayMessage: string | null = null;
|
|
40
|
+
let isAuthFlowInProgress = false;
|
|
41
|
+
let currentAuthFlowId = 0;
|
|
42
|
+
const gotSingleInstanceLock = app.requestSingleInstanceLock();
|
|
43
|
+
|
|
44
|
+
type AuthRendererState =
|
|
45
|
+
| {
|
|
46
|
+
status: "idle";
|
|
47
|
+
message: null;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
status: "awaiting-browser" | "finishing" | "timed-out" | "error";
|
|
51
|
+
message: string;
|
|
52
|
+
detail?: string | null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
let currentAuthState: AuthRendererState = {
|
|
56
|
+
status: "idle",
|
|
57
|
+
message: null,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (!gotSingleInstanceLock) {
|
|
61
|
+
app.quit();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function registerProtocolClient(): void {
|
|
65
|
+
if (process.defaultApp) {
|
|
66
|
+
if (process.platform === "win32" && process.argv.length >= 2) {
|
|
67
|
+
app.setAsDefaultProtocolClient(APP_SCHEME, process.execPath, [
|
|
68
|
+
path.resolve(process.argv[1]),
|
|
69
|
+
]);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.info(
|
|
74
|
+
`[electron-main] skipping ${APP_SCHEME} protocol registration in development on ${process.platform}; packaged builds handle deep links normally.`
|
|
75
|
+
);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
app.setAsDefaultProtocolClient(APP_SCHEME);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function broadcastAuthState(): void {
|
|
83
|
+
if (!mainWindow || mainWindow.isDestroyed()) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
mainWindow.webContents.send("chatjs:auth-state-changed", currentAuthState);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function resetAuthFlow(): Promise<void> {
|
|
91
|
+
if (pendingAuthRefreshTimer) {
|
|
92
|
+
clearTimeout(pendingAuthRefreshTimer);
|
|
93
|
+
pendingAuthRefreshTimer = null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
isAuthFlowInProgress = false;
|
|
97
|
+
currentAuthFlowId += 1;
|
|
98
|
+
|
|
99
|
+
await setAuthState({
|
|
100
|
+
status: "idle",
|
|
101
|
+
message: null,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function setAuthState(nextState: AuthRendererState): Promise<void> {
|
|
106
|
+
currentAuthState = nextState;
|
|
107
|
+
broadcastAuthState();
|
|
108
|
+
|
|
109
|
+
if (nextState.status === "idle") {
|
|
110
|
+
await setAuthOverlay(mainWindow, { visible: false });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!mainWindow || mainWindow.isDestroyed()) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Let the renderer-owned shadcn overlay handle normal auth states when the
|
|
119
|
+
// app page is already loaded. Keep the main-process DOM overlay only as a
|
|
120
|
+
// fallback during main-frame loads, where React cannot render yet.
|
|
121
|
+
if (mainWindow.webContents.isLoadingMainFrame()) {
|
|
122
|
+
await setAuthOverlay(mainWindow, {
|
|
123
|
+
visible: true,
|
|
124
|
+
message: nextState.message,
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await setAuthOverlay(mainWindow, { visible: false });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function setAuthOverlay(
|
|
133
|
+
win: BrowserWindow | null,
|
|
134
|
+
options:
|
|
135
|
+
| {
|
|
136
|
+
visible: false;
|
|
137
|
+
}
|
|
138
|
+
| {
|
|
139
|
+
visible: true;
|
|
140
|
+
message: string;
|
|
141
|
+
}
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
if (!win || win.isDestroyed()) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
currentAuthOverlayMessage = options.visible ? options.message : null;
|
|
148
|
+
|
|
149
|
+
const script = options.visible
|
|
150
|
+
? `
|
|
151
|
+
(() => {
|
|
152
|
+
const message = ${JSON.stringify(options.message)};
|
|
153
|
+
const existing = document.getElementById("chatjs-electron-auth-overlay");
|
|
154
|
+
if (existing) existing.remove();
|
|
155
|
+
const styles = getComputedStyle(document.documentElement);
|
|
156
|
+
const background = styles.getPropertyValue("--background").trim() || "hsl(0 0% 97.0392%)";
|
|
157
|
+
const foreground = styles.getPropertyValue("--foreground").trim() || "hsl(0 0% 20%)";
|
|
158
|
+
const card = styles.getPropertyValue("--card").trim() || "hsl(0 0% 100%)";
|
|
159
|
+
const border = styles.getPropertyValue("--border").trim() || "hsl(220 13% 91%)";
|
|
160
|
+
const mutedForeground =
|
|
161
|
+
styles.getPropertyValue("--muted-foreground").trim() || "hsl(220 8.9362% 46.0784%)";
|
|
162
|
+
const primary = styles.getPropertyValue("--primary").trim() || "hsl(217.2193 91.2195% 59.8039%)";
|
|
163
|
+
const radius = styles.getPropertyValue("--radius").trim() || "0.75rem";
|
|
164
|
+
const overlay = document.createElement("div");
|
|
165
|
+
overlay.id = "chatjs-electron-auth-overlay";
|
|
166
|
+
overlay.style.position = "fixed";
|
|
167
|
+
overlay.style.inset = "0";
|
|
168
|
+
overlay.style.zIndex = "999999";
|
|
169
|
+
overlay.style.display = "flex";
|
|
170
|
+
overlay.style.alignItems = "center";
|
|
171
|
+
overlay.style.justifyContent = "center";
|
|
172
|
+
overlay.style.background = "color-mix(in srgb, " + background + " 82%, transparent)";
|
|
173
|
+
overlay.style.backdropFilter = "blur(10px)";
|
|
174
|
+
overlay.style.webkitBackdropFilter = "blur(10px)";
|
|
175
|
+
overlay.innerHTML = \`
|
|
176
|
+
<div style="display:flex;min-width:320px;max-width:360px;flex-direction:column;align-items:center;gap:14px;padding:28px 32px;border:1px solid \${border};border-radius:calc(\${radius} + 4px);background:\${card};box-shadow:0 18px 50px rgba(15,23,42,0.12);font-family:var(--font-geist, ui-sans-serif, system-ui, sans-serif);color:\${foreground};">
|
|
177
|
+
<div style="width:28px;height:28px;border-radius:9999px;border:3px solid color-mix(in srgb, \${mutedForeground} 28%, transparent);border-top-color:\${primary};animation:chatjs-electron-spin 0.8s linear infinite;"></div>
|
|
178
|
+
<div id="chatjs-electron-auth-overlay-message" style="font-size:15px;font-weight:600;"></div>
|
|
179
|
+
<div style="font-size:13px;color:\${mutedForeground};text-align:center;">You can return here once the browser finishes.</div>
|
|
180
|
+
</div>
|
|
181
|
+
\`;
|
|
182
|
+
overlay.querySelector("#chatjs-electron-auth-overlay-message").textContent = message;
|
|
183
|
+
if (!document.getElementById("chatjs-electron-auth-overlay-style")) {
|
|
184
|
+
const style = document.createElement("style");
|
|
185
|
+
style.id = "chatjs-electron-auth-overlay-style";
|
|
186
|
+
style.textContent = "@keyframes chatjs-electron-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }";
|
|
187
|
+
document.head.appendChild(style);
|
|
188
|
+
}
|
|
189
|
+
document.body.appendChild(overlay);
|
|
190
|
+
})();
|
|
191
|
+
`
|
|
192
|
+
: `
|
|
193
|
+
(() => {
|
|
194
|
+
document.getElementById("chatjs-electron-auth-overlay")?.remove();
|
|
195
|
+
})();
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
if (win.webContents.isLoadingMainFrame()) {
|
|
200
|
+
win.webContents.once("did-finish-load", () => {
|
|
201
|
+
win.webContents.executeJavaScript(script).catch((error) => {
|
|
202
|
+
console.warn("[electron-main] failed to update auth overlay", error);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await win.webContents.executeJavaScript(script);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.warn("[electron-main] failed to update auth overlay", error);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Setup the @better-auth/electron main process handler.
|
|
215
|
+
// Registers the protocol handler, deep-link listeners, CSP updates, and
|
|
216
|
+
// renderer bridges. Must be called before the app is ready.
|
|
217
|
+
electronAuthClient.setupMain({
|
|
218
|
+
getWindow: () => mainWindow,
|
|
219
|
+
scheme: false,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
registerProtocolClient();
|
|
223
|
+
|
|
224
|
+
// Better Auth should register these bridges in setupMain(), but we also
|
|
225
|
+
// register them explicitly so the preload bridge stays reliable in dev builds.
|
|
226
|
+
ipcMain.removeHandler("better-auth:requestAuth");
|
|
227
|
+
ipcMain.handle("better-auth:requestAuth", async (_event, options) => {
|
|
228
|
+
if (isAuthFlowInProgress) {
|
|
229
|
+
mainWindow?.show();
|
|
230
|
+
mainWindow?.focus();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
isAuthFlowInProgress = true;
|
|
235
|
+
currentAuthFlowId += 1;
|
|
236
|
+
|
|
237
|
+
await setAuthState({
|
|
238
|
+
status: "awaiting-browser",
|
|
239
|
+
message: "Waiting for sign-in in your browser...",
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
await electronAuthClient.requestAuth(options);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
isAuthFlowInProgress = false;
|
|
246
|
+
await setAuthState({
|
|
247
|
+
status: "error",
|
|
248
|
+
message: "Couldn't open the browser sign-in flow.",
|
|
249
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
250
|
+
});
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
ipcMain.handle("chatjs:cancel-auth-flow", async () => {
|
|
256
|
+
await resetAuthFlow();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
ipcMain.removeHandler("better-auth:signOut");
|
|
260
|
+
ipcMain.handle("better-auth:signOut", async () => {
|
|
261
|
+
const result = await electronAuthClient.signOut();
|
|
262
|
+
await syncAuthSessionCookies();
|
|
263
|
+
return result;
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
ipcMain.removeHandler("better-auth:getUser");
|
|
267
|
+
ipcMain.handle("better-auth:getUser", async () => {
|
|
268
|
+
const sessionResult = await electronAuthClient.getSession();
|
|
269
|
+
return sessionResult.data?.user ?? null;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
function getAppAssetPath(...segments: string[]): string {
|
|
273
|
+
return path.join(app.getAppPath(), ...segments);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function isBetterAuthCookieName(name: string): boolean {
|
|
277
|
+
return (
|
|
278
|
+
name.startsWith(ELECTRON_AUTH_COOKIE_PREFIX) ||
|
|
279
|
+
name.startsWith(`__Secure-${ELECTRON_AUTH_COOKIE_PREFIX}`) ||
|
|
280
|
+
name.endsWith("session_token") ||
|
|
281
|
+
name.endsWith("session_data")
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function syncAuthSessionCookies(win?: BrowserWindow | null): Promise<void> {
|
|
286
|
+
const targetWindow = win ?? mainWindow;
|
|
287
|
+
const targetSession = targetWindow?.webContents.session;
|
|
288
|
+
|
|
289
|
+
if (!targetSession) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const url = new URL(APP_URL);
|
|
294
|
+
const existingCookies = await targetSession.cookies.get({ url: url.origin });
|
|
295
|
+
|
|
296
|
+
await Promise.all(
|
|
297
|
+
existingCookies
|
|
298
|
+
.filter((cookie) => isBetterAuthCookieName(cookie.name))
|
|
299
|
+
.map((cookie) => targetSession.cookies.remove(url.origin, cookie.name))
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const cookieHeader = electronAuthClient.getCookie();
|
|
303
|
+
|
|
304
|
+
if (!cookieHeader) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const cookies = cookieHeader
|
|
309
|
+
.split(/;\s*/)
|
|
310
|
+
.map((entry: string) => {
|
|
311
|
+
const index = entry.indexOf("=");
|
|
312
|
+
if (index < 1) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
name: entry.slice(0, index),
|
|
318
|
+
value: entry.slice(index + 1),
|
|
319
|
+
};
|
|
320
|
+
})
|
|
321
|
+
.filter((cookie): cookie is { name: string; value: string } => cookie !== null)
|
|
322
|
+
.filter((cookie: { name: string; value: string }) => isBetterAuthCookieName(cookie.name));
|
|
323
|
+
|
|
324
|
+
await Promise.all(
|
|
325
|
+
cookies.map((cookie: { name: string; value: string }) =>
|
|
326
|
+
targetSession.cookies.set({
|
|
327
|
+
name: cookie.name,
|
|
328
|
+
path: "/",
|
|
329
|
+
secure: url.protocol === "https:",
|
|
330
|
+
url: url.origin,
|
|
331
|
+
value: cookie.value,
|
|
332
|
+
})
|
|
333
|
+
)
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function hasSessionCookie(cookieHeader: string): boolean {
|
|
338
|
+
return /(?:^|;\s*)(?:__Secure-)?better-auth\.session_token=/.test(cookieHeader);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function authenticateFromDeepLink(url: string): Promise<boolean> {
|
|
342
|
+
try {
|
|
343
|
+
if (!isAuthFlowInProgress) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const parsed = new URL(url);
|
|
348
|
+
const token = parsed.hash.startsWith("#token=")
|
|
349
|
+
? parsed.hash.slice("#token=".length)
|
|
350
|
+
: null;
|
|
351
|
+
|
|
352
|
+
if (!token) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
await setAuthState({
|
|
357
|
+
status: "finishing",
|
|
358
|
+
message: "Finishing sign-in...",
|
|
359
|
+
});
|
|
360
|
+
await electronAuthClient.authenticate({ token });
|
|
361
|
+
return true;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.error("[electron-main] deep link authentication failed", error);
|
|
364
|
+
isAuthFlowInProgress = false;
|
|
365
|
+
await setAuthState({
|
|
366
|
+
status: "error",
|
|
367
|
+
message: "We couldn't finish sign-in automatically.",
|
|
368
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
369
|
+
});
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function waitForElectronSession(timeoutMs = 8_000): Promise<boolean> {
|
|
375
|
+
const start = Date.now();
|
|
376
|
+
|
|
377
|
+
while (Date.now() - start < timeoutMs) {
|
|
378
|
+
const cookieHeader = electronAuthClient.getCookie();
|
|
379
|
+
const hasCookie = hasSessionCookie(cookieHeader);
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
const sessionResult = await electronAuthClient.getSession();
|
|
383
|
+
const hasUser = !!sessionResult.data?.user;
|
|
384
|
+
|
|
385
|
+
if (hasCookie && hasUser) {
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.warn("[electron-main] session check failed while waiting", error);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function scheduleAuthRefresh(): void {
|
|
399
|
+
const targetWindow = mainWindow;
|
|
400
|
+
const authFlowId = currentAuthFlowId;
|
|
401
|
+
|
|
402
|
+
if (!targetWindow) {
|
|
403
|
+
console.warn("[electron-main] scheduleAuthRefresh without a window");
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (pendingAuthRefreshTimer) {
|
|
408
|
+
clearTimeout(pendingAuthRefreshTimer);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
targetWindow.show();
|
|
412
|
+
targetWindow.focus();
|
|
413
|
+
|
|
414
|
+
pendingAuthRefreshTimer = setTimeout(() => {
|
|
415
|
+
void waitForElectronSession()
|
|
416
|
+
.then(async (ready) => {
|
|
417
|
+
if (!ready) {
|
|
418
|
+
isAuthFlowInProgress = false;
|
|
419
|
+
if (authFlowId === currentAuthFlowId) {
|
|
420
|
+
await setAuthState({
|
|
421
|
+
status: "timed-out",
|
|
422
|
+
message: "Still waiting for the desktop app to finish signing in...",
|
|
423
|
+
detail: "Please try the browser flow again.",
|
|
424
|
+
});
|
|
425
|
+
} else {
|
|
426
|
+
await setAuthState({
|
|
427
|
+
status: "idle",
|
|
428
|
+
message: null,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
await syncAuthSessionCookies(targetWindow);
|
|
435
|
+
isAuthFlowInProgress = false;
|
|
436
|
+
await setAuthState({
|
|
437
|
+
status: "idle",
|
|
438
|
+
message: null,
|
|
439
|
+
});
|
|
440
|
+
})
|
|
441
|
+
.catch((error) => {
|
|
442
|
+
console.error("[electron-main] auth refresh failed", error);
|
|
443
|
+
isAuthFlowInProgress = false;
|
|
444
|
+
void setAuthState({
|
|
445
|
+
status: "error",
|
|
446
|
+
message: "Sign-in refresh failed.",
|
|
447
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
pendingAuthRefreshTimer = null;
|
|
451
|
+
}, 250);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function createWindow(): Promise<BrowserWindow> {
|
|
455
|
+
const win = new BrowserWindow({
|
|
456
|
+
...WINDOW_DEFAULTS,
|
|
457
|
+
...(process.platform === "darwin" || process.platform === "win32"
|
|
458
|
+
? { titleBarStyle: "default" as const }
|
|
459
|
+
: { titleBarStyle: "hidden" as const, titleBarOverlay: true }),
|
|
460
|
+
webPreferences: {
|
|
461
|
+
preload: getAppAssetPath("dist", "preload.js"),
|
|
462
|
+
contextIsolation: true,
|
|
463
|
+
nodeIntegration: false,
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
if (process.platform === "win32" || process.platform === "linux") {
|
|
468
|
+
win.removeMenu();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Avoid touching encrypted auth storage on app launch. On macOS this can
|
|
472
|
+
// trigger an immediate Keychain prompt before the window even loads, which
|
|
473
|
+
// feels like a crash. Session sync still runs after explicit auth events.
|
|
474
|
+
win.loadURL(APP_URL);
|
|
475
|
+
|
|
476
|
+
win.webContents.on("did-finish-load", () => {
|
|
477
|
+
if (currentAuthOverlayMessage) {
|
|
478
|
+
void setAuthOverlay(win, {
|
|
479
|
+
visible: true,
|
|
480
|
+
message: currentAuthOverlayMessage,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Open all new-window requests (including OAuth popups) in the default browser.
|
|
486
|
+
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
487
|
+
shell.openExternal(url);
|
|
488
|
+
return { action: "deny" };
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// Minimize to tray on close
|
|
492
|
+
win.on("close", (event) => {
|
|
493
|
+
if (!isQuitting) {
|
|
494
|
+
event.preventDefault();
|
|
495
|
+
win.hide();
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
if (!app.isPackaged) {
|
|
500
|
+
win.webContents.openDevTools({ mode: "detach" });
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return win;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function createTray(): Tray {
|
|
507
|
+
const iconPath = getAppAssetPath("build", "icon.png");
|
|
508
|
+
const trayIcon = nativeImage.createFromPath(iconPath);
|
|
509
|
+
const t = new Tray(trayIcon.resize({ width: 16, height: 16 }));
|
|
510
|
+
|
|
511
|
+
const contextMenu = Menu.buildFromTemplate([
|
|
512
|
+
{
|
|
513
|
+
label: `Show ${APP_NAME}`,
|
|
514
|
+
click: () => {
|
|
515
|
+
mainWindow?.show();
|
|
516
|
+
mainWindow?.focus();
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
{ type: "separator" },
|
|
520
|
+
{
|
|
521
|
+
label: "Quit",
|
|
522
|
+
click: () => {
|
|
523
|
+
isQuitting = true;
|
|
524
|
+
app.quit();
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
]);
|
|
528
|
+
|
|
529
|
+
t.setToolTip(APP_NAME);
|
|
530
|
+
t.setContextMenu(contextMenu);
|
|
531
|
+
|
|
532
|
+
t.on("click", () => {
|
|
533
|
+
if (mainWindow?.isVisible()) {
|
|
534
|
+
mainWindow.hide();
|
|
535
|
+
} else {
|
|
536
|
+
mainWindow?.show();
|
|
537
|
+
mainWindow?.focus();
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
return t;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function setupApplicationMenu(): void {
|
|
545
|
+
if (process.platform !== "darwin") {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const template: MenuItemConstructorOptions[] = [
|
|
550
|
+
{
|
|
551
|
+
label: APP_NAME,
|
|
552
|
+
submenu: [
|
|
553
|
+
{ role: "about" },
|
|
554
|
+
{ type: "separator" },
|
|
555
|
+
{ role: "services" },
|
|
556
|
+
{ type: "separator" },
|
|
557
|
+
{ role: "hide" },
|
|
558
|
+
{ role: "hideOthers" },
|
|
559
|
+
{ role: "unhide" },
|
|
560
|
+
{ type: "separator" },
|
|
561
|
+
{ role: "quit" },
|
|
562
|
+
],
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
role: "editMenu",
|
|
566
|
+
},
|
|
567
|
+
...(!app.isPackaged
|
|
568
|
+
? ([
|
|
569
|
+
{
|
|
570
|
+
role: "viewMenu",
|
|
571
|
+
submenu: [
|
|
572
|
+
{ role: "reload" },
|
|
573
|
+
{ role: "forceReload" },
|
|
574
|
+
{ type: "separator" },
|
|
575
|
+
{ role: "toggleDevTools" },
|
|
576
|
+
],
|
|
577
|
+
},
|
|
578
|
+
] satisfies MenuItemConstructorOptions[])
|
|
579
|
+
: []),
|
|
580
|
+
{
|
|
581
|
+
role: "windowMenu",
|
|
582
|
+
},
|
|
583
|
+
];
|
|
584
|
+
|
|
585
|
+
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function setupAutoUpdater(): void {
|
|
589
|
+
if (!app.isPackaged) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
const { updateElectronApp } = require("update-electron-app") as {
|
|
595
|
+
updateElectronApp: (options?: {
|
|
596
|
+
logger?: Pick<typeof console, "error" | "log" | "warn">;
|
|
597
|
+
notifyUser?: boolean;
|
|
598
|
+
updateInterval?: string;
|
|
599
|
+
}) => void;
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
updateElectronApp({
|
|
603
|
+
logger: console,
|
|
604
|
+
notifyUser: true,
|
|
605
|
+
updateInterval: "1 hour",
|
|
606
|
+
});
|
|
607
|
+
} catch (error) {
|
|
608
|
+
console.warn(
|
|
609
|
+
"[electron-main] update-electron-app is unavailable; automatic updates are disabled.",
|
|
610
|
+
error
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
ipcMain.handle("chatjs:sync-auth-session", async () => {
|
|
616
|
+
await syncAuthSessionCookies();
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
ipcMain.handle("chatjs:get-auth-state", () => currentAuthState);
|
|
620
|
+
|
|
621
|
+
app.whenReady().then(async () => {
|
|
622
|
+
app.setName(APP_NAME);
|
|
623
|
+
setupApplicationMenu();
|
|
624
|
+
mainWindow = await createWindow();
|
|
625
|
+
tray = createTray();
|
|
626
|
+
setupAutoUpdater();
|
|
627
|
+
|
|
628
|
+
app.on("activate", () => {
|
|
629
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
630
|
+
void createWindow().then((window) => {
|
|
631
|
+
mainWindow = window;
|
|
632
|
+
});
|
|
633
|
+
} else {
|
|
634
|
+
mainWindow?.show();
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
app.on("open-url", (_event, url) => {
|
|
640
|
+
void authenticateFromDeepLink(url).then((didAuthenticate) => {
|
|
641
|
+
if (didAuthenticate) {
|
|
642
|
+
scheduleAuthRefresh();
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
app.on("second-instance", (_event, commandLine) => {
|
|
648
|
+
const deepLinkUrl = commandLine.find((value) =>
|
|
649
|
+
value.startsWith(`${APP_SCHEME}://`)
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
if (deepLinkUrl) {
|
|
653
|
+
void authenticateFromDeepLink(deepLinkUrl).then((didAuthenticate) => {
|
|
654
|
+
if (didAuthenticate) {
|
|
655
|
+
scheduleAuthRefresh();
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
app.on("before-quit", () => {
|
|
662
|
+
isQuitting = true;
|
|
663
|
+
tray?.destroy();
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
app.on("window-all-closed", () => {
|
|
667
|
+
if (process.platform !== "darwin") {
|
|
668
|
+
app.quit();
|
|
669
|
+
}
|
|
670
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { electronAuthClient } from "./lib/auth-client";
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
type Bridges = typeof electronAuthClient.$Infer.Bridges;
|
|
5
|
+
type ElectronRendererAuthState =
|
|
6
|
+
| {
|
|
7
|
+
status: "idle";
|
|
8
|
+
message: null;
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
status: "awaiting-browser" | "finishing" | "timed-out" | "error";
|
|
12
|
+
message: string;
|
|
13
|
+
detail?: string | null;
|
|
14
|
+
};
|
|
15
|
+
interface Window extends Bridges {
|
|
16
|
+
electronAPI?: {
|
|
17
|
+
cancelAuthFlow?: () => Promise<void>;
|
|
18
|
+
getAuthState?: () => Promise<ElectronRendererAuthState>;
|
|
19
|
+
isElectron: boolean;
|
|
20
|
+
onAuthStateChanged?: (
|
|
21
|
+
callback: (state: ElectronRendererAuthState) => void
|
|
22
|
+
) => () => void;
|
|
23
|
+
platform: NodeJS.Platform;
|
|
24
|
+
syncAuthSession?: () => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|