Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ jobs:
- run: node --check assets/tmwd_cdp_bridge/background.js
- run: node --check assets/tmwd_cdp_bridge/content.js
- run: node --check assets/tmwd_cdp_bridge/popup.js
- run: node --test tests/*.test.mjs
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

所有重要变更都会记录在这里。日期使用北京时间自然日。

## 未发布

### 修复

- Chrome 扩展不再全局移除页面 CSP 响应头或 meta 标签;升级时会清理旧版持久化的动态规则,避免与 Tampermonkey 等注入脚本共同导致 YouTube renderer 满载卡死。受 CSP 限制的页面执行继续使用现有 CDP fallback。

## 已发布

## v0.3.4 - 2026-05-19
Expand Down
25 changes: 13 additions & 12 deletions assets/tmwd_cdp_bridge/background.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
// background.js - Cookie + CDP Bridge
const LEGACY_CSP_RULE_ID = 9999;

async function removeLegacyCspRule() {
try {
// Dynamic rules survive extension upgrades, so explicitly migrate existing installs.
await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: [LEGACY_CSP_RULE_ID] });
} catch (e) {
console.warn('[TMWD] Failed to remove legacy CSP rule:', e.message);
}
}

removeLegacyCspRule();
chrome.runtime.onInstalled.addListener(() => {
console.log('CDP Bridge installed');
// Strip CSP headers to allow eval/inline scripts
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [9999],
addRules: [{
id: 9999, priority: 1,
action: { type: 'modifyHeaders', responseHeaders: [
{ header: 'content-security-policy', operation: 'remove' },
{ header: 'content-security-policy-report-only', operation: 'remove' }
]},
condition: { urlFilter: '*', resourceTypes: ['main_frame', 'sub_frame'] }
}]
});
removeLegacyCspRule();
});

let lastCommandAt = 0;
Expand Down
3 changes: 0 additions & 3 deletions assets/tmwd_cdp_bridge/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ if (window.__agentBrowserCliCleanup) window.__agentBrowserCliCleanup();
if (window.__agentBrowserCliObserverCleanup) window.__agentBrowserCliObserverCleanup();
document.querySelectorAll('#agent-browser-cli-ind,#agent-browser-cli-style').forEach(e => e.remove());

// Remove meta CSP tags
document.querySelectorAll('meta[http-equiv="Content-Security-Policy"]').forEach(e => e.remove());

/**
* Render a right-side floating badge that reflects the real bridge connection status.
*/
Expand Down
121 changes: 121 additions & 0 deletions tests/extension_csp_policy.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import assert from 'node:assert/strict';
import { webcrypto } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
import vm from 'node:vm';

const backgroundPath = new URL('../assets/tmwd_cdp_bridge/background.js', import.meta.url);
const contentPath = new URL('../assets/tmwd_cdp_bridge/content.js', import.meta.url);

function eventTarget(listeners = []) {
return {
addListener(listener) {
listeners.push(listener);
},
removeListener() {},
};
}

function createBackgroundContext(dynamicRuleCalls) {
const onInstalled = [];
const noOpEvent = eventTarget();
const chrome = {
alarms: {
clear: async () => true,
create() {},
onAlarm: noOpEvent,
},
debugger: {
onDetach: noOpEvent,
onEvent: noOpEvent,
},
declarativeNetRequest: {
async updateDynamicRules(update) {
dynamicRuleCalls.push(update);
},
},
extension: {
async isAllowedFileSchemeAccess() {
return false;
},
},
runtime: {
getManifest() {
return { version: 'test' };
},
onInstalled: eventTarget(onInstalled),
onMessage: noOpEvent,
onStartup: noOpEvent,
},
scripting: {
async executeScript() {
return [];
},
},
storage: {
local: {
async get(defaults) {
return defaults;
},
async set() {},
},
},
tabGroups: {
async query() {
return [];
},
},
tabs: {
async query() {
return [];
},
onCreated: noOpEvent,
onRemoved: noOpEvent,
onUpdated: noOpEvent,
},
};

class FakeWebSocket {}
FakeWebSocket.CONNECTING = 0;
FakeWebSocket.OPEN = 1;

return {
context: vm.createContext({
AbortController,
WebSocket: FakeWebSocket,
chrome,
console: { log() {}, warn() {}, error() {} },
crypto: webcrypto,
fetch: async () => {
throw new Error('daemon unavailable in test');
},
setTimeout: () => 1,
}),
onInstalled,
};
}

async function flushMicrotasks() {
await Promise.resolve();
await Promise.resolve();
}

test('extension startup removes the legacy CSP rule without installing a replacement', async () => {
const source = await readFile(backgroundPath, 'utf8');
const dynamicRuleCalls = [];
const { context, onInstalled } = createBackgroundContext(dynamicRuleCalls);

vm.runInContext(source, context, { filename: backgroundPath.pathname });
await flushMicrotasks();

assert.deepEqual(JSON.parse(JSON.stringify(dynamicRuleCalls[0])), { removeRuleIds: [9999] });
assert.ok(dynamicRuleCalls.every((update) => !('addRules' in update)));

for (const listener of onInstalled) await listener({ reason: 'update' });
assert.ok(dynamicRuleCalls.every((update) => !('addRules' in update)));
});

test('content script leaves page CSP metadata untouched', async () => {
const source = await readFile(contentPath, 'utf8');
assert.doesNotMatch(source, /Content-Security-Policy/i);
});
Loading