Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3984e0ae5d |
Vendored
+73
-4
@@ -31,7 +31,7 @@ async function ensureAttached(tabId) {
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
async function evaluate(tabId, expression) {
|
||||
async function evaluate$1(tabId, expression) {
|
||||
await ensureAttached(tabId);
|
||||
const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
|
||||
expression,
|
||||
@@ -44,7 +44,6 @@ async function evaluate(tabId, expression) {
|
||||
}
|
||||
return result.result?.value;
|
||||
}
|
||||
const evaluateAsync = evaluate;
|
||||
async function screenshot(tabId, options = {}) {
|
||||
await ensureAttached(tabId);
|
||||
const format = options.format ?? "png";
|
||||
@@ -74,7 +73,7 @@ async function screenshot(tabId, options = {}) {
|
||||
}
|
||||
}
|
||||
}
|
||||
function detach(tabId) {
|
||||
function detach$1(tabId) {
|
||||
if (!attached.has(tabId)) return;
|
||||
attached.delete(tabId);
|
||||
try {
|
||||
@@ -82,7 +81,7 @@ function detach(tabId) {
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
function registerListeners() {
|
||||
function registerListeners$1() {
|
||||
chrome.tabs.onRemoved.addListener((tabId) => {
|
||||
attached.delete(tabId);
|
||||
});
|
||||
@@ -91,6 +90,76 @@ function registerListeners() {
|
||||
});
|
||||
}
|
||||
|
||||
const cspBlockedTabs = /* @__PURE__ */ new Set();
|
||||
async function evaluate(tabId, expression) {
|
||||
if (cspBlockedTabs.has(tabId)) {
|
||||
return evaluate$1(tabId, expression);
|
||||
}
|
||||
try {
|
||||
const results = await chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
world: "MAIN",
|
||||
func: (expr) => {
|
||||
try {
|
||||
const result = (0, eval)(expr);
|
||||
if (result && typeof result === "object" && typeof result.then === "function") {
|
||||
return result.then(
|
||||
(v) => JSON.stringify({ ok: true, v }),
|
||||
(e) => JSON.stringify({ ok: false, err: e?.message || String(e) })
|
||||
);
|
||||
}
|
||||
return JSON.stringify({ ok: true, v: result });
|
||||
} catch (e) {
|
||||
return JSON.stringify({ ok: false, err: e?.message || String(e) });
|
||||
}
|
||||
},
|
||||
args: [expression]
|
||||
});
|
||||
if (!results || results.length === 0) {
|
||||
throw new Error("executeScript returned no results");
|
||||
}
|
||||
const frame = results[0];
|
||||
if (frame.error) {
|
||||
throw new Error(frame.error.message || String(frame.error));
|
||||
}
|
||||
const raw = frame.result;
|
||||
if (raw === null || raw === void 0) {
|
||||
return evaluate$1(tabId, expression);
|
||||
}
|
||||
if (typeof raw === "string") {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed.ok) {
|
||||
const err = parsed.err || "";
|
||||
if (err.includes("Content Security Policy") || err.includes("'unsafe-eval'")) {
|
||||
cspBlockedTabs.add(tabId);
|
||||
return evaluate$1(tabId, expression);
|
||||
}
|
||||
throw new Error(err || "Eval error");
|
||||
}
|
||||
return parsed.v;
|
||||
}
|
||||
return raw;
|
||||
} catch (e) {
|
||||
const msg = e?.message || String(e);
|
||||
if (msg.includes("Content Security Policy") || msg.includes("'unsafe-eval'")) {
|
||||
cspBlockedTabs.add(tabId);
|
||||
return evaluate$1(tabId, expression);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
const evaluateAsync = evaluate;
|
||||
function detach(tabId) {
|
||||
cspBlockedTabs.delete(tabId);
|
||||
detach$1(tabId);
|
||||
}
|
||||
function registerListeners() {
|
||||
registerListeners$1();
|
||||
chrome.tabs.onRemoved.addListener((tabId) => {
|
||||
cspBlockedTabs.delete(tabId);
|
||||
});
|
||||
}
|
||||
|
||||
let ws = null;
|
||||
let reconnectTimer = null;
|
||||
let reconnectAttempts = 0;
|
||||
|
||||
@@ -4,11 +4,15 @@
|
||||
"version": "0.2.0",
|
||||
"description": "Bridge between opencli CLI and your browser — execute commands, read cookies, manage tabs.",
|
||||
"permissions": [
|
||||
"debugger",
|
||||
"tabs",
|
||||
"cookies",
|
||||
"activeTab",
|
||||
"alarms"
|
||||
"alarms",
|
||||
"scripting",
|
||||
"debugger"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "dist/background.js",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import type { Command, Result } from './protocol';
|
||||
import { DAEMON_WS_URL, WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY } from './protocol';
|
||||
import * as executor from './cdp';
|
||||
import * as executor from './scripting';
|
||||
|
||||
let ws: WebSocket | null = null;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* JS execution via chrome.scripting.executeScript (Manifest V3).
|
||||
*
|
||||
* Unlike cdp.ts which uses chrome.debugger (CDP), this executor leaves
|
||||
* ZERO CDP fingerprint on the page. Websites cannot detect that
|
||||
* automation is controlling the browser.
|
||||
*
|
||||
* LIMITATION: Sites with strict Content Security Policy (CSP) that block
|
||||
* `unsafe-eval` (e.g., Twitter/X, Google) will reject eval(). In that
|
||||
* case, we automatically fall back to CDP via cdp.ts.
|
||||
*/
|
||||
|
||||
import * as cdp from './cdp';
|
||||
|
||||
/** Set of tabIds where scripting failed due to CSP, so we skip straight to CDP */
|
||||
const cspBlockedTabs = new Set<number>();
|
||||
|
||||
export async function evaluate(tabId: number, expression: string): Promise<unknown> {
|
||||
// If we already know this tab blocks eval, go straight to CDP
|
||||
if (cspBlockedTabs.has(tabId)) {
|
||||
return cdp.evaluate(tabId, expression);
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
world: 'MAIN',
|
||||
func: (expr: string) => {
|
||||
try {
|
||||
const result = (0, eval)(expr);
|
||||
if (result && typeof result === 'object' && typeof result.then === 'function') {
|
||||
return result.then(
|
||||
(v: unknown) => JSON.stringify({ ok: true, v }),
|
||||
(e: unknown) => JSON.stringify({ ok: false, err: (e as Error)?.message || String(e) }),
|
||||
);
|
||||
}
|
||||
return JSON.stringify({ ok: true, v: result });
|
||||
} catch (e) {
|
||||
return JSON.stringify({ ok: false, err: (e as Error)?.message || String(e) });
|
||||
}
|
||||
},
|
||||
args: [expression],
|
||||
});
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
throw new Error('executeScript returned no results');
|
||||
}
|
||||
|
||||
const frame = results[0];
|
||||
if ((frame as any).error) {
|
||||
throw new Error((frame as any).error.message || String((frame as any).error));
|
||||
}
|
||||
|
||||
const raw = frame.result;
|
||||
|
||||
// MAIN world eval() returns null if Chrome can't serialize the return value
|
||||
if (raw === null || raw === undefined) {
|
||||
// Fall back to CDP for this execution
|
||||
return cdp.evaluate(tabId, expression);
|
||||
}
|
||||
|
||||
// Parse our JSON envelope
|
||||
if (typeof raw === 'string') {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed.ok) {
|
||||
const err = parsed.err || '';
|
||||
// Detect CSP errors and remember this tab
|
||||
if (err.includes('Content Security Policy') || err.includes("'unsafe-eval'")) {
|
||||
cspBlockedTabs.add(tabId);
|
||||
return cdp.evaluate(tabId, expression);
|
||||
}
|
||||
throw new Error(err || 'Eval error');
|
||||
}
|
||||
return parsed.v;
|
||||
}
|
||||
|
||||
return raw;
|
||||
} catch (e) {
|
||||
const msg = (e as Error)?.message || String(e);
|
||||
// Catch CSP errors that bubble up as exceptions too
|
||||
if (msg.includes('Content Security Policy') || msg.includes("'unsafe-eval'")) {
|
||||
cspBlockedTabs.add(tabId);
|
||||
return cdp.evaluate(tabId, expression);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export const evaluateAsync = evaluate;
|
||||
|
||||
// Delegate to CDP for screenshot (no scripting API alternative)
|
||||
export { screenshot } from './cdp';
|
||||
|
||||
export function detach(tabId: number): void {
|
||||
cspBlockedTabs.delete(tabId);
|
||||
cdp.detach(tabId);
|
||||
}
|
||||
|
||||
export function registerListeners(): void {
|
||||
cdp.registerListeners();
|
||||
// Clean up CSP cache when tabs are removed
|
||||
chrome.tabs.onRemoved.addListener((tabId) => {
|
||||
cspBlockedTabs.delete(tabId);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user