nothing-browser 0.0.10 → 0.0.12
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/dist/client/index.js +46 -20
- package/dist/piggy.js +288 -20
- package/dist/register/index.js +242 -0
- package/package.json +1 -1
- package/piggy/client/index.ts +124 -248
- package/piggy/intercept/scripts.ts +153 -0
- package/piggy/register/index.ts +182 -36
package/piggy/client/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { dirname } from "path";
|
|
|
5
5
|
import { platform } from "os";
|
|
6
6
|
import logger from "../logger";
|
|
7
7
|
|
|
8
|
-
const SOCKET_PATH = platform() === 'win32'
|
|
8
|
+
const SOCKET_PATH = platform() === 'win32'
|
|
9
9
|
? '\\\\.\\pipe\\piggy'
|
|
10
10
|
: '/tmp/piggy';
|
|
11
11
|
|
|
@@ -17,6 +17,7 @@ export class PiggyClient {
|
|
|
17
17
|
private buf = "";
|
|
18
18
|
private eventBuffer = "";
|
|
19
19
|
private eventHandlers = new Map<string, Map<string, (data: any) => Promise<any>>>();
|
|
20
|
+
private globalEventHandlers = new Map<string, Set<(data: any) => void>>();
|
|
20
21
|
|
|
21
22
|
constructor(socketPath = SOCKET_PATH) {
|
|
22
23
|
this.socketPath = socketPath;
|
|
@@ -39,17 +40,17 @@ export class PiggyClient {
|
|
|
39
40
|
this.eventBuffer += chunk;
|
|
40
41
|
const lines = this.eventBuffer.split("\n");
|
|
41
42
|
this.eventBuffer = lines.pop()!;
|
|
42
|
-
|
|
43
|
+
|
|
43
44
|
for (const line of lines) {
|
|
44
45
|
if (!line.trim()) continue;
|
|
45
46
|
try {
|
|
46
47
|
const msg = JSON.parse(line);
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
if (msg.type === "event") {
|
|
49
50
|
this.handleEvent(msg);
|
|
50
51
|
continue;
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
|
|
53
54
|
const p = this.pending.get(msg.id);
|
|
54
55
|
if (p) {
|
|
55
56
|
this.pending.delete(msg.id);
|
|
@@ -75,31 +76,31 @@ export class PiggyClient {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
private handleEvent(event: any) {
|
|
79
|
+
// ── exposed_call ──────────────────────────────────────────────────────────
|
|
78
80
|
if (event.event === "exposed_call") {
|
|
79
81
|
const { tabId, name, callId, data } = event;
|
|
80
82
|
const effectiveTabId = tabId || "default";
|
|
81
83
|
const handlers = this.eventHandlers.get(effectiveTabId);
|
|
82
84
|
const handler = handlers?.get(name);
|
|
83
|
-
|
|
85
|
+
|
|
84
86
|
if (handler) {
|
|
85
|
-
|
|
87
|
+
// ✅ FIX: data can be a plain string (e.g., "OPENING") or JSON
|
|
88
|
+
let parsedData;
|
|
89
|
+
try {
|
|
90
|
+
parsedData = JSON.parse(data || "null");
|
|
91
|
+
} catch {
|
|
92
|
+
parsedData = data; // fallback to raw string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Promise.resolve(handler(parsedData))
|
|
86
96
|
.then(response => {
|
|
87
97
|
if (response && typeof response === "object" && "success" in response) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}).catch(e => logger.error(`Failed to send exposed result: ${e}`));
|
|
95
|
-
} else {
|
|
96
|
-
this.send("exposed.result", {
|
|
97
|
-
tabId: effectiveTabId,
|
|
98
|
-
callId,
|
|
99
|
-
result: response.error || "Unknown error",
|
|
100
|
-
isError: true
|
|
101
|
-
}).catch(e => logger.error(`Failed to send exposed error: ${e}`));
|
|
102
|
-
}
|
|
98
|
+
this.send("exposed.result", {
|
|
99
|
+
tabId: effectiveTabId,
|
|
100
|
+
callId,
|
|
101
|
+
result: response.success ? JSON.stringify(response.result) : (response.error || "Unknown error"),
|
|
102
|
+
isError: !response.success
|
|
103
|
+
}).catch(e => logger.error(`Failed to send exposed result: ${e}`));
|
|
103
104
|
} else {
|
|
104
105
|
this.send("exposed.result", {
|
|
105
106
|
tabId: effectiveTabId,
|
|
@@ -120,15 +121,45 @@ export class PiggyClient {
|
|
|
120
121
|
} else {
|
|
121
122
|
logger.warn(`No handler for exposed function: ${name} in tab ${effectiveTabId}`);
|
|
122
123
|
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── navigate ──────────────────────────────────────────────────────────────
|
|
128
|
+
if (event.event === "navigate") {
|
|
129
|
+
const handlers = this.globalEventHandlers.get(`navigate:${event.tabId}`);
|
|
130
|
+
if (handlers) {
|
|
131
|
+
for (const h of handlers) {
|
|
132
|
+
try { h(event.url); } catch (e) { logger.error(`navigate handler error: ${e}`); }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Also fire wildcard listeners (no tabId filter)
|
|
136
|
+
const wildcard = this.globalEventHandlers.get("navigate:*");
|
|
137
|
+
if (wildcard) {
|
|
138
|
+
for (const h of wildcard) {
|
|
139
|
+
try { h({ url: event.url, tabId: event.tabId }); } catch {}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
123
143
|
}
|
|
124
144
|
}
|
|
125
145
|
|
|
146
|
+
// ── Global event subscription ─────────────────────────────────────────────
|
|
147
|
+
onEvent(eventName: string, tabId: string, handler: (data: any) => void): () => void {
|
|
148
|
+
const key = `${eventName}:${tabId}`;
|
|
149
|
+
if (!this.globalEventHandlers.has(key)) {
|
|
150
|
+
this.globalEventHandlers.set(key, new Set());
|
|
151
|
+
}
|
|
152
|
+
this.globalEventHandlers.get(key)!.add(handler);
|
|
153
|
+
// Return unsubscribe fn
|
|
154
|
+
return () => this.globalEventHandlers.get(key)?.delete(handler);
|
|
155
|
+
}
|
|
156
|
+
|
|
126
157
|
disconnect() {
|
|
127
158
|
this.socket?.destroy();
|
|
128
159
|
this.socket = null;
|
|
129
160
|
}
|
|
130
161
|
|
|
131
|
-
|
|
162
|
+
send<T = any>(cmd: string, payload: Record<string, any> = {}): Promise<T> {
|
|
132
163
|
return new Promise((resolve, reject) => {
|
|
133
164
|
if (!this.socket) return reject(new Error("Not connected"));
|
|
134
165
|
const id = String(++this.reqId);
|
|
@@ -137,118 +168,43 @@ export class PiggyClient {
|
|
|
137
168
|
});
|
|
138
169
|
}
|
|
139
170
|
|
|
140
|
-
// ── Tabs
|
|
141
|
-
|
|
142
|
-
async
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
async
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async getUrl(tabId = "default"): Promise<string> {
|
|
179
|
-
return this.send<string>("page.url", { tabId });
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async content(tabId = "default"): Promise<string> {
|
|
183
|
-
return this.send<string>("page.content", { tabId });
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// ── Eval / JS ─────────────────────────────────────────────────────────────────
|
|
187
|
-
|
|
188
|
-
async evaluate(js: string, tabId = "default"): Promise<any> {
|
|
189
|
-
return this.send("evaluate", { js, tabId });
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// ── Init Script ───────────────────────────────────────────────────────────────
|
|
193
|
-
// HERE IT IS - ADD THIS METHOD TO THE CLIENT
|
|
194
|
-
async addInitScript(js: string, tabId = "default"): Promise<void> {
|
|
195
|
-
await this.send("addInitScript", { js, tabId });
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ── Interactions ──────────────────────────────────────────────────────────────
|
|
199
|
-
|
|
200
|
-
async click(selector: string, tabId = "default"): Promise<boolean> {
|
|
201
|
-
return this.send<boolean>("click", { selector, tabId });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async doubleClick(selector: string, tabId = "default"): Promise<boolean> {
|
|
205
|
-
return this.send<boolean>("dblclick", { selector, tabId });
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async hover(selector: string, tabId = "default"): Promise<boolean> {
|
|
209
|
-
return this.send<boolean>("hover", { selector, tabId });
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async type(selector: string, text: string, tabId = "default"): Promise<boolean> {
|
|
213
|
-
return this.send<boolean>("type", { selector, text, tabId });
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
async select(selector: string, value: string, tabId = "default"): Promise<boolean> {
|
|
217
|
-
return this.send<boolean>("select", { selector, value, tabId });
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async keyPress(key: string, tabId = "default"): Promise<boolean> {
|
|
221
|
-
return this.send<boolean>("keyboard.press", { key, tabId });
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async keyCombo(combo: string, tabId = "default"): Promise<boolean> {
|
|
225
|
-
return this.send<boolean>("keyboard.combo", { combo, tabId });
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async mouseMove(x: number, y: number, tabId = "default"): Promise<boolean> {
|
|
229
|
-
return this.send<boolean>("mouse.move", { x, y, tabId });
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async mouseDrag(from: { x: number; y: number }, to: { x: number; y: number }, tabId = "default"): Promise<boolean> {
|
|
233
|
-
return this.send<boolean>("mouse.drag", { from, to, tabId });
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// ── Scroll ────────────────────────────────────────────────────────────────────
|
|
237
|
-
|
|
238
|
-
async scrollTo(selector: string, tabId = "default"): Promise<boolean> {
|
|
239
|
-
return this.send<boolean>("scroll.to", { selector, tabId });
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
async scrollBy(px: number, tabId = "default"): Promise<boolean> {
|
|
243
|
-
return this.send<boolean>("scroll.by", { px, tabId });
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// ── Fetch ─────────────────────────────────────────────────────────────────────
|
|
247
|
-
|
|
248
|
-
async fetchText(query: string, tabId = "default"): Promise<string | null> {
|
|
249
|
-
return this.send<string | null>("fetch.text", { query, tabId });
|
|
250
|
-
}
|
|
251
|
-
|
|
171
|
+
// ── Tabs ──────────────────────────────────────────────────────────────────
|
|
172
|
+
async newTab(): Promise<string> { return this.send<string>("tab.new", {}); }
|
|
173
|
+
async closeTab(tabId: string): Promise<void> { await this.send("tab.close", { tabId }); }
|
|
174
|
+
async listTabs(): Promise<string[]> { return this.send<string[]>("tab.list", {}); }
|
|
175
|
+
|
|
176
|
+
// ── Navigation ────────────────────────────────────────────────────────────
|
|
177
|
+
async navigate(url: string, tabId = "default"): Promise<void> { await this.send("navigate", { url, tabId }); }
|
|
178
|
+
async reload(tabId = "default"): Promise<void> { await this.send("reload", { tabId }); }
|
|
179
|
+
async goBack(tabId = "default"): Promise<void> { await this.send("go.back", { tabId }); }
|
|
180
|
+
async goForward(tabId = "default"): Promise<void> { await this.send("go.forward", { tabId }); }
|
|
181
|
+
|
|
182
|
+
// ── Page info ─────────────────────────────────────────────────────────────
|
|
183
|
+
async getTitle(tabId = "default"): Promise<string> { return this.send<string>("page.title", { tabId }); }
|
|
184
|
+
async getUrl(tabId = "default"): Promise<string> { return this.send<string>("page.url", { tabId }); }
|
|
185
|
+
async content(tabId = "default"): Promise<string> { return this.send<string>("page.content", { tabId }); }
|
|
186
|
+
|
|
187
|
+
// ── Eval / JS ─────────────────────────────────────────────────────────────
|
|
188
|
+
async evaluate(js: string, tabId = "default"): Promise<any> { return this.send("evaluate", { js, tabId }); }
|
|
189
|
+
async addInitScript(js: string, tabId = "default"): Promise<void> { await this.send("addInitScript", { js, tabId }); }
|
|
190
|
+
|
|
191
|
+
// ── Interactions ──────────────────────────────────────────────────────────
|
|
192
|
+
async click(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("click", { selector, tabId }); }
|
|
193
|
+
async doubleClick(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("dblclick", { selector, tabId }); }
|
|
194
|
+
async hover(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("hover", { selector, tabId }); }
|
|
195
|
+
async type(selector: string, text: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("type", { selector, text, tabId }); }
|
|
196
|
+
async select(selector: string, value: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("select", { selector, value, tabId }); }
|
|
197
|
+
async keyPress(key: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("keyboard.press", { key, tabId }); }
|
|
198
|
+
async keyCombo(combo: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("keyboard.combo", { combo, tabId }); }
|
|
199
|
+
async mouseMove(x: number, y: number, tabId = "default"): Promise<boolean> { return this.send<boolean>("mouse.move", { x, y, tabId }); }
|
|
200
|
+
async mouseDrag(from: { x: number; y: number }, to: { x: number; y: number }, tabId = "default"): Promise<boolean> { return this.send<boolean>("mouse.drag", { from, to, tabId }); }
|
|
201
|
+
|
|
202
|
+
// ── Scroll ────────────────────────────────────────────────────────────────
|
|
203
|
+
async scrollTo(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("scroll.to", { selector, tabId }); }
|
|
204
|
+
async scrollBy(px: number, tabId = "default"): Promise<boolean> { return this.send<boolean>("scroll.by", { px, tabId }); }
|
|
205
|
+
|
|
206
|
+
// ── Fetch ─────────────────────────────────────────────────────────────────
|
|
207
|
+
async fetchText(query: string, tabId = "default"): Promise<string | null> { return this.send<string | null>("fetch.text", { query, tabId }); }
|
|
252
208
|
async fetchLinks(query: string, tabId = "default"): Promise<string[]> {
|
|
253
209
|
if (query === "a" || query === "body") {
|
|
254
210
|
const result = await this.send<string[]>("fetch.links.all", { tabId });
|
|
@@ -257,148 +213,68 @@ export class PiggyClient {
|
|
|
257
213
|
const result = await this.send<string[]>("fetch.links", { query, tabId });
|
|
258
214
|
return Array.isArray(result) ? result : [];
|
|
259
215
|
}
|
|
260
|
-
|
|
261
216
|
async fetchImages(query: string, tabId = "default"): Promise<string[]> {
|
|
262
217
|
const result = await this.send<string[]>("fetch.image", { query, tabId });
|
|
263
218
|
return Array.isArray(result) ? result : [];
|
|
264
219
|
}
|
|
265
220
|
|
|
266
|
-
// ── Search
|
|
267
|
-
|
|
268
|
-
async
|
|
269
|
-
return this.send("search.css", { query, tabId });
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
async searchId(query: string, tabId = "default"): Promise<any> {
|
|
273
|
-
return this.send("search.id", { query, tabId });
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// ── Wait ──────────────────────────────────────────────────────────────────────
|
|
277
|
-
|
|
278
|
-
async waitForSelector(selector: string, timeout = 30000, tabId = "default"): Promise<void> {
|
|
279
|
-
await this.send("wait.selector", { selector, timeout, tabId });
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async waitForNavigation(tabId = "default"): Promise<void> {
|
|
283
|
-
await this.send("wait.navigation", { tabId });
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
async waitForResponse(urlPattern: string, timeout = 30000, tabId = "default"): Promise<void> {
|
|
287
|
-
await this.send("wait.response", { url: urlPattern, timeout, tabId });
|
|
288
|
-
}
|
|
221
|
+
// ── Search ────────────────────────────────────────────────────────────────
|
|
222
|
+
async searchCss(query: string, tabId = "default"): Promise<any> { return this.send("search.css", { query, tabId }); }
|
|
223
|
+
async searchId(query: string, tabId = "default"): Promise<any> { return this.send("search.id", { query, tabId }); }
|
|
289
224
|
|
|
290
|
-
// ──
|
|
225
|
+
// ── Wait ──────────────────────────────────────────────────────────────────
|
|
226
|
+
async waitForSelector(selector: string, timeout = 30000, tabId = "default"): Promise<void> { await this.send("wait.selector", { selector, timeout, tabId }); }
|
|
227
|
+
async waitForNavigation(tabId = "default"): Promise<void> { await this.send("wait.navigation", { tabId }); }
|
|
228
|
+
async waitForResponse(urlPattern: string, timeout = 30000, tabId = "default"): Promise<void> { await this.send("wait.response", { url: urlPattern, timeout, tabId }); }
|
|
291
229
|
|
|
230
|
+
// ── Screenshot / PDF ──────────────────────────────────────────────────────
|
|
292
231
|
async screenshot(filePath?: string, tabId = "default"): Promise<string> {
|
|
293
232
|
const b64 = await this.send<string>("screenshot", { tabId });
|
|
294
|
-
if (filePath) {
|
|
295
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
296
|
-
writeFileSync(filePath, Buffer.from(b64, "base64"));
|
|
297
|
-
}
|
|
233
|
+
if (filePath) { mkdirSync(dirname(filePath), { recursive: true }); writeFileSync(filePath, Buffer.from(b64, "base64")); }
|
|
298
234
|
return filePath ?? b64;
|
|
299
235
|
}
|
|
300
|
-
|
|
301
|
-
// ── PDF ───────────────────────────────────────────────────────────────────────
|
|
302
|
-
|
|
303
236
|
async pdf(filePath?: string, tabId = "default"): Promise<string> {
|
|
304
237
|
const b64 = await this.send<string>("pdf", { tabId });
|
|
305
|
-
if (filePath) {
|
|
306
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
307
|
-
writeFileSync(filePath, Buffer.from(b64, "base64"));
|
|
308
|
-
}
|
|
238
|
+
if (filePath) { mkdirSync(dirname(filePath), { recursive: true }); writeFileSync(filePath, Buffer.from(b64, "base64")); }
|
|
309
239
|
return filePath ?? b64;
|
|
310
240
|
}
|
|
311
241
|
|
|
312
|
-
// ── Image blocking
|
|
313
|
-
|
|
314
|
-
async
|
|
315
|
-
await this.send("intercept.block.images", { tabId });
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async unblockImages(tabId = "default"): Promise<void> {
|
|
319
|
-
await this.send("intercept.unblock.images", { tabId });
|
|
320
|
-
}
|
|
242
|
+
// ── Image blocking ────────────────────────────────────────────────────────
|
|
243
|
+
async blockImages(tabId = "default"): Promise<void> { await this.send("intercept.block.images", { tabId }); }
|
|
244
|
+
async unblockImages(tabId = "default"): Promise<void> { await this.send("intercept.unblock.images", { tabId }); }
|
|
321
245
|
|
|
322
|
-
// ── Cookies
|
|
323
|
-
|
|
324
|
-
async
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
async getCookie(name: string, tabId = "default"): Promise<any> {
|
|
329
|
-
return this.send("cookie.get", { name, tabId });
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async deleteCookie(name: string, tabId = "default"): Promise<void> {
|
|
333
|
-
await this.send("cookie.delete", { name, tabId });
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
async listCookies(tabId = "default"): Promise<any[]> {
|
|
337
|
-
return this.send<any[]>("cookie.list", { tabId });
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ── Interception ──────────────────────────────────────────────────────────────
|
|
246
|
+
// ── Cookies ───────────────────────────────────────────────────────────────
|
|
247
|
+
async setCookie(name: string, value: string, domain: string, path = "/", tabId = "default"): Promise<void> { await this.send("cookie.set", { name, value, domain, path, tabId }); }
|
|
248
|
+
async getCookie(name: string, tabId = "default"): Promise<any> { return this.send("cookie.get", { name, tabId }); }
|
|
249
|
+
async deleteCookie(name: string, tabId = "default"): Promise<void> { await this.send("cookie.delete", { name, tabId }); }
|
|
250
|
+
async listCookies(tabId = "default"): Promise<any[]> { return this.send<any[]>("cookie.list", { tabId }); }
|
|
341
251
|
|
|
252
|
+
// ── Interception ──────────────────────────────────────────────────────────
|
|
342
253
|
async addInterceptRule(action: "block" | "redirect" | "modifyHeaders", pattern: string, options: { redirectUrl?: string; headers?: Record<string, string> } = {}, tabId = "default"): Promise<void> {
|
|
343
254
|
await this.send("intercept.rule.add", { action, pattern, ...options, tabId });
|
|
344
255
|
}
|
|
256
|
+
async clearInterceptRules(tabId = "default"): Promise<void> { await this.send("intercept.rule.clear", { tabId }); }
|
|
345
257
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
async
|
|
353
|
-
|
|
354
|
-
}
|
|
258
|
+
// ── Network capture ───────────────────────────────────────────────────────
|
|
259
|
+
async captureStart(tabId = "default"): Promise<void> { await this.send("capture.start", { tabId }); }
|
|
260
|
+
async captureStop(tabId = "default"): Promise<void> { await this.send("capture.stop", { tabId }); }
|
|
261
|
+
async captureRequests(tabId = "default"): Promise<any[]> { return this.send<any[]>("capture.requests", { tabId }); }
|
|
262
|
+
async captureWs(tabId = "default"): Promise<any[]> { return this.send<any[]>("capture.ws", { tabId }); }
|
|
263
|
+
async captureCookies(tabId = "default"): Promise<any[]> { return this.send<any[]>("capture.cookies", { tabId }); }
|
|
264
|
+
async captureStorage(tabId = "default"): Promise<any> { return this.send("capture.storage", { tabId }); }
|
|
265
|
+
async captureClear(tabId = "default"): Promise<void> { await this.send("capture.clear", { tabId }); }
|
|
355
266
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
async captureRequests(tabId = "default"): Promise<any[]> {
|
|
361
|
-
return this.send<any[]>("capture.requests", { tabId });
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async captureWs(tabId = "default"): Promise<any[]> {
|
|
365
|
-
return this.send<any[]>("capture.ws", { tabId });
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
async captureCookies(tabId = "default"): Promise<any[]> {
|
|
369
|
-
return this.send<any[]>("capture.cookies", { tabId });
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
async captureStorage(tabId = "default"): Promise<any> {
|
|
373
|
-
return this.send("capture.storage", { tabId });
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
async captureClear(tabId = "default"): Promise<void> {
|
|
377
|
-
await this.send("capture.clear", { tabId });
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// ── Session ───────────────────────────────────────────────────────────────────
|
|
381
|
-
|
|
382
|
-
async sessionExport(tabId = "default"): Promise<any> {
|
|
383
|
-
return this.send("session.export", { tabId });
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
async sessionImport(data: any, tabId = "default"): Promise<void> {
|
|
387
|
-
await this.send("session.import", { data, tabId });
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// ── Expose Function ───────────────────────────────────────────────────────────
|
|
267
|
+
// ── Session ───────────────────────────────────────────────────────────────
|
|
268
|
+
async sessionExport(tabId = "default"): Promise<any> { return this.send("session.export", { tabId }); }
|
|
269
|
+
async sessionImport(data: any, tabId = "default"): Promise<void> { await this.send("session.import", { data, tabId }); }
|
|
391
270
|
|
|
271
|
+
// ── Expose Function ───────────────────────────────────────────────────────
|
|
392
272
|
async exposeFunction(name: string, handler: (data: any) => Promise<any> | any, tabId = "default"): Promise<void> {
|
|
393
|
-
if (!this.eventHandlers.has(tabId))
|
|
394
|
-
this.eventHandlers.set(tabId, new Map());
|
|
395
|
-
}
|
|
273
|
+
if (!this.eventHandlers.has(tabId)) this.eventHandlers.set(tabId, new Map());
|
|
396
274
|
this.eventHandlers.get(tabId)!.set(name, async (data: any) => {
|
|
397
275
|
try {
|
|
398
276
|
const result = await handler(data);
|
|
399
|
-
if (result && typeof result === "object" && ("success" in result || "error" in result))
|
|
400
|
-
return result;
|
|
401
|
-
}
|
|
277
|
+
if (result && typeof result === "object" && ("success" in result || "error" in result)) return result;
|
|
402
278
|
return { success: true, result };
|
|
403
279
|
} catch (err: any) {
|
|
404
280
|
return { success: false, error: err.message || String(err) };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// piggy/intercept/scripts.ts
|
|
2
|
+
// JS injection helpers for intercept.respond and intercept.modifyResponse.
|
|
3
|
+
// Both work purely in the browser's JS layer — no C++ changes needed.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generates a script that short-circuits matching fetch/XHR requests
|
|
7
|
+
* and returns a static fake response — the request never hits the network.
|
|
8
|
+
*/
|
|
9
|
+
export function buildRespondScript(
|
|
10
|
+
pattern: string,
|
|
11
|
+
status: number,
|
|
12
|
+
contentType: string,
|
|
13
|
+
body: string
|
|
14
|
+
): string {
|
|
15
|
+
const safePattern = pattern.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
16
|
+
const safeBody = body.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
17
|
+
const safeContentType = contentType.replace(/'/g, "\\'");
|
|
18
|
+
|
|
19
|
+
return `
|
|
20
|
+
(function() {
|
|
21
|
+
'use strict';
|
|
22
|
+
if (!window.__PIGGY_RESPOND_RULES__) window.__PIGGY_RESPOND_RULES__ = [];
|
|
23
|
+
window.__PIGGY_RESPOND_RULES__.push({
|
|
24
|
+
pattern: '${safePattern}',
|
|
25
|
+
status: ${status},
|
|
26
|
+
contentType: '${safeContentType}',
|
|
27
|
+
body: \`${safeBody}\`
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function _piggyMatchUrl(url, pattern) {
|
|
31
|
+
try { return url.includes(pattern) || new RegExp(pattern).test(url); }
|
|
32
|
+
catch { return url.includes(pattern); }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Only install wrappers once per page
|
|
36
|
+
if (window.__PIGGY_RESPOND_INSTALLED__) return;
|
|
37
|
+
window.__PIGGY_RESPOND_INSTALLED__ = true;
|
|
38
|
+
|
|
39
|
+
// ── fetch wrapper ──────────────────────────────────────────────────────────
|
|
40
|
+
const _origFetch = window.fetch;
|
|
41
|
+
window.fetch = function(input, init) {
|
|
42
|
+
const url = typeof input === 'string' ? input : (input?.url ?? String(input));
|
|
43
|
+
const rules = window.__PIGGY_RESPOND_RULES__ || [];
|
|
44
|
+
for (const rule of rules) {
|
|
45
|
+
if (_piggyMatchUrl(url, rule.pattern)) {
|
|
46
|
+
return Promise.resolve(new Response(rule.body, {
|
|
47
|
+
status: rule.status,
|
|
48
|
+
headers: { 'Content-Type': rule.contentType }
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return _origFetch.apply(this, arguments);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ── XHR wrapper ────────────────────────────────────────────────────────────
|
|
56
|
+
const _origOpen = XMLHttpRequest.prototype.open;
|
|
57
|
+
const _origSend = XMLHttpRequest.prototype.send;
|
|
58
|
+
|
|
59
|
+
XMLHttpRequest.prototype.open = function(method, url) {
|
|
60
|
+
this.__piggy_url__ = String(url);
|
|
61
|
+
return _origOpen.apply(this, arguments);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
XMLHttpRequest.prototype.send = function() {
|
|
65
|
+
const url = this.__piggy_url__ || '';
|
|
66
|
+
const rules = window.__PIGGY_RESPOND_RULES__ || [];
|
|
67
|
+
for (const rule of rules) {
|
|
68
|
+
if (_piggyMatchUrl(url, rule.pattern)) {
|
|
69
|
+
const self = this;
|
|
70
|
+
Object.defineProperty(self, 'readyState', { get: () => 4, configurable: true });
|
|
71
|
+
Object.defineProperty(self, 'status', { get: () => rule.status, configurable: true });
|
|
72
|
+
Object.defineProperty(self, 'responseText', { get: () => rule.body, configurable: true });
|
|
73
|
+
Object.defineProperty(self, 'response', { get: () => rule.body, configurable: true });
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
if (typeof self.onreadystatechange === 'function') self.onreadystatechange();
|
|
76
|
+
self.dispatchEvent(new Event('readystatechange'));
|
|
77
|
+
self.dispatchEvent(new Event('load'));
|
|
78
|
+
self.dispatchEvent(new Event('loadend'));
|
|
79
|
+
}, 0);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return _origSend.apply(this, arguments);
|
|
84
|
+
};
|
|
85
|
+
})();
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Generates a script that lets the request hit the network, then calls
|
|
91
|
+
* an exposed function with { body, status, headers }.
|
|
92
|
+
* The exposed function returns { body?, status?, headers? } modifications
|
|
93
|
+
* or an empty object {} to pass through unchanged.
|
|
94
|
+
*/
|
|
95
|
+
export function buildModifyResponseScript(pattern: string, exposedFnName: string): string {
|
|
96
|
+
const safePattern = pattern.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
97
|
+
const safeFnName = exposedFnName.replace(/'/g, "\\'");
|
|
98
|
+
|
|
99
|
+
return `
|
|
100
|
+
(function() {
|
|
101
|
+
'use strict';
|
|
102
|
+
if (!window.__PIGGY_MODIFY_RULES__) window.__PIGGY_MODIFY_RULES__ = [];
|
|
103
|
+
window.__PIGGY_MODIFY_RULES__.push({ pattern: '${safePattern}', fn: '${safeFnName}' });
|
|
104
|
+
|
|
105
|
+
function _piggyMatchUrl(url, pattern) {
|
|
106
|
+
try { return url.includes(pattern) || new RegExp(pattern).test(url); }
|
|
107
|
+
catch { return url.includes(pattern); }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Only install wrappers once per page
|
|
111
|
+
if (window.__PIGGY_MODIFY_INSTALLED__) return;
|
|
112
|
+
window.__PIGGY_MODIFY_INSTALLED__ = true;
|
|
113
|
+
|
|
114
|
+
const _origFetch = window.fetch;
|
|
115
|
+
window.fetch = async function(input, init) {
|
|
116
|
+
const url = typeof input === 'string' ? input : (input?.url ?? String(input));
|
|
117
|
+
const rules = window.__PIGGY_MODIFY_RULES__ || [];
|
|
118
|
+
|
|
119
|
+
let matchedFn = null;
|
|
120
|
+
for (const rule of rules) {
|
|
121
|
+
if (_piggyMatchUrl(url, rule.pattern)) { matchedFn = rule.fn; break; }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// No match — pass through untouched
|
|
125
|
+
const resp = await _origFetch.apply(this, arguments);
|
|
126
|
+
if (!matchedFn) return resp;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const bodyText = await resp.clone().text();
|
|
130
|
+
const headers = {};
|
|
131
|
+
resp.headers.forEach((v, k) => { headers[k] = v; });
|
|
132
|
+
|
|
133
|
+
const handlerFn = window[matchedFn];
|
|
134
|
+
if (typeof handlerFn !== 'function') return resp;
|
|
135
|
+
|
|
136
|
+
// Call Node.js handler via exposeFunction bridge
|
|
137
|
+
const mod = await handlerFn({ body: bodyText, status: resp.status, headers });
|
|
138
|
+
if (!mod || typeof mod !== 'object' || Object.keys(mod).length === 0) return resp;
|
|
139
|
+
|
|
140
|
+
return new Response(
|
|
141
|
+
mod.body !== undefined ? mod.body : bodyText,
|
|
142
|
+
{
|
|
143
|
+
status: mod.status !== undefined ? mod.status : resp.status,
|
|
144
|
+
headers: mod.headers !== undefined ? mod.headers : headers,
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
} catch {
|
|
148
|
+
return resp; // On any error, pass through original response
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
})();
|
|
152
|
+
`;
|
|
153
|
+
}
|