@tom2012/cc-web 2026.5.24-a → 2026.5.24-c

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.
Files changed (55) hide show
  1. package/README.md +1 -1
  2. package/backend/dist/__tests__/browser-chrome-e2e.test.d.ts +2 -0
  3. package/backend/dist/__tests__/browser-chrome-e2e.test.d.ts.map +1 -0
  4. package/backend/dist/__tests__/browser-chrome-e2e.test.js +121 -0
  5. package/backend/dist/__tests__/browser-chrome-e2e.test.js.map +1 -0
  6. package/backend/dist/__tests__/browser-chrome.test.d.ts +2 -0
  7. package/backend/dist/__tests__/browser-chrome.test.d.ts.map +1 -0
  8. package/backend/dist/__tests__/browser-chrome.test.js +169 -0
  9. package/backend/dist/__tests__/browser-chrome.test.js.map +1 -0
  10. package/backend/dist/__tests__/browser-proxy.test.js +81 -47
  11. package/backend/dist/__tests__/browser-proxy.test.js.map +1 -1
  12. package/backend/dist/browser-chrome/input-forwarder.d.ts +32 -0
  13. package/backend/dist/browser-chrome/input-forwarder.d.ts.map +1 -0
  14. package/backend/dist/browser-chrome/input-forwarder.js +92 -0
  15. package/backend/dist/browser-chrome/input-forwarder.js.map +1 -0
  16. package/backend/dist/browser-chrome/screencast.d.ts +21 -0
  17. package/backend/dist/browser-chrome/screencast.d.ts.map +1 -0
  18. package/backend/dist/browser-chrome/screencast.js +78 -0
  19. package/backend/dist/browser-chrome/screencast.js.map +1 -0
  20. package/backend/dist/browser-chrome/session-manager.d.ts +38 -0
  21. package/backend/dist/browser-chrome/session-manager.d.ts.map +1 -0
  22. package/backend/dist/browser-chrome/session-manager.js +175 -0
  23. package/backend/dist/browser-chrome/session-manager.js.map +1 -0
  24. package/backend/dist/index.d.ts.map +1 -1
  25. package/backend/dist/index.js +53 -0
  26. package/backend/dist/index.js.map +1 -1
  27. package/backend/dist/routes/browser-chrome.d.ts +4 -0
  28. package/backend/dist/routes/browser-chrome.d.ts.map +1 -0
  29. package/backend/dist/routes/browser-chrome.js +106 -0
  30. package/backend/dist/routes/browser-chrome.js.map +1 -0
  31. package/backend/dist/routes/browser-proxy.d.ts +16 -4
  32. package/backend/dist/routes/browser-proxy.d.ts.map +1 -1
  33. package/backend/dist/routes/browser-proxy.js +78 -41
  34. package/backend/dist/routes/browser-proxy.js.map +1 -1
  35. package/backend/package-lock.json +45 -0
  36. package/backend/package.json +1 -0
  37. package/frontend/dist/assets/{ChatOverlay-W-pSFHsV.js → ChatOverlay-DWnJouqf.js} +1 -1
  38. package/frontend/dist/assets/{GraphPreview-CcuxUzQH.js → GraphPreview-BhYiu0BC.js} +1 -1
  39. package/frontend/dist/assets/{MobilePage-DEBO3i1o.js → MobilePage-CpwaYT93.js} +3 -3
  40. package/frontend/dist/assets/{OfficePreview-DY-ew_ex.js → OfficePreview-AIsCrFJN.js} +2 -2
  41. package/frontend/dist/assets/{PdfPreview-Bl0MS_5m.js → PdfPreview-BOf3iH2G.js} +1 -1
  42. package/frontend/dist/assets/{ProjectPage-tcwSSEFX.js → ProjectPage-BLDngPUN.js} +5 -5
  43. package/frontend/dist/assets/SettingsPage-898duvMO.js +13 -0
  44. package/frontend/dist/assets/{SkillHubPage-hH5XcB57.js → SkillHubPage-Cf3oFMk3.js} +1 -1
  45. package/frontend/dist/assets/{chevron-down-B3q2CU-d.js → chevron-down-CWdHpHQs.js} +1 -1
  46. package/frontend/dist/assets/{index-CZOz50el.js → index-DoKF15jh.js} +1 -1
  47. package/frontend/dist/assets/{index-nYsy3LMF.js → index-DtlKk29C.js} +2 -2
  48. package/frontend/dist/assets/{index-DsYny3RQ.js → index-DzV3STk-.js} +1 -1
  49. package/frontend/dist/assets/{jszip.min-9o6zKk8v.js → jszip.min-BiyTb6vp.js} +1 -1
  50. package/frontend/dist/assets/{search-B9B3Cq4V.js → search-DAr7qjB5.js} +1 -1
  51. package/frontend/dist/assets/select-IaWIRuom.js +13 -0
  52. package/frontend/dist/index.html +1 -1
  53. package/package.json +1 -1
  54. package/frontend/dist/assets/SettingsPage-DU649aEa.js +0 -13
  55. package/frontend/dist/assets/select-BOLkADQv.js +0 -13
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A self-hosted web application (distributed as npm package) that provides a browser-based interface for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI sessions. Create projects, each with a persistent terminal running Claude Code, and interact with them through a real-time terminal UI.
4
4
 
5
- **Current version**: v2026.5.24-a | [GitHub](https://github.com/zbc0315/cc-web) | MIT License
5
+ **Current version**: v2026.5.24-c | [GitHub](https://github.com/zbc0315/cc-web) | MIT License
6
6
 
7
7
  ## Features
8
8
 
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=browser-chrome-e2e.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-chrome-e2e.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/browser-chrome-e2e.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ /**
37
+ * E2E: launches real headless chromium via SessionManager and verifies
38
+ * screencast frames are delivered. Skipped if chromium binary missing.
39
+ *
40
+ * Run: npx vitest run src/__tests__/browser-chrome-e2e.test.ts
41
+ */
42
+ const vitest_1 = require("vitest");
43
+ const http = __importStar(require("http"));
44
+ const playwright_1 = require("playwright");
45
+ const session_manager_1 = require("../browser-chrome/session-manager");
46
+ const screencast_1 = require("../browser-chrome/screencast");
47
+ // Skip if chromium binary not installed locally — keeps CI green when
48
+ // `npx playwright install chromium` hasn't run.
49
+ let chromiumAvailable = false;
50
+ try {
51
+ const path = playwright_1.chromium.executablePath();
52
+ chromiumAvailable = !!path;
53
+ }
54
+ catch {
55
+ chromiumAvailable = false;
56
+ }
57
+ vitest_1.describe.skipIf(!chromiumAvailable)('browser-chrome e2e (real chromium)', () => {
58
+ let upstream;
59
+ let upstreamPort = 0;
60
+ // Minimal mock WS that captures sent messages — avoids needing a real
61
+ // server/client pair just to verify screencast delivers frames.
62
+ function mockWs() {
63
+ const sent = [];
64
+ const ws = {
65
+ readyState: 1, // OPEN
66
+ bufferedAmount: 0,
67
+ send: (data) => sent.push(data),
68
+ close: () => { ws.readyState = 3; },
69
+ };
70
+ return { sent, ws };
71
+ }
72
+ (0, vitest_1.afterAll)(async () => {
73
+ await session_manager_1.browserChromeSessions.destroyAll();
74
+ if (upstream)
75
+ await new Promise(r => upstream.close(() => r()));
76
+ }, 30000);
77
+ (0, vitest_1.it)('starts chromium, navigates to local server, and delivers screencast frames', async () => {
78
+ upstream = http.createServer((req, res) => {
79
+ res.writeHead(200, { 'Content-Type': 'text/html' });
80
+ // Animated page is required — CDP screencast only emits frames on
81
+ // visual change, a fully static page yields zero frames.
82
+ res.end(`<html><body style="background:#f00;font-size:48px">
83
+ <div id="c">0</div>
84
+ <script>let n=0;setInterval(()=>{document.getElementById('c').textContent=++n},50)</script>
85
+ </body></html>`);
86
+ });
87
+ await new Promise(r => upstream.listen(0, '127.0.0.1', r));
88
+ upstreamPort = upstream.address().port;
89
+ const session = await session_manager_1.browserChromeSessions.getOrCreate('e2e-user');
90
+ (0, vitest_1.expect)(session.sid).toBeTruthy();
91
+ (0, vitest_1.expect)(session.username).toBe('e2e-user');
92
+ await session.page.goto(`http://127.0.0.1:${upstreamPort}/`, { waitUntil: 'load' });
93
+ (0, vitest_1.expect)(session.page.url()).toContain('127.0.0.1');
94
+ const { sent, ws } = mockWs();
95
+ const stop = await (0, screencast_1.startScreencast)(session, ws);
96
+ // Wait for at least 1 frame (chromium usually emits within 200ms after page paints).
97
+ await new Promise((resolve, reject) => {
98
+ const t = setTimeout(() => reject(new Error('no frame in 3s')), 3000);
99
+ const i = setInterval(() => {
100
+ if (sent.length > 0) {
101
+ clearTimeout(t);
102
+ clearInterval(i);
103
+ resolve();
104
+ }
105
+ }, 50);
106
+ });
107
+ (0, vitest_1.expect)(sent.length).toBeGreaterThan(0);
108
+ const msg = JSON.parse(sent[0]);
109
+ (0, vitest_1.expect)(msg.type).toBe('frame');
110
+ (0, vitest_1.expect)(msg.format).toBe('jpeg');
111
+ (0, vitest_1.expect)(typeof msg.data).toBe('string');
112
+ (0, vitest_1.expect)(msg.data.length).toBeGreaterThan(100);
113
+ await stop();
114
+ }, 30000);
115
+ (0, vitest_1.it)('reuses session for same user', async () => {
116
+ const a = await session_manager_1.browserChromeSessions.getOrCreate('reuse-user');
117
+ const b = await session_manager_1.browserChromeSessions.getOrCreate('reuse-user');
118
+ (0, vitest_1.expect)(a.sid).toBe(b.sid);
119
+ }, 30000);
120
+ });
121
+ //# sourceMappingURL=browser-chrome-e2e.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-chrome-e2e.test.js","sourceRoot":"","sources":["../../src/__tests__/browser-chrome-e2e.test.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;GAKG;AACH,mCAAwD;AACxD,2CAA6B;AAE7B,2CAAsC;AACtC,uEAA0E;AAC1E,6DAA+D;AAE/D,sEAAsE;AACtE,gDAAgD;AAChD,IAAI,iBAAiB,GAAG,KAAK,CAAC;AAC9B,IAAI,CAAC;IACH,MAAM,IAAI,GAAG,qBAAQ,CAAC,cAAc,EAAE,CAAC;IACvC,iBAAiB,GAAG,CAAC,CAAC,IAAI,CAAC;AAC7B,CAAC;AAAC,MAAM,CAAC;IACP,iBAAiB,GAAG,KAAK,CAAC;AAC5B,CAAC;AAED,iBAAQ,CAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC,oCAAoC,EAAE,GAAG,EAAE;IAC7E,IAAI,QAAqB,CAAC;IAC1B,IAAI,YAAY,GAAG,CAAC,CAAC;IAErB,sEAAsE;IACtE,gEAAgE;IAChE,SAAS,MAAM;QACb,MAAM,IAAI,GAAa,EAAE,CAAC;QAC1B,MAAM,EAAE,GAAG;YACT,UAAU,EAAE,CAAC,EAAE,OAAO;YACtB,cAAc,EAAE,CAAC;YACjB,IAAI,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;YACvC,KAAK,EAAE,GAAG,EAAE,GAAI,EAA6B,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC;SAChE,CAAC;QACF,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IACtB,CAAC;IAED,IAAA,iBAAQ,EAAC,KAAK,IAAI,EAAE;QAClB,MAAM,uCAAqB,CAAC,UAAU,EAAE,CAAC;QACzC,IAAI,QAAQ;YAAE,MAAM,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACxE,CAAC,EAAE,KAAM,CAAC,CAAC;IAEX,IAAA,WAAE,EAAC,4EAA4E,EAAE,KAAK,IAAI,EAAE;QAC1F,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACxC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;YACpD,kEAAkE;YAClE,yDAAyD;YACzD,GAAG,CAAC,GAAG,CAAC;;;qBAGO,CAAC,CAAC;QACnB,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC;QACjE,YAAY,GAAI,QAAQ,CAAC,OAAO,EAAkB,CAAC,IAAI,CAAC;QAExD,MAAM,OAAO,GAAG,MAAM,uCAAqB,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;QACpE,IAAA,eAAM,EAAC,OAAO,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAE1C,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,oBAAoB,YAAY,GAAG,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;QACpF,IAAA,eAAM,EAAC,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAElD,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,MAAM,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAG,MAAM,IAAA,4BAAe,EAAC,OAAO,EAAE,EAAW,CAAC,CAAC;QAEzD,qFAAqF;QACrF,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC1C,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YACtE,MAAM,CAAC,GAAG,WAAW,CAAC,GAAG,EAAE;gBACzB,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAAC,YAAY,CAAC,CAAC,CAAC,CAAC;oBAAC,aAAa,CAAC,CAAC,CAAC,CAAC;oBAAC,OAAO,EAAE,CAAC;gBAAC,CAAC;YACxE,CAAC,EAAE,EAAE,CAAC,CAAC;QACT,CAAC,CAAC,CAAC;QAEH,IAAA,eAAM,EAAC,IAAI,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QACvC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAChC,IAAA,eAAM,EAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAA,eAAM,EAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAChC,IAAA,eAAM,EAAC,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACvC,IAAA,eAAM,EAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QAE7C,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,EAAE,KAAM,CAAC,CAAC;IAEX,IAAA,WAAE,EAAC,8BAA8B,EAAE,KAAK,IAAI,EAAE;QAC5C,MAAM,CAAC,GAAG,MAAM,uCAAqB,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;QAChE,MAAM,CAAC,GAAG,MAAM,uCAAqB,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;QAChE,IAAA,eAAM,EAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC,EAAE,KAAM,CAAC,CAAC;AACb,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=browser-chrome.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-chrome.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/browser-chrome.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ const jwt = __importStar(require("jsonwebtoken"));
38
+ const session_manager_1 = require("../browser-chrome/session-manager");
39
+ const config_1 = require("../config");
40
+ (0, vitest_1.describe)('mintSessionToken / verifySessionToken', () => {
41
+ (0, vitest_1.it)('round-trips a valid token', () => {
42
+ const token = (0, session_manager_1.mintSessionToken)('sid-1', 'tom');
43
+ const claim = (0, session_manager_1.verifySessionToken)(token);
44
+ (0, vitest_1.expect)(claim).toEqual({ sid: 'sid-1', username: 'tom' });
45
+ });
46
+ (0, vitest_1.it)('rejects token with wrong typ', () => {
47
+ const config = (0, config_1.getConfig)();
48
+ const wrongTyp = jwt.sign({ sid: 'x', username: 'tom', typ: 'user' }, config.jwtSecret, { expiresIn: '1h' });
49
+ (0, vitest_1.expect)((0, session_manager_1.verifySessionToken)(wrongTyp)).toBeNull();
50
+ });
51
+ (0, vitest_1.it)('rejects token signed with wrong secret', () => {
52
+ const wrong = jwt.sign({ sid: 'x', username: 'tom', typ: 'browser-chrome' }, 'wrong-secret', { expiresIn: '1h' });
53
+ (0, vitest_1.expect)((0, session_manager_1.verifySessionToken)(wrong)).toBeNull();
54
+ });
55
+ (0, vitest_1.it)('rejects token missing sid or username', () => {
56
+ const config = (0, config_1.getConfig)();
57
+ const noSid = jwt.sign({ username: 'tom', typ: 'browser-chrome' }, config.jwtSecret, { expiresIn: '1h' });
58
+ (0, vitest_1.expect)((0, session_manager_1.verifySessionToken)(noSid)).toBeNull();
59
+ const noUser = jwt.sign({ sid: 'x', typ: 'browser-chrome' }, config.jwtSecret, { expiresIn: '1h' });
60
+ (0, vitest_1.expect)((0, session_manager_1.verifySessionToken)(noUser)).toBeNull();
61
+ });
62
+ (0, vitest_1.it)('rejects malformed token', () => {
63
+ (0, vitest_1.expect)((0, session_manager_1.verifySessionToken)('not-a-jwt')).toBeNull();
64
+ (0, vitest_1.expect)((0, session_manager_1.verifySessionToken)('')).toBeNull();
65
+ });
66
+ });
67
+ // Pure-logic tests for input-forwarder require it to export sanitize helpers
68
+ // or accept a mock page. We test sanitize/clamp behavior indirectly by
69
+ // constructing edge-case messages and asserting they don't throw with a
70
+ // minimal mock Page.
71
+ (0, vitest_1.describe)('handleInput (with mock page)', () => {
72
+ // Lazy import to avoid pulling playwright at module load.
73
+ (0, vitest_1.it)('clamps out-of-range click coords and ignores invalid modifiers', async () => {
74
+ const calls = [];
75
+ const mockPage = {
76
+ mouse: {
77
+ click: async (x, y, opts) => { calls.push(['click', x, y, opts.button]); },
78
+ move: async () => { calls.push(['move']); },
79
+ wheel: async (dx, dy) => { calls.push(['wheel', dx, dy]); },
80
+ },
81
+ keyboard: {
82
+ down: async (k) => { calls.push(['kd', k]); },
83
+ up: async (k) => { calls.push(['ku', k]); },
84
+ press: async (k) => { calls.push(['kp', k]); },
85
+ type: async (t) => { calls.push(['type', t]); },
86
+ },
87
+ setViewportSize: async (s) => { calls.push(['vp', s.width, s.height]); },
88
+ };
89
+ const mockSession = {
90
+ sid: 's', username: 'u', browser: {}, context: {},
91
+ page: mockPage, cdp: {},
92
+ createdAt: 0, lastActivityAt: 0,
93
+ viewport: { w: 1280, h: 800 }, url: '',
94
+ };
95
+ const { handleInput } = await Promise.resolve().then(() => __importStar(require('../browser-chrome/input-forwarder')));
96
+ // Negative coords clamp to 0; over-max clamp to viewport.
97
+ await handleInput(mockSession, { type: 'click', x: -100, y: -50 });
98
+ (0, vitest_1.expect)(calls).toContainEqual(['click', 0, 0, 'left']);
99
+ calls.length = 0;
100
+ await handleInput(mockSession, { type: 'click', x: 9999, y: 9999, button: 'right', modifiers: ['Shift', 'EvilMod'] });
101
+ // EvilMod dropped; Shift held around click.
102
+ (0, vitest_1.expect)(calls).toEqual([
103
+ ['kd', 'Shift'],
104
+ ['click', 1280, 800, 'right'],
105
+ ['ku', 'Shift'],
106
+ ]);
107
+ // Scroll clamps delta.
108
+ calls.length = 0;
109
+ await handleInput(mockSession, { type: 'scroll', x: 100, y: 100, deltaX: 999999, deltaY: -999999 });
110
+ (0, vitest_1.expect)(calls).toEqual([
111
+ ['move'],
112
+ ['wheel', 10000, -10000],
113
+ ]);
114
+ // type with too-long text rejected silently.
115
+ calls.length = 0;
116
+ await handleInput(mockSession, { type: 'type', text: 'x'.repeat(2000) });
117
+ (0, vitest_1.expect)(calls).toEqual([]);
118
+ // type with normal text works.
119
+ calls.length = 0;
120
+ await handleInput(mockSession, { type: 'type', text: 'hello' });
121
+ (0, vitest_1.expect)(calls).toEqual([['type', 'hello']]);
122
+ // resize clamps to bounds.
123
+ calls.length = 0;
124
+ await handleInput(mockSession, { type: 'resize', w: 50, h: 100000 });
125
+ (0, vitest_1.expect)(calls).toEqual([['vp', 200, 2160]]);
126
+ (0, vitest_1.expect)(mockSession.viewport).toEqual({ w: 200, h: 2160 });
127
+ });
128
+ (0, vitest_1.it)('key event uses press by default and releases modifiers in reverse', async () => {
129
+ const calls = [];
130
+ const mockPage = {
131
+ mouse: { click: async () => { }, move: async () => { }, wheel: async () => { } },
132
+ keyboard: {
133
+ down: async (k) => { calls.push(`d:${k}`); },
134
+ up: async (k) => { calls.push(`u:${k}`); },
135
+ press: async (k) => { calls.push(`p:${k}`); },
136
+ type: async () => { },
137
+ },
138
+ setViewportSize: async () => { },
139
+ };
140
+ const mockSession = {
141
+ sid: 's', username: 'u', browser: {}, context: {},
142
+ page: mockPage, cdp: {},
143
+ createdAt: 0, lastActivityAt: 0,
144
+ viewport: { w: 1280, h: 800 }, url: '',
145
+ };
146
+ const { handleInput } = await Promise.resolve().then(() => __importStar(require('../browser-chrome/input-forwarder')));
147
+ await handleInput(mockSession, { type: 'key', action: 'press', key: 'a', modifiers: ['Control', 'Shift'] });
148
+ (0, vitest_1.expect)(calls).toEqual(['d:Control', 'd:Shift', 'p:a', 'u:Shift', 'u:Control']);
149
+ });
150
+ (0, vitest_1.it)('drops key event when key is empty or too long', async () => {
151
+ const calls = [];
152
+ const mockPage = {
153
+ mouse: { click: async () => { }, move: async () => { }, wheel: async () => { } },
154
+ keyboard: { down: async () => { }, up: async () => { }, press: async (k) => { calls.push(k); }, type: async () => { } },
155
+ setViewportSize: async () => { },
156
+ };
157
+ const mockSession = {
158
+ sid: 's', username: 'u', browser: {}, context: {},
159
+ page: mockPage, cdp: {},
160
+ createdAt: 0, lastActivityAt: 0,
161
+ viewport: { w: 1280, h: 800 }, url: '',
162
+ };
163
+ const { handleInput } = await Promise.resolve().then(() => __importStar(require('../browser-chrome/input-forwarder')));
164
+ await handleInput(mockSession, { type: 'key', action: 'press', key: '' });
165
+ await handleInput(mockSession, { type: 'key', action: 'press', key: 'x'.repeat(100) });
166
+ (0, vitest_1.expect)(calls).toEqual([]);
167
+ });
168
+ });
169
+ //# sourceMappingURL=browser-chrome.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-chrome.test.js","sourceRoot":"","sources":["../../src/__tests__/browser-chrome.test.ts"],"names":[],"mappings":"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"}
@@ -42,13 +42,14 @@ const http = __importStar(require("http"));
42
42
  const jwt = __importStar(require("jsonwebtoken"));
43
43
  const browser_proxy_1 = __importStar(require("../routes/browser-proxy"));
44
44
  const config_1 = require("../config");
45
- // Mint a valid browser-proxy session cookie using the real jwt secret —
46
- // the test machine has a config.json so getConfig() works. CI without
47
- // config would skip these e2e tests.
48
- function mintProxyCookie() {
45
+ // Mint a valid browser-proxy session token using the real jwt secret —
46
+ // the test machine has a config.json so getConfig() works.
47
+ function mintProxyToken() {
49
48
  const config = (0, config_1.getConfig)();
50
- const token = jwt.sign({ username: 'test', typ: 'browser-proxy' }, config.jwtSecret, { expiresIn: '1h' });
51
- return `ccweb_bp=${encodeURIComponent(token)}`;
49
+ return jwt.sign({ username: 'test', typ: 'browser-proxy' }, config.jwtSecret, { expiresIn: '1h' });
50
+ }
51
+ function tok() {
52
+ return `_bp_tok=${encodeURIComponent(mintProxyToken())}`;
52
53
  }
53
54
  (0, vitest_1.describe)('parseHostport', () => {
54
55
  (0, vitest_1.it)('接受 host:port (http only)', () => {
@@ -134,69 +135,94 @@ function mintProxyCookie() {
134
135
  });
135
136
  (0, vitest_1.describe)('rewriteHtml', () => {
136
137
  const prefix = '/api/browser-proxy/127.0.0.1:8080';
137
- (0, vitest_1.it)('重写 src/href/action 的 root-absolute path', () => {
138
+ const T = 'TESTTOK';
139
+ (0, vitest_1.it)('重写 src/href/action 的 root-absolute path 并带上 token', () => {
138
140
  const input = '<script src="/main.js"></script><a href="/about">x</a><form action="/login">';
139
- const out = (0, browser_proxy_1.rewriteHtml)(input, prefix);
140
- (0, vitest_1.expect)(out).toContain('src="/api/browser-proxy/127.0.0.1:8080/main.js"');
141
- (0, vitest_1.expect)(out).toContain('href="/api/browser-proxy/127.0.0.1:8080/about"');
142
- (0, vitest_1.expect)(out).toContain('action="/api/browser-proxy/127.0.0.1:8080/login"');
141
+ const out = (0, browser_proxy_1.rewriteHtml)(input, prefix, T);
142
+ (0, vitest_1.expect)(out).toContain('src="/api/browser-proxy/127.0.0.1:8080/main.js?_bp_tok=TESTTOK"');
143
+ (0, vitest_1.expect)(out).toContain('href="/api/browser-proxy/127.0.0.1:8080/about?_bp_tok=TESTTOK"');
144
+ (0, vitest_1.expect)(out).toContain('action="/api/browser-proxy/127.0.0.1:8080/login?_bp_tok=TESTTOK"');
145
+ });
146
+ (0, vitest_1.it)('已有 query string 用 & 拼 token', () => {
147
+ const input = '<a href="/foo?x=1">';
148
+ const out = (0, browser_proxy_1.rewriteHtml)(input, prefix, T);
149
+ (0, vitest_1.expect)(out).toContain('href="/api/browser-proxy/127.0.0.1:8080/foo?x=1&_bp_tok=TESTTOK"');
143
150
  });
144
151
  (0, vitest_1.it)('strip 上游 <base href> 避免覆盖代理路径', () => {
145
152
  const input = '<head><base href="/"><base href="https://other.example/"></head><body></body>';
146
- (0, vitest_1.expect)((0, browser_proxy_1.rewriteHtml)(input, prefix)).toBe('<head></head><body></body>');
153
+ (0, vitest_1.expect)((0, browser_proxy_1.rewriteHtml)(input, prefix, T)).toBe('<head></head><body></body>');
147
154
  });
148
155
  (0, vitest_1.it)('protocol-relative (//) 不改', () => {
149
156
  const input = '<img src="//cdn.example.com/x.png">';
150
- (0, vitest_1.expect)((0, browser_proxy_1.rewriteHtml)(input, prefix)).toBe(input);
157
+ (0, vitest_1.expect)((0, browser_proxy_1.rewriteHtml)(input, prefix, T)).toBe(input);
151
158
  });
152
159
  (0, vitest_1.it)('相对路径不改', () => {
153
160
  const input = '<img src="foo/bar.png"><a href="../other">x</a>';
154
- (0, vitest_1.expect)((0, browser_proxy_1.rewriteHtml)(input, prefix)).toBe(input);
161
+ (0, vitest_1.expect)((0, browser_proxy_1.rewriteHtml)(input, prefix, T)).toBe(input);
155
162
  });
156
163
  (0, vitest_1.it)('绝对 URL 不改', () => {
157
164
  const input = '<a href="https://example.com/path">x</a>';
158
- (0, vitest_1.expect)((0, browser_proxy_1.rewriteHtml)(input, prefix)).toBe(input);
165
+ (0, vitest_1.expect)((0, browser_proxy_1.rewriteHtml)(input, prefix, T)).toBe(input);
159
166
  });
160
167
  (0, vitest_1.it)('单引号 attribute 也支持', () => {
161
168
  const input = "<a href='/foo'>";
162
- (0, vitest_1.expect)((0, browser_proxy_1.rewriteHtml)(input, prefix)).toContain("href='/api/browser-proxy/127.0.0.1:8080/foo'");
169
+ (0, vitest_1.expect)((0, browser_proxy_1.rewriteHtml)(input, prefix, T)).toContain("href='/api/browser-proxy/127.0.0.1:8080/foo?_bp_tok=TESTTOK'");
163
170
  });
164
171
  });
165
172
  (0, vitest_1.describe)('rewriteLocationHeader', () => {
166
173
  const parsed = { host: '127.0.0.1', port: 8080 };
167
174
  const prefix = '/api/browser-proxy/127.0.0.1:8080';
168
- (0, vitest_1.it)('absolute redirect 同 scheme/host/port → 重写为 proxy URL', () => {
169
- (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('http://127.0.0.1:8080/new-path?x=1', parsed, prefix))
170
- .toBe('/api/browser-proxy/127.0.0.1:8080/new-path?x=1');
175
+ const T = 'TESTTOK';
176
+ (0, vitest_1.it)('absolute redirect 同 scheme/host/port → 重写为 proxy URL 并带 token', () => {
177
+ (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('http://127.0.0.1:8080/new-path?x=1', parsed, prefix, T))
178
+ .toBe('/api/browser-proxy/127.0.0.1:8080/new-path?x=1&_bp_tok=TESTTOK');
171
179
  });
172
180
  (0, vitest_1.it)('absolute redirect 不同 scheme → 原样保留(防协议混淆)', () => {
173
- (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('https://127.0.0.1:8080/foo', parsed, prefix))
181
+ (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('https://127.0.0.1:8080/foo', parsed, prefix, T))
174
182
  .toBe('https://127.0.0.1:8080/foo');
175
183
  });
176
184
  (0, vitest_1.it)('absolute redirect 不同 host → 原样保留', () => {
177
- (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('http://other.example/foo', parsed, prefix))
185
+ (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('http://other.example/foo', parsed, prefix, T))
178
186
  .toBe('http://other.example/foo');
179
187
  });
180
- (0, vitest_1.it)('root-relative redirect → 加前缀', () => {
181
- (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('/foo?x=1', parsed, prefix))
182
- .toBe('/api/browser-proxy/127.0.0.1:8080/foo?x=1');
188
+ (0, vitest_1.it)('root-relative redirect → 加前缀 + token', () => {
189
+ (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('/foo?x=1', parsed, prefix, T))
190
+ .toBe('/api/browser-proxy/127.0.0.1:8080/foo?x=1&_bp_tok=TESTTOK');
183
191
  });
184
192
  (0, vitest_1.it)('protocol-relative (//) → 不改', () => {
185
- (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('//cdn.example/x', parsed, prefix))
193
+ (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('//cdn.example/x', parsed, prefix, T))
186
194
  .toBe('//cdn.example/x');
187
195
  });
188
196
  (0, vitest_1.it)('纯相对路径 → 不改', () => {
189
- (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('foo/bar', parsed, prefix))
197
+ (0, vitest_1.expect)((0, browser_proxy_1.rewriteLocationHeader)('foo/bar', parsed, prefix, T))
190
198
  .toBe('foo/bar');
191
199
  });
192
200
  });
201
+ (0, vitest_1.describe)('stripTokenFromSubPath', () => {
202
+ (0, vitest_1.it)('移除 _bp_tok 不留空 query', () => {
203
+ (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/foo?_bp_tok=abc')).toBe('/foo');
204
+ (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/foo?_bp_tok=abc#hash')).toBe('/foo#hash');
205
+ });
206
+ (0, vitest_1.it)('保留其它 query', () => {
207
+ (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/foo?x=1&_bp_tok=abc&y=2')).toBe('/foo?x=1&y=2');
208
+ (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/foo?_bp_tok=abc&x=1')).toBe('/foo?x=1');
209
+ (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/foo?x=1&_bp_tok=abc')).toBe('/foo?x=1');
210
+ });
211
+ (0, vitest_1.it)('没 token 不动', () => {
212
+ (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/foo')).toBe('/foo');
213
+ (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/foo?x=1')).toBe('/foo?x=1');
214
+ (0, vitest_1.expect)((0, browser_proxy_1.stripTokenFromSubPath)('/')).toBe('/');
215
+ });
216
+ });
193
217
  (0, vitest_1.describe)('e2e: browser-proxy router 跑通本地 dummy server', () => {
194
218
  let upstream;
195
219
  let upstreamPort = 0;
196
220
  let proxy;
197
221
  let proxyPort = 0;
222
+ const upstreamSeenUrls = [];
198
223
  (0, vitest_1.beforeAll)(async () => {
199
224
  upstream = http.createServer((req, res) => {
225
+ upstreamSeenUrls.push(req.url || '');
200
226
  if (req.url === '/index.html') {
201
227
  res.writeHead(200, {
202
228
  'Content-Type': 'text/html; charset=utf-8',
@@ -232,26 +258,25 @@ function mintProxyCookie() {
232
258
  await new Promise((resolve) => upstream.close(() => resolve()));
233
259
  await new Promise((resolve) => proxy.close(() => resolve()));
234
260
  });
235
- const withCookie = (path, init) => fetch(`http://127.0.0.1:${proxyPort}${path}`, { ...(init ?? {}), headers: { ...(init?.headers ?? {}), Cookie: mintProxyCookie() } });
236
- (0, vitest_1.it)(' cookie 403 (session required)', async () => {
261
+ const withTok = (path, init) => {
262
+ const sep = path.includes('?') ? '&' : '?';
263
+ return fetch(`http://127.0.0.1:${proxyPort}${path}${sep}${tok()}`, init);
264
+ };
265
+ (0, vitest_1.it)('无 token → 403 (session required)', async () => {
237
266
  const r = await fetch(`http://127.0.0.1:${proxyPort}/api/browser-proxy/127.0.0.1:${upstreamPort}/index.html`);
238
267
  (0, vitest_1.expect)(r.status).toBe(403);
239
268
  });
240
- (0, vitest_1.it)('伪造的 cookie (错 typ 或错 secret) → 403', async () => {
269
+ (0, vitest_1.it)('伪造的 token (错 typ 或错 secret) → 403', async () => {
241
270
  const config = (0, config_1.getConfig)();
242
271
  const wrongTyp = jwt.sign({ username: 'x', typ: 'user' }, config.jwtSecret, { expiresIn: '1h' });
243
- const r1 = await fetch(`http://127.0.0.1:${proxyPort}/api/browser-proxy/127.0.0.1:${upstreamPort}/index.html`, {
244
- headers: { Cookie: `ccweb_bp=${encodeURIComponent(wrongTyp)}` },
245
- });
272
+ const r1 = await fetch(`http://127.0.0.1:${proxyPort}/api/browser-proxy/127.0.0.1:${upstreamPort}/index.html?_bp_tok=${encodeURIComponent(wrongTyp)}`);
246
273
  (0, vitest_1.expect)(r1.status).toBe(403);
247
274
  const wrongSecret = jwt.sign({ username: 'x', typ: 'browser-proxy' }, 'not-the-secret', { expiresIn: '1h' });
248
- const r2 = await fetch(`http://127.0.0.1:${proxyPort}/api/browser-proxy/127.0.0.1:${upstreamPort}/index.html`, {
249
- headers: { Cookie: `ccweb_bp=${encodeURIComponent(wrongSecret)}` },
250
- });
275
+ const r2 = await fetch(`http://127.0.0.1:${proxyPort}/api/browser-proxy/127.0.0.1:${upstreamPort}/index.html?_bp_tok=${encodeURIComponent(wrongSecret)}`);
251
276
  (0, vitest_1.expect)(r2.status).toBe(403);
252
277
  });
253
- (0, vitest_1.it)('代理 HTML:strip X-Frame / CSP / Clear-Site-Data + rewrite path + 注入 CSP sandbox', async () => {
254
- const r = await withCookie(`/api/browser-proxy/127.0.0.1:${upstreamPort}/index.html`);
278
+ (0, vitest_1.it)('代理 HTML:strip X-Frame / CSP / Clear-Site-Data + rewrite path + 注入 CSP sandbox + 带 token', async () => {
279
+ const r = await withTok(`/api/browser-proxy/127.0.0.1:${upstreamPort}/index.html`);
255
280
  (0, vitest_1.expect)(r.status).toBe(200);
256
281
  (0, vitest_1.expect)(r.headers.get('x-frame-options')).toBeNull();
257
282
  (0, vitest_1.expect)(r.headers.get('content-security-policy')).toContain('sandbox');
@@ -259,36 +284,45 @@ function mintProxyCookie() {
259
284
  (0, vitest_1.expect)(r.headers.get('content-security-policy')).not.toContain('allow-popups-to-escape-sandbox');
260
285
  (0, vitest_1.expect)(r.headers.get('clear-site-data')).toBeNull();
261
286
  const body = await r.text();
262
- (0, vitest_1.expect)(body).toContain(`href="/api/browser-proxy/127.0.0.1:${upstreamPort}/about"`);
263
- (0, vitest_1.expect)(body).toContain(`src="/api/browser-proxy/127.0.0.1:${upstreamPort}/m.js"`);
287
+ (0, vitest_1.expect)(body).toMatch(new RegExp(`href="/api/browser-proxy/127\\.0\\.0\\.1:${upstreamPort}/about\\?_bp_tok=`));
288
+ (0, vitest_1.expect)(body).toMatch(new RegExp(`src="/api/browser-proxy/127\\.0\\.0\\.1:${upstreamPort}/m\\.js\\?_bp_tok=`));
264
289
  (0, vitest_1.expect)(body).not.toContain('<base');
265
290
  });
266
291
  (0, vitest_1.it)('代理 JS:原样透传不重写', async () => {
267
- const r = await withCookie(`/api/browser-proxy/127.0.0.1:${upstreamPort}/m.js`);
292
+ const r = await withTok(`/api/browser-proxy/127.0.0.1:${upstreamPort}/m.js`);
268
293
  (0, vitest_1.expect)(r.status).toBe(200);
269
294
  (0, vitest_1.expect)(await r.text()).toBe('console.log(1);');
270
295
  });
271
- (0, vitest_1.it)('代理 redirect:root-relative Location 加前缀', async () => {
272
- const r = await withCookie(`/api/browser-proxy/127.0.0.1:${upstreamPort}/redir`, { redirect: 'manual' });
296
+ (0, vitest_1.it)('代理 redirect:root-relative Location 加前缀 + token', async () => {
297
+ const r = await withTok(`/api/browser-proxy/127.0.0.1:${upstreamPort}/redir`, { redirect: 'manual' });
273
298
  (0, vitest_1.expect)(r.status).toBe(302);
274
- (0, vitest_1.expect)(r.headers.get('location')).toBe(`/api/browser-proxy/127.0.0.1:${upstreamPort}/landed`);
299
+ (0, vitest_1.expect)(r.headers.get('location')).toMatch(new RegExp(`^/api/browser-proxy/127\\.0\\.0\\.1:${upstreamPort}/landed\\?_bp_tok=`));
275
300
  });
276
301
  (0, vitest_1.it)('拒绝公网字面量 IP (403)', async () => {
277
- const r = await withCookie('/api/browser-proxy/1.1.1.1:80/');
302
+ const r = await withTok('/api/browser-proxy/1.1.1.1:80/');
278
303
  (0, vitest_1.expect)(r.status).toBe(403);
279
304
  });
280
305
  (0, vitest_1.it)('拒绝 cloud metadata IP (403)', async () => {
281
- const r = await withCookie('/api/browser-proxy/169.254.169.254:80/');
306
+ const r = await withTok('/api/browser-proxy/169.254.169.254:80/');
282
307
  (0, vitest_1.expect)(r.status).toBe(403);
283
308
  });
284
309
  (0, vitest_1.it)('拒绝非法 hostport (400)', async () => {
285
- const r = await withCookie('/api/browser-proxy/not-a-host');
310
+ const r = await withTok('/api/browser-proxy/not-a-host');
286
311
  (0, vitest_1.expect)(r.status).toBe(400);
287
312
  });
288
313
  (0, vitest_1.it)('拒绝黑名单端口 (400)', async () => {
289
- const r = await withCookie('/api/browser-proxy/127.0.0.1:22/');
314
+ const r = await withTok('/api/browser-proxy/127.0.0.1:22/');
290
315
  (0, vitest_1.expect)(r.status).toBe(400);
291
316
  });
317
+ (0, vitest_1.it)('session token 不泄露给上游', async () => {
318
+ upstreamSeenUrls.length = 0;
319
+ await withTok(`/api/browser-proxy/127.0.0.1:${upstreamPort}/index.html`);
320
+ await withTok(`/api/browser-proxy/127.0.0.1:${upstreamPort}/m.js`);
321
+ (0, vitest_1.expect)(upstreamSeenUrls.length).toBeGreaterThan(0);
322
+ for (const u of upstreamSeenUrls) {
323
+ (0, vitest_1.expect)(u).not.toContain('_bp_tok');
324
+ }
325
+ });
292
326
  (0, vitest_1.it)('_session 无 Bearer token → 401', async () => {
293
327
  const r = await fetch(`http://127.0.0.1:${proxyPort}/api/browser-proxy/_session`, { method: 'POST' });
294
328
  (0, vitest_1.expect)(r.status).toBe(401);