@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.
- package/README.md +1 -1
- package/backend/dist/__tests__/browser-chrome-e2e.test.d.ts +2 -0
- package/backend/dist/__tests__/browser-chrome-e2e.test.d.ts.map +1 -0
- package/backend/dist/__tests__/browser-chrome-e2e.test.js +121 -0
- package/backend/dist/__tests__/browser-chrome-e2e.test.js.map +1 -0
- package/backend/dist/__tests__/browser-chrome.test.d.ts +2 -0
- package/backend/dist/__tests__/browser-chrome.test.d.ts.map +1 -0
- package/backend/dist/__tests__/browser-chrome.test.js +169 -0
- package/backend/dist/__tests__/browser-chrome.test.js.map +1 -0
- package/backend/dist/__tests__/browser-proxy.test.js +81 -47
- package/backend/dist/__tests__/browser-proxy.test.js.map +1 -1
- package/backend/dist/browser-chrome/input-forwarder.d.ts +32 -0
- package/backend/dist/browser-chrome/input-forwarder.d.ts.map +1 -0
- package/backend/dist/browser-chrome/input-forwarder.js +92 -0
- package/backend/dist/browser-chrome/input-forwarder.js.map +1 -0
- package/backend/dist/browser-chrome/screencast.d.ts +21 -0
- package/backend/dist/browser-chrome/screencast.d.ts.map +1 -0
- package/backend/dist/browser-chrome/screencast.js +78 -0
- package/backend/dist/browser-chrome/screencast.js.map +1 -0
- package/backend/dist/browser-chrome/session-manager.d.ts +38 -0
- package/backend/dist/browser-chrome/session-manager.d.ts.map +1 -0
- package/backend/dist/browser-chrome/session-manager.js +175 -0
- package/backend/dist/browser-chrome/session-manager.js.map +1 -0
- package/backend/dist/index.d.ts.map +1 -1
- package/backend/dist/index.js +53 -0
- package/backend/dist/index.js.map +1 -1
- package/backend/dist/routes/browser-chrome.d.ts +4 -0
- package/backend/dist/routes/browser-chrome.d.ts.map +1 -0
- package/backend/dist/routes/browser-chrome.js +106 -0
- package/backend/dist/routes/browser-chrome.js.map +1 -0
- package/backend/dist/routes/browser-proxy.d.ts +16 -4
- package/backend/dist/routes/browser-proxy.d.ts.map +1 -1
- package/backend/dist/routes/browser-proxy.js +78 -41
- package/backend/dist/routes/browser-proxy.js.map +1 -1
- package/backend/package-lock.json +45 -0
- package/backend/package.json +1 -0
- package/frontend/dist/assets/{ChatOverlay-W-pSFHsV.js → ChatOverlay-DWnJouqf.js} +1 -1
- package/frontend/dist/assets/{GraphPreview-CcuxUzQH.js → GraphPreview-BhYiu0BC.js} +1 -1
- package/frontend/dist/assets/{MobilePage-DEBO3i1o.js → MobilePage-CpwaYT93.js} +3 -3
- package/frontend/dist/assets/{OfficePreview-DY-ew_ex.js → OfficePreview-AIsCrFJN.js} +2 -2
- package/frontend/dist/assets/{PdfPreview-Bl0MS_5m.js → PdfPreview-BOf3iH2G.js} +1 -1
- package/frontend/dist/assets/{ProjectPage-tcwSSEFX.js → ProjectPage-BLDngPUN.js} +5 -5
- package/frontend/dist/assets/SettingsPage-898duvMO.js +13 -0
- package/frontend/dist/assets/{SkillHubPage-hH5XcB57.js → SkillHubPage-Cf3oFMk3.js} +1 -1
- package/frontend/dist/assets/{chevron-down-B3q2CU-d.js → chevron-down-CWdHpHQs.js} +1 -1
- package/frontend/dist/assets/{index-CZOz50el.js → index-DoKF15jh.js} +1 -1
- package/frontend/dist/assets/{index-nYsy3LMF.js → index-DtlKk29C.js} +2 -2
- package/frontend/dist/assets/{index-DsYny3RQ.js → index-DzV3STk-.js} +1 -1
- package/frontend/dist/assets/{jszip.min-9o6zKk8v.js → jszip.min-BiyTb6vp.js} +1 -1
- package/frontend/dist/assets/{search-B9B3Cq4V.js → search-DAr7qjB5.js} +1 -1
- package/frontend/dist/assets/select-IaWIRuom.js +13 -0
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
- package/frontend/dist/assets/SettingsPage-DU649aEa.js +0 -13
- 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-
|
|
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 @@
|
|
|
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":"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"}
|
|
@@ -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
|
|
46
|
-
// the test machine has a config.json so getConfig() works.
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
236
|
-
|
|
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)('伪造的
|
|
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
|
|
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).
|
|
263
|
-
(0, vitest_1.expect)(body).
|
|
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
|
|
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
|
|
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')).
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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);
|