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/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-CSUJZAO6.js"></script>
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.0",
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",
@@ -32,6 +32,7 @@ class FakeWebContents extends EventEmitter {
32
32
  this.session = { on() {} };
33
33
  }
34
34
  setZoomFactor() {}
35
+ setUserAgent() {}
35
36
  setWindowOpenHandler() {}
36
37
  isDestroyed() { return false; }
37
38
  }
@@ -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
+ });
@@ -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);