nothing-browser 0.0.10 → 0.0.11
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 +39 -19
- package/dist/piggy.js +281 -19
- package/dist/register/index.js +242 -0
- package/package.json +1 -1
- package/piggy/client/index.ts +117 -247
- 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
|
|
|
@@ -18,6 +18,9 @@ export class PiggyClient {
|
|
|
18
18
|
private eventBuffer = "";
|
|
19
19
|
private eventHandlers = new Map<string, Map<string, (data: any) => Promise<any>>>();
|
|
20
20
|
|
|
21
|
+
// ── Global event emitter for navigate / etc ───────────────────────────────
|
|
22
|
+
private globalEventHandlers = new Map<string, Set<(data: any) => void>>();
|
|
23
|
+
|
|
21
24
|
constructor(socketPath = SOCKET_PATH) {
|
|
22
25
|
this.socketPath = socketPath;
|
|
23
26
|
this.eventHandlers.set("default", new Map());
|
|
@@ -39,17 +42,17 @@ export class PiggyClient {
|
|
|
39
42
|
this.eventBuffer += chunk;
|
|
40
43
|
const lines = this.eventBuffer.split("\n");
|
|
41
44
|
this.eventBuffer = lines.pop()!;
|
|
42
|
-
|
|
45
|
+
|
|
43
46
|
for (const line of lines) {
|
|
44
47
|
if (!line.trim()) continue;
|
|
45
48
|
try {
|
|
46
49
|
const msg = JSON.parse(line);
|
|
47
|
-
|
|
50
|
+
|
|
48
51
|
if (msg.type === "event") {
|
|
49
52
|
this.handleEvent(msg);
|
|
50
53
|
continue;
|
|
51
54
|
}
|
|
52
|
-
|
|
55
|
+
|
|
53
56
|
const p = this.pending.get(msg.id);
|
|
54
57
|
if (p) {
|
|
55
58
|
this.pending.delete(msg.id);
|
|
@@ -75,31 +78,23 @@ export class PiggyClient {
|
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
private handleEvent(event: any) {
|
|
81
|
+
// ── exposed_call ──────────────────────────────────────────────────────────
|
|
78
82
|
if (event.event === "exposed_call") {
|
|
79
83
|
const { tabId, name, callId, data } = event;
|
|
80
84
|
const effectiveTabId = tabId || "default";
|
|
81
85
|
const handlers = this.eventHandlers.get(effectiveTabId);
|
|
82
86
|
const handler = handlers?.get(name);
|
|
83
|
-
|
|
87
|
+
|
|
84
88
|
if (handler) {
|
|
85
89
|
Promise.resolve(handler(JSON.parse(data || "null")))
|
|
86
90
|
.then(response => {
|
|
87
91
|
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
|
-
}
|
|
92
|
+
this.send("exposed.result", {
|
|
93
|
+
tabId: effectiveTabId,
|
|
94
|
+
callId,
|
|
95
|
+
result: response.success ? JSON.stringify(response.result) : (response.error || "Unknown error"),
|
|
96
|
+
isError: !response.success
|
|
97
|
+
}).catch(e => logger.error(`Failed to send exposed result: ${e}`));
|
|
103
98
|
} else {
|
|
104
99
|
this.send("exposed.result", {
|
|
105
100
|
tabId: effectiveTabId,
|
|
@@ -120,7 +115,37 @@ export class PiggyClient {
|
|
|
120
115
|
} else {
|
|
121
116
|
logger.warn(`No handler for exposed function: ${name} in tab ${effectiveTabId}`);
|
|
122
117
|
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── navigate ──────────────────────────────────────────────────────────────
|
|
122
|
+
if (event.event === "navigate") {
|
|
123
|
+
const handlers = this.globalEventHandlers.get(`navigate:${event.tabId}`);
|
|
124
|
+
if (handlers) {
|
|
125
|
+
for (const h of handlers) {
|
|
126
|
+
try { h(event.url); } catch (e) { logger.error(`navigate handler error: ${e}`); }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Also fire wildcard listeners (no tabId filter)
|
|
130
|
+
const wildcard = this.globalEventHandlers.get("navigate:*");
|
|
131
|
+
if (wildcard) {
|
|
132
|
+
for (const h of wildcard) {
|
|
133
|
+
try { h({ url: event.url, tabId: event.tabId }); } catch {}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Global event subscription ─────────────────────────────────────────────
|
|
141
|
+
onEvent(eventName: string, tabId: string, handler: (data: any) => void): () => void {
|
|
142
|
+
const key = `${eventName}:${tabId}`;
|
|
143
|
+
if (!this.globalEventHandlers.has(key)) {
|
|
144
|
+
this.globalEventHandlers.set(key, new Set());
|
|
123
145
|
}
|
|
146
|
+
this.globalEventHandlers.get(key)!.add(handler);
|
|
147
|
+
// Return unsubscribe fn
|
|
148
|
+
return () => this.globalEventHandlers.get(key)?.delete(handler);
|
|
124
149
|
}
|
|
125
150
|
|
|
126
151
|
disconnect() {
|
|
@@ -128,7 +153,7 @@ export class PiggyClient {
|
|
|
128
153
|
this.socket = null;
|
|
129
154
|
}
|
|
130
155
|
|
|
131
|
-
|
|
156
|
+
send<T = any>(cmd: string, payload: Record<string, any> = {}): Promise<T> {
|
|
132
157
|
return new Promise((resolve, reject) => {
|
|
133
158
|
if (!this.socket) return reject(new Error("Not connected"));
|
|
134
159
|
const id = String(++this.reqId);
|
|
@@ -137,118 +162,43 @@ export class PiggyClient {
|
|
|
137
162
|
});
|
|
138
163
|
}
|
|
139
164
|
|
|
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
|
-
|
|
165
|
+
// ── Tabs ──────────────────────────────────────────────────────────────────
|
|
166
|
+
async newTab(): Promise<string> { return this.send<string>("tab.new", {}); }
|
|
167
|
+
async closeTab(tabId: string): Promise<void> { await this.send("tab.close", { tabId }); }
|
|
168
|
+
async listTabs(): Promise<string[]> { return this.send<string[]>("tab.list", {}); }
|
|
169
|
+
|
|
170
|
+
// ── Navigation ────────────────────────────────────────────────────────────
|
|
171
|
+
async navigate(url: string, tabId = "default"): Promise<void> { await this.send("navigate", { url, tabId }); }
|
|
172
|
+
async reload(tabId = "default"): Promise<void> { await this.send("reload", { tabId }); }
|
|
173
|
+
async goBack(tabId = "default"): Promise<void> { await this.send("go.back", { tabId }); }
|
|
174
|
+
async goForward(tabId = "default"): Promise<void> { await this.send("go.forward", { tabId }); }
|
|
175
|
+
|
|
176
|
+
// ── Page info ─────────────────────────────────────────────────────────────
|
|
177
|
+
async getTitle(tabId = "default"): Promise<string> { return this.send<string>("page.title", { tabId }); }
|
|
178
|
+
async getUrl(tabId = "default"): Promise<string> { return this.send<string>("page.url", { tabId }); }
|
|
179
|
+
async content(tabId = "default"): Promise<string> { return this.send<string>("page.content", { tabId }); }
|
|
180
|
+
|
|
181
|
+
// ── Eval / JS ─────────────────────────────────────────────────────────────
|
|
182
|
+
async evaluate(js: string, tabId = "default"): Promise<any> { return this.send("evaluate", { js, tabId }); }
|
|
183
|
+
async addInitScript(js: string, tabId = "default"): Promise<void> { await this.send("addInitScript", { js, tabId }); }
|
|
184
|
+
|
|
185
|
+
// ── Interactions ──────────────────────────────────────────────────────────
|
|
186
|
+
async click(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("click", { selector, tabId }); }
|
|
187
|
+
async doubleClick(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("dblclick", { selector, tabId }); }
|
|
188
|
+
async hover(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("hover", { selector, tabId }); }
|
|
189
|
+
async type(selector: string, text: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("type", { selector, text, tabId }); }
|
|
190
|
+
async select(selector: string, value: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("select", { selector, value, tabId }); }
|
|
191
|
+
async keyPress(key: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("keyboard.press", { key, tabId }); }
|
|
192
|
+
async keyCombo(combo: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("keyboard.combo", { combo, tabId }); }
|
|
193
|
+
async mouseMove(x: number, y: number, tabId = "default"): Promise<boolean> { return this.send<boolean>("mouse.move", { x, y, tabId }); }
|
|
194
|
+
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 }); }
|
|
195
|
+
|
|
196
|
+
// ── Scroll ────────────────────────────────────────────────────────────────
|
|
197
|
+
async scrollTo(selector: string, tabId = "default"): Promise<boolean> { return this.send<boolean>("scroll.to", { selector, tabId }); }
|
|
198
|
+
async scrollBy(px: number, tabId = "default"): Promise<boolean> { return this.send<boolean>("scroll.by", { px, tabId }); }
|
|
199
|
+
|
|
200
|
+
// ── Fetch ─────────────────────────────────────────────────────────────────
|
|
201
|
+
async fetchText(query: string, tabId = "default"): Promise<string | null> { return this.send<string | null>("fetch.text", { query, tabId }); }
|
|
252
202
|
async fetchLinks(query: string, tabId = "default"): Promise<string[]> {
|
|
253
203
|
if (query === "a" || query === "body") {
|
|
254
204
|
const result = await this.send<string[]>("fetch.links.all", { tabId });
|
|
@@ -257,148 +207,68 @@ export class PiggyClient {
|
|
|
257
207
|
const result = await this.send<string[]>("fetch.links", { query, tabId });
|
|
258
208
|
return Array.isArray(result) ? result : [];
|
|
259
209
|
}
|
|
260
|
-
|
|
261
210
|
async fetchImages(query: string, tabId = "default"): Promise<string[]> {
|
|
262
211
|
const result = await this.send<string[]>("fetch.image", { query, tabId });
|
|
263
212
|
return Array.isArray(result) ? result : [];
|
|
264
213
|
}
|
|
265
214
|
|
|
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
|
-
}
|
|
215
|
+
// ── Search ────────────────────────────────────────────────────────────────
|
|
216
|
+
async searchCss(query: string, tabId = "default"): Promise<any> { return this.send("search.css", { query, tabId }); }
|
|
217
|
+
async searchId(query: string, tabId = "default"): Promise<any> { return this.send("search.id", { query, tabId }); }
|
|
275
218
|
|
|
276
|
-
// ── Wait
|
|
277
|
-
|
|
278
|
-
async
|
|
279
|
-
|
|
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
|
-
}
|
|
289
|
-
|
|
290
|
-
// ── Screenshot ────────────────────────────────────────────────────────────────
|
|
219
|
+
// ── Wait ──────────────────────────────────────────────────────────────────
|
|
220
|
+
async waitForSelector(selector: string, timeout = 30000, tabId = "default"): Promise<void> { await this.send("wait.selector", { selector, timeout, tabId }); }
|
|
221
|
+
async waitForNavigation(tabId = "default"): Promise<void> { await this.send("wait.navigation", { tabId }); }
|
|
222
|
+
async waitForResponse(urlPattern: string, timeout = 30000, tabId = "default"): Promise<void> { await this.send("wait.response", { url: urlPattern, timeout, tabId }); }
|
|
291
223
|
|
|
224
|
+
// ── Screenshot / PDF ──────────────────────────────────────────────────────
|
|
292
225
|
async screenshot(filePath?: string, tabId = "default"): Promise<string> {
|
|
293
226
|
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
|
-
}
|
|
227
|
+
if (filePath) { mkdirSync(dirname(filePath), { recursive: true }); writeFileSync(filePath, Buffer.from(b64, "base64")); }
|
|
298
228
|
return filePath ?? b64;
|
|
299
229
|
}
|
|
300
|
-
|
|
301
|
-
// ── PDF ───────────────────────────────────────────────────────────────────────
|
|
302
|
-
|
|
303
230
|
async pdf(filePath?: string, tabId = "default"): Promise<string> {
|
|
304
231
|
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
|
-
}
|
|
232
|
+
if (filePath) { mkdirSync(dirname(filePath), { recursive: true }); writeFileSync(filePath, Buffer.from(b64, "base64")); }
|
|
309
233
|
return filePath ?? b64;
|
|
310
234
|
}
|
|
311
235
|
|
|
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
|
-
}
|
|
321
|
-
|
|
322
|
-
// ── Cookies ───────────────────────────────────────────────────────────────────
|
|
236
|
+
// ── Image blocking ────────────────────────────────────────────────────────
|
|
237
|
+
async blockImages(tabId = "default"): Promise<void> { await this.send("intercept.block.images", { tabId }); }
|
|
238
|
+
async unblockImages(tabId = "default"): Promise<void> { await this.send("intercept.unblock.images", { tabId }); }
|
|
323
239
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
async
|
|
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 ──────────────────────────────────────────────────────────────
|
|
240
|
+
// ── Cookies ───────────────────────────────────────────────────────────────
|
|
241
|
+
async setCookie(name: string, value: string, domain: string, path = "/", tabId = "default"): Promise<void> { await this.send("cookie.set", { name, value, domain, path, tabId }); }
|
|
242
|
+
async getCookie(name: string, tabId = "default"): Promise<any> { return this.send("cookie.get", { name, tabId }); }
|
|
243
|
+
async deleteCookie(name: string, tabId = "default"): Promise<void> { await this.send("cookie.delete", { name, tabId }); }
|
|
244
|
+
async listCookies(tabId = "default"): Promise<any[]> { return this.send<any[]>("cookie.list", { tabId }); }
|
|
341
245
|
|
|
246
|
+
// ── Interception ──────────────────────────────────────────────────────────
|
|
342
247
|
async addInterceptRule(action: "block" | "redirect" | "modifyHeaders", pattern: string, options: { redirectUrl?: string; headers?: Record<string, string> } = {}, tabId = "default"): Promise<void> {
|
|
343
248
|
await this.send("intercept.rule.add", { action, pattern, ...options, tabId });
|
|
344
249
|
}
|
|
250
|
+
async clearInterceptRules(tabId = "default"): Promise<void> { await this.send("intercept.rule.clear", { tabId }); }
|
|
345
251
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
async
|
|
353
|
-
|
|
354
|
-
}
|
|
252
|
+
// ── Network capture ───────────────────────────────────────────────────────
|
|
253
|
+
async captureStart(tabId = "default"): Promise<void> { await this.send("capture.start", { tabId }); }
|
|
254
|
+
async captureStop(tabId = "default"): Promise<void> { await this.send("capture.stop", { tabId }); }
|
|
255
|
+
async captureRequests(tabId = "default"): Promise<any[]> { return this.send<any[]>("capture.requests", { tabId }); }
|
|
256
|
+
async captureWs(tabId = "default"): Promise<any[]> { return this.send<any[]>("capture.ws", { tabId }); }
|
|
257
|
+
async captureCookies(tabId = "default"): Promise<any[]> { return this.send<any[]>("capture.cookies", { tabId }); }
|
|
258
|
+
async captureStorage(tabId = "default"): Promise<any> { return this.send("capture.storage", { tabId }); }
|
|
259
|
+
async captureClear(tabId = "default"): Promise<void> { await this.send("capture.clear", { tabId }); }
|
|
355
260
|
|
|
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 ───────────────────────────────────────────────────────────
|
|
261
|
+
// ── Session ───────────────────────────────────────────────────────────────
|
|
262
|
+
async sessionExport(tabId = "default"): Promise<any> { return this.send("session.export", { tabId }); }
|
|
263
|
+
async sessionImport(data: any, tabId = "default"): Promise<void> { await this.send("session.import", { data, tabId }); }
|
|
391
264
|
|
|
265
|
+
// ── Expose Function ───────────────────────────────────────────────────────
|
|
392
266
|
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
|
-
}
|
|
267
|
+
if (!this.eventHandlers.has(tabId)) this.eventHandlers.set(tabId, new Map());
|
|
396
268
|
this.eventHandlers.get(tabId)!.set(name, async (data: any) => {
|
|
397
269
|
try {
|
|
398
270
|
const result = await handler(data);
|
|
399
|
-
if (result && typeof result === "object" && ("success" in result || "error" in result))
|
|
400
|
-
return result;
|
|
401
|
-
}
|
|
271
|
+
if (result && typeof result === "object" && ("success" in result || "error" in result)) return result;
|
|
402
272
|
return { success: true, result };
|
|
403
273
|
} catch (err: any) {
|
|
404
274
|
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
|
+
}
|