refactor: split handleCommand into handleCommandInternal + HTTP wrapper

Chain subcommands now route through handleCommandInternal for full security
enforcement (scope, domain, tab ownership, rate limiting, content wrapping).
Adds recursion guard for nested chains, rate-limit exemption for chain
subcommands, and activity event suppression (1 event per chain, not per sub).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-05 11:05:11 -07:00
parent 49eac4c299
commit 905f1ddd38
4 changed files with 229 additions and 161 deletions

View File

@@ -44,10 +44,13 @@ describe('Server auth security', () => {
});
// Test 1c: newtab must check domain restrictions (CSO finding #5)
// Domain check for newtab is now unified with goto in the scope check section:
// (command === 'goto' || command === 'newtab') && args[0] → checkDomain
test('newtab enforces domain restrictions', () => {
const newtabBlock = sliceBetween(SERVER_SRC, "newtab with ownership for scoped tokens", "Block mutation commands while watching");
expect(newtabBlock).toContain('checkDomain');
expect(newtabBlock).toContain('Domain not allowed');
const scopeBlock = sliceBetween(SERVER_SRC, "Scope check (for scoped tokens)", "Pin to a specific tab");
expect(scopeBlock).toContain("command === 'newtab'");
expect(scopeBlock).toContain('checkDomain');
expect(scopeBlock).toContain('Domain not allowed');
});
// Test 2: /refs endpoint requires auth via validateAuth
@@ -168,7 +171,7 @@ describe('Server auth security', () => {
// Test 10d: server passes tokenInfo to handleMetaCommand
test('server passes tokenInfo to handleMetaCommand', () => {
expect(SERVER_SRC).toContain('handleMetaCommand(command, args, browserManager, shutdown, tokenInfo)');
expect(SERVER_SRC).toContain('handleMetaCommand(command, args, browserManager, shutdown, tokenInfo,');
});
// Test 10e: activity attribution includes clientId

View File

@@ -502,12 +502,12 @@ describe('BROWSE_TAB tab pinning (cross-tab isolation)', () => {
expect(cliSrc).toContain('tabId: parseInt(browseTab');
});
test('handleCommand accepts tabId from request body', () => {
test('handleCommandInternal accepts tabId from request body', () => {
const handleFn = serverSrc.slice(
serverSrc.indexOf('async function handleCommand('),
serverSrc.indexOf('\nasync function ', serverSrc.indexOf('async function handleCommand(') + 1) > 0
? serverSrc.indexOf('\nasync function ', serverSrc.indexOf('async function handleCommand(') + 1)
: serverSrc.indexOf('\n// ', serverSrc.indexOf('async function handleCommand(') + 200),
serverSrc.indexOf('async function handleCommandInternal('),
serverSrc.indexOf('\n/** HTTP wrapper', serverSrc.indexOf('async function handleCommandInternal(') + 1) > 0
? serverSrc.indexOf('\n/** HTTP wrapper', serverSrc.indexOf('async function handleCommandInternal(') + 1)
: serverSrc.indexOf('\nasync function ', serverSrc.indexOf('async function handleCommandInternal(') + 200),
);
// Should destructure tabId from body
expect(handleFn).toContain('tabId');
@@ -516,10 +516,10 @@ describe('BROWSE_TAB tab pinning (cross-tab isolation)', () => {
expect(handleFn).toContain('switchTab(tabId');
});
test('handleCommand restores active tab after command (success path)', () => {
test('handleCommandInternal restores active tab after command (success path)', () => {
// On success, should restore savedTabId without stealing focus
const handleFn = serverSrc.slice(
serverSrc.indexOf('async function handleCommand('),
serverSrc.indexOf('async function handleCommandInternal('),
serverSrc.length,
);
// Count restore calls — should appear in both success and error paths
@@ -527,18 +527,18 @@ describe('BROWSE_TAB tab pinning (cross-tab isolation)', () => {
expect(restoreCount).toBeGreaterThanOrEqual(2); // success + error paths
});
test('handleCommand restores active tab on error path', () => {
test('handleCommandInternal restores active tab on error path', () => {
// The catch block should also restore
const catchBlock = serverSrc.slice(
serverSrc.indexOf('} catch (err: any) {', serverSrc.indexOf('async function handleCommand(')),
serverSrc.indexOf('} catch (err: any) {', serverSrc.indexOf('async function handleCommandInternal(')),
);
expect(catchBlock).toContain('switchTab(savedTabId');
});
test('tab pinning only activates when tabId is provided', () => {
const handleFn = serverSrc.slice(
serverSrc.indexOf('async function handleCommand('),
serverSrc.indexOf('try {', serverSrc.indexOf('async function handleCommand(') + 1),
serverSrc.indexOf('async function handleCommandInternal('),
serverSrc.indexOf('try {', serverSrc.indexOf('async function handleCommandInternal(') + 1),
);
// Should check tabId is not undefined/null before switching
expect(handleFn).toContain('tabId !== undefined');