claude-code-session-manager 0.45.0 → 0.45.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/assets/{TiptapBody-Pv4sVeQ8.js → TiptapBody-CFlF9v--.js} +1 -1
- package/dist/assets/{index-CSUJZAO6.js → index-B_gf3jGN.js} +4 -4
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/main/__tests__/browserView-destroyed-handler.test.cjs +1 -0
- package/src/main/__tests__/browserView-oauth-popup.test.cjs +126 -0
- package/src/main/browserView.cjs +61 -0
package/dist/index.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
10
|
-
<script type="module" crossorigin src="./assets/index-
|
|
10
|
+
<script type="module" crossorigin src="./assets/index-B_gf3jGN.js"></script>
|
|
11
11
|
<link rel="modulepreload" crossorigin href="./assets/monaco-editor-BW5C4Iv1.js">
|
|
12
12
|
<link rel="stylesheet" crossorigin href="./assets/monaco-editor-BTnBOi8r.css">
|
|
13
13
|
<link rel="stylesheet" crossorigin href="./assets/index-l5y1xr1v.css">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-session-manager",
|
|
3
|
-
"version": "0.45.
|
|
3
|
+
"version": "0.45.1",
|
|
4
4
|
"description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main/index.cjs",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browserView-oauth-popup.test.cjs — regression test for Google (and other
|
|
3
|
+
* identity-provider) OAuth login failing inside the Browser tab.
|
|
4
|
+
*
|
|
5
|
+
* Root cause: wc.setWindowOpenHandler() in browserView.cjs unconditionally
|
|
6
|
+
* denied every window.open() popup, which kills Google's GIS/FedCM popup —
|
|
7
|
+
* it needs a real window.opener to postMessage the credential back to. This
|
|
8
|
+
* test asserts identity-provider popups get `{ action: 'allow', ... }` (a
|
|
9
|
+
* real child window) while every other popup keeps the existing deny +
|
|
10
|
+
* browser:open-tab-request behavior, and that a realistic Chrome
|
|
11
|
+
* User-Agent (not Electron's default) is set on both the main view and the
|
|
12
|
+
* popup's webContents.
|
|
13
|
+
*
|
|
14
|
+
* `electron` isn't installed as a runnable binary here, and browserView.cjs
|
|
15
|
+
* pulls it in via a plain CJS `require` (not vite-node's ESM graph), so
|
|
16
|
+
* `vi.mock('electron', ...)` doesn't intercept it. Instead we pre-seed
|
|
17
|
+
* Node's own require cache for the resolved `electron` path with a fake
|
|
18
|
+
* `WebContentsView`, the only export browserView.cjs actually uses — same
|
|
19
|
+
* pattern as browserView-destroyed-handler.test.cjs.
|
|
20
|
+
*
|
|
21
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/browserView-oauth-popup.test.cjs
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
import { test, expect, beforeEach } from 'vitest';
|
|
27
|
+
import { EventEmitter } from 'node:events';
|
|
28
|
+
|
|
29
|
+
let nextWcId = 1;
|
|
30
|
+
class FakeWebContents extends EventEmitter {
|
|
31
|
+
constructor() {
|
|
32
|
+
super();
|
|
33
|
+
this.id = nextWcId++;
|
|
34
|
+
this.session = { on() {} };
|
|
35
|
+
this.userAgent = null;
|
|
36
|
+
this._windowOpenHandler = null;
|
|
37
|
+
}
|
|
38
|
+
setZoomFactor() {}
|
|
39
|
+
setUserAgent(ua) { this.userAgent = ua; }
|
|
40
|
+
setWindowOpenHandler(fn) { this._windowOpenHandler = fn; }
|
|
41
|
+
isDestroyed() { return false; }
|
|
42
|
+
}
|
|
43
|
+
class FakeWebContentsView {
|
|
44
|
+
constructor() {
|
|
45
|
+
this.webContents = new FakeWebContents();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
class FakeBrowserWindow {
|
|
49
|
+
constructor() {
|
|
50
|
+
this.webContents = new FakeWebContents();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const electronPath = require.resolve('electron');
|
|
55
|
+
require.cache[electronPath] = {
|
|
56
|
+
id: electronPath,
|
|
57
|
+
filename: electronPath,
|
|
58
|
+
loaded: true,
|
|
59
|
+
exports: { WebContentsView: FakeWebContentsView },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const { registerBrowserView, views, isBrowserViewContents } = require('../browserView.cjs');
|
|
63
|
+
|
|
64
|
+
function makeFakeIpcMain() {
|
|
65
|
+
const handlers = new Map();
|
|
66
|
+
return {
|
|
67
|
+
handle: (channel, fn) => handlers.set(channel, fn),
|
|
68
|
+
on: (channel, fn) => handlers.set(channel, fn),
|
|
69
|
+
invoke: (channel, payload) => handlers.get(channel)(null, payload),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let ipcMain;
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
ipcMain = makeFakeIpcMain();
|
|
76
|
+
registerBrowserView({ mainWindow: { isDestroyed: () => true }, ipcMain });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('main view webContents gets a real Chrome user-agent, not the Electron default', async () => {
|
|
80
|
+
const viewId = `oauth-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
81
|
+
await ipcMain.invoke('browser:create', { viewId, partition: 'sm-test' });
|
|
82
|
+
const view = views.get(viewId);
|
|
83
|
+
expect(view.webContents.userAgent).toBeTruthy();
|
|
84
|
+
expect(view.webContents.userAgent).not.toMatch(/Electron/);
|
|
85
|
+
expect(view.webContents.userAgent).toMatch(/Chrome\//);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('setWindowOpenHandler allows identity-provider popups with a real child window and denies others', async () => {
|
|
89
|
+
const viewId = `oauth2-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
90
|
+
await ipcMain.invoke('browser:create', { viewId, partition: 'sm-test' });
|
|
91
|
+
const view = views.get(viewId);
|
|
92
|
+
const handler = view.webContents._windowOpenHandler;
|
|
93
|
+
expect(typeof handler).toBe('function');
|
|
94
|
+
|
|
95
|
+
const allowed = handler({ url: 'https://accounts.google.com/o/oauth2/v2/auth?foo=bar' });
|
|
96
|
+
expect(allowed.action).toBe('allow');
|
|
97
|
+
expect(allowed.overrideBrowserWindowOptions.webPreferences).toEqual({
|
|
98
|
+
contextIsolation: true,
|
|
99
|
+
nodeIntegration: false,
|
|
100
|
+
sandbox: true,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const denied = handler({ url: 'https://example.com/some-popup' });
|
|
104
|
+
expect(denied).toEqual({ action: 'deny' });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('identity-provider popup child window gets a Chrome UA and cleans up on destroy', async () => {
|
|
108
|
+
const viewId = `oauth3-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
109
|
+
await ipcMain.invoke('browser:create', { viewId, partition: 'sm-test' });
|
|
110
|
+
const view = views.get(viewId);
|
|
111
|
+
|
|
112
|
+
const childWindow = new FakeBrowserWindow();
|
|
113
|
+
view.webContents.emit('did-create-window', childWindow);
|
|
114
|
+
|
|
115
|
+
expect(childWindow.webContents.userAgent).toBeTruthy();
|
|
116
|
+
expect(childWindow.webContents.userAgent).toMatch(/Chrome\//);
|
|
117
|
+
|
|
118
|
+
// The popup's webContents.id must be exempted from index.cjs's global
|
|
119
|
+
// will-navigate lock the same way the main Browser view is — otherwise
|
|
120
|
+
// the OAuth handshake this fix enables would immediately get nav-locked
|
|
121
|
+
// shut once the popup tries to navigate through consent/callback pages.
|
|
122
|
+
expect(isBrowserViewContents(childWindow.webContents.id)).toBe(true);
|
|
123
|
+
|
|
124
|
+
expect(() => childWindow.webContents.emit('destroyed')).not.toThrow();
|
|
125
|
+
expect(isBrowserViewContents(childWindow.webContents.id)).toBe(false);
|
|
126
|
+
});
|
package/src/main/browserView.cjs
CHANGED
|
@@ -36,6 +36,39 @@ const HISTORY_PATH = path.join(BROWSER_DATA_DIR, 'history.json');
|
|
|
36
36
|
const ZOOM_PATH = path.join(BROWSER_DATA_DIR, 'zoom.json');
|
|
37
37
|
const HISTORY_MAX = 500;
|
|
38
38
|
|
|
39
|
+
// ── Identity-provider OAuth popup allowlist ───────────────────────────
|
|
40
|
+
// window.open() popups from these hosts get a REAL child BrowserWindow
|
|
41
|
+
// (setWindowOpenHandler returns `allow`) instead of the default deny —
|
|
42
|
+
// Google's GIS/FedCM handshake (and equivalent flows from other providers)
|
|
43
|
+
// needs a genuine window.opener to postMessage the credential back to.
|
|
44
|
+
// Every other popup keeps the existing deny + browser:open-tab-request
|
|
45
|
+
// broadcast. Add appleid.apple.com / login.microsoftonline.com / github.com
|
|
46
|
+
// here when those flows need the same treatment — no restructuring needed.
|
|
47
|
+
const IDENTITY_PROVIDER_HOSTS = ['accounts.google.com'];
|
|
48
|
+
|
|
49
|
+
function isIdentityProviderUrl(rawUrl) {
|
|
50
|
+
try {
|
|
51
|
+
return IDENTITY_PROVIDER_HOSTS.includes(new URL(rawUrl).hostname);
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A realistic desktop Chrome UA built from this Electron build's OWN
|
|
58
|
+
// Chromium version (process.versions.chrome) rather than a hardcoded string
|
|
59
|
+
// that drifts stale — OAuth endpoints can flag Electron's default UA
|
|
60
|
+
// ("… Electron/x.y.z …") under a disallowed_useragent policy for embedded
|
|
61
|
+
// webviews.
|
|
62
|
+
function buildChromeUserAgent() {
|
|
63
|
+
const chromeVersion = process.versions.chrome;
|
|
64
|
+
const platformToken = process.platform === 'win32'
|
|
65
|
+
? 'Windows NT 10.0; Win64; x64'
|
|
66
|
+
: process.platform === 'darwin'
|
|
67
|
+
? 'Macintosh; Intel Mac OS X 10_15_7'
|
|
68
|
+
: 'X11; Linux x86_64';
|
|
69
|
+
return `Mozilla/5.0 (${platformToken}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
|
70
|
+
}
|
|
71
|
+
|
|
39
72
|
let lastZoomFactor = 1;
|
|
40
73
|
// Startup-only, fixed constant path (not user input) — a plain sync read is
|
|
41
74
|
// simpler than threading an async load through registerBrowserView's callers.
|
|
@@ -189,12 +222,39 @@ function wireNavEvents(viewId, view) {
|
|
|
189
222
|
});
|
|
190
223
|
|
|
191
224
|
wc.setWindowOpenHandler(({ url }) => {
|
|
225
|
+
if (isIdentityProviderUrl(url)) {
|
|
226
|
+
return {
|
|
227
|
+
action: 'allow',
|
|
228
|
+
overrideBrowserWindowOptions: {
|
|
229
|
+
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
192
233
|
if (/^https?:\/\//i.test(url)) {
|
|
193
234
|
sendIfAlive(win, 'browser:open-tab-request', { url });
|
|
194
235
|
}
|
|
195
236
|
return { action: 'deny' };
|
|
196
237
|
});
|
|
197
238
|
|
|
239
|
+
// Fires only when setWindowOpenHandler above returned `allow` — i.e. only
|
|
240
|
+
// for identity-provider popups. Sets a real Chrome UA on the popup (same
|
|
241
|
+
// reasoning as the main view below) and registers its webContents.id in
|
|
242
|
+
// browserViewContentsIds so index.cjs's global will-navigate lock exempts
|
|
243
|
+
// it too — an OAuth popup needs to navigate through several redirects
|
|
244
|
+
// (consent screen, callback) exactly like the main Browser view already
|
|
245
|
+
// does, otherwise the very handshake this fix enables would immediately
|
|
246
|
+
// get nav-locked shut. Cleaned up on the popup's own 'destroyed' event,
|
|
247
|
+
// following create()'s `view.webContents.once('destroyed', ...)` pattern
|
|
248
|
+
// so nothing dangles once the popup closes.
|
|
249
|
+
wc.on('did-create-window', (childWindow) => {
|
|
250
|
+
const childWc = childWindow.webContents;
|
|
251
|
+
childWc.setUserAgent(buildChromeUserAgent());
|
|
252
|
+
browserViewContentsIds.add(childWc.id);
|
|
253
|
+
childWc.once('destroyed', () => {
|
|
254
|
+
browserViewContentsIds.delete(childWc.id);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
198
258
|
wc.session.on('will-download', (event) => {
|
|
199
259
|
event.preventDefault();
|
|
200
260
|
});
|
|
@@ -227,6 +287,7 @@ function create({ viewId, partition }) {
|
|
|
227
287
|
browserViewContentsIds.add(wcId);
|
|
228
288
|
contentsIdToViewId.set(wcId, viewId);
|
|
229
289
|
view.webContents.setZoomFactor(lastZoomFactor);
|
|
290
|
+
view.webContents.setUserAgent(buildChromeUserAgent());
|
|
230
291
|
wireNavEvents(viewId, view);
|
|
231
292
|
view.webContents.once('destroyed', () => {
|
|
232
293
|
browserViewContentsIds.delete(wcId);
|