nothing-browser 0.0.16 → 0.0.18
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 +224 -40
- package/dist/piggy/client/index.d.ts +77 -2
- package/dist/piggy/client/index.d.ts.map +1 -1
- package/dist/piggy/pool/index.d.ts +23 -0
- package/dist/piggy/pool/index.d.ts.map +1 -0
- package/dist/piggy/register/index.d.ts +3 -1
- package/dist/piggy/register/index.d.ts.map +1 -1
- package/dist/piggy/server/index.d.ts +22 -1
- package/dist/piggy/server/index.d.ts.map +1 -1
- package/dist/piggy/store/index.d.ts +22 -0
- package/dist/piggy/store/index.d.ts.map +1 -0
- package/dist/piggy.d.ts +6 -174
- package/dist/piggy.d.ts.map +1 -1
- package/dist/piggy.js +7736 -277
- package/dist/register/index.js +6291 -205
- package/dist/server/index.js +6252 -79
- package/package.json +3 -1
- package/piggy/client/index.ts +325 -54
- package/piggy/pool/index.d.ts +12 -0
- package/piggy/pool/index.ts +75 -0
- package/piggy/register/index.ts +231 -214
- package/piggy/server/index.d.ts +51 -14
- package/piggy/server/index.ts +68 -15
- package/piggy/store/index.d.ts +26 -0
- package/piggy/store/index.ts +230 -0
- package/piggy.ts +118 -320
package/piggy/register/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// piggy/register/index.ts
|
|
2
2
|
import { PiggyClient } from "../client";
|
|
3
3
|
import logger from "../logger";
|
|
4
|
-
import { routeRegistry, keepAliveSites, type RouteHandler, type BeforeMiddleware } from "../server";
|
|
4
|
+
import { routeRegistry, keepAliveSites, type RouteHandler, type BeforeMiddleware, type RouteDetail } from "../server";
|
|
5
5
|
import { randomDelay, humanTypeSequence } from "../human";
|
|
6
6
|
import { buildRespondScript, buildModifyResponseScript } from "../intercept/scripts";
|
|
7
|
+
import { storeRecord } from "../store";
|
|
8
|
+
import { TabPool } from "../pool";
|
|
7
9
|
|
|
8
10
|
let globalClient: PiggyClient | null = null;
|
|
9
11
|
export let humanMode = false;
|
|
@@ -25,13 +27,24 @@ async function retry<T>(label: string, fn: () => Promise<T>, retries = 2, backof
|
|
|
25
27
|
throw last;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
export function createSiteObject(
|
|
30
|
+
export function createSiteObject(
|
|
31
|
+
name: string,
|
|
32
|
+
registeredUrl: string,
|
|
33
|
+
client: PiggyClient,
|
|
34
|
+
tabId: string,
|
|
35
|
+
pool?: TabPool
|
|
36
|
+
) {
|
|
29
37
|
let _currentUrl: string = registeredUrl;
|
|
38
|
+
let _modifyRuleCounter = 0;
|
|
39
|
+
|
|
40
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
41
|
+
// If pool exists, run fn with a pool tab. Otherwise use the fixed tabId.
|
|
42
|
+
function withTab<T>(fn: (t: string) => Promise<T>): Promise<T> {
|
|
43
|
+
return pool ? pool.withTab(fn) : fn(tabId);
|
|
44
|
+
}
|
|
30
45
|
|
|
31
|
-
// ── Event listeners store ──────────────────────────────────────────────────
|
|
32
46
|
const _eventListeners = new Map<string, Set<(data: any) => void>>();
|
|
33
47
|
|
|
34
|
-
// Wire the client-level navigate event into site-level listeners
|
|
35
48
|
const _unsubNavigate = client.onEvent("navigate", tabId, (url: string) => {
|
|
36
49
|
_currentUrl = url;
|
|
37
50
|
const handlers = _eventListeners.get("navigate");
|
|
@@ -51,59 +64,66 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
|
|
|
51
64
|
}
|
|
52
65
|
};
|
|
53
66
|
|
|
54
|
-
// ── Intercept helper: unique fn name per pattern ───────────────────────────
|
|
55
|
-
let _modifyRuleCounter = 0;
|
|
56
|
-
|
|
57
67
|
const site: any = {
|
|
58
68
|
_name: name,
|
|
59
69
|
_tabId: tabId,
|
|
70
|
+
_pool: pool ?? null,
|
|
71
|
+
|
|
72
|
+
// ── Pool stats ────────────────────────────────────────────────────────────
|
|
73
|
+
poolStats: () => pool?.stats ?? null,
|
|
60
74
|
|
|
61
|
-
// ── Navigation
|
|
75
|
+
// ── Navigation ────────────────────────────────────────────────────────────
|
|
62
76
|
navigate: (url?: string, opts?: { retries?: number }) => {
|
|
63
77
|
const target = url ?? registeredUrl;
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
return withTab(t =>
|
|
79
|
+
retry(name, async () => {
|
|
80
|
+
logger.network(`[${name}] navigating → ${target}`);
|
|
81
|
+
await client.navigate(target, t);
|
|
82
|
+
_currentUrl = target;
|
|
83
|
+
}, opts?.retries ?? 2)
|
|
84
|
+
);
|
|
69
85
|
},
|
|
70
86
|
|
|
71
|
-
reload:
|
|
72
|
-
goBack:
|
|
73
|
-
goForward:
|
|
74
|
-
waitForNavigation:
|
|
87
|
+
reload: () => withTab(t => client.reload(t)),
|
|
88
|
+
goBack: () => withTab(t => client.goBack(t)),
|
|
89
|
+
goForward: () => withTab(t => client.goForward(t)),
|
|
90
|
+
waitForNavigation: () => withTab(t => client.waitForNavigation(t)),
|
|
91
|
+
|
|
92
|
+
title: () => withTab(async t => {
|
|
93
|
+
const title = await client.getTitle(t);
|
|
94
|
+
logger.info(`[${name}] title: ${title}`);
|
|
95
|
+
return title;
|
|
96
|
+
}),
|
|
75
97
|
|
|
76
|
-
title: async () => {
|
|
77
|
-
const t = await client.getTitle(tabId);
|
|
78
|
-
logger.info(`[${name}] title: ${t}`);
|
|
79
|
-
return t;
|
|
80
|
-
},
|
|
81
98
|
url: () => _currentUrl,
|
|
82
|
-
content: () => client.content(
|
|
99
|
+
content: () => withTab(t => client.content(t)),
|
|
83
100
|
|
|
84
101
|
wait: (ms: number) => {
|
|
85
102
|
const actual = humanMode ? ms + Math.floor(Math.random() * 600) - 300 : ms;
|
|
86
103
|
return new Promise<void>(r => setTimeout(r, Math.max(0, actual)));
|
|
87
104
|
},
|
|
88
105
|
|
|
89
|
-
waitForSelector: (selector: string, timeout = 30000) =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
waitForSelector: (selector: string, timeout = 30000) =>
|
|
107
|
+
withTab(t => {
|
|
108
|
+
logger.debug(`[${name}] waitForSelector: ${selector}`);
|
|
109
|
+
return client.waitForSelector(selector, timeout, t);
|
|
110
|
+
}),
|
|
111
|
+
|
|
112
|
+
waitForVisible: (selector: string, timeout = 30000) =>
|
|
113
|
+
withTab(t => client.waitForSelector(selector, timeout, t)),
|
|
114
|
+
|
|
115
|
+
waitForResponse: (pattern: string, timeout = 30000) =>
|
|
116
|
+
withTab(t => client.waitForResponse(pattern, timeout, t)),
|
|
95
117
|
|
|
96
|
-
// ── Init Script
|
|
118
|
+
// ── Init Script ───────────────────────────────────────────────────────────
|
|
97
119
|
addInitScript: async (js: string | (() => void)) => {
|
|
98
|
-
const code = typeof js ===
|
|
99
|
-
await client.addInitScript(code,
|
|
120
|
+
const code = typeof js === "function" ? `(${js.toString()})();` : js;
|
|
121
|
+
await withTab(t => client.addInitScript(code, t));
|
|
100
122
|
logger.success(`[${name}] init script added`);
|
|
101
123
|
return site;
|
|
102
124
|
},
|
|
103
125
|
|
|
104
|
-
// ──
|
|
105
|
-
// Usage: site.on('navigate', url => console.log('went to', url))
|
|
106
|
-
// Returns unsubscribe function
|
|
126
|
+
// ── Events ────────────────────────────────────────────────────────────────
|
|
107
127
|
on: (event: string, handler: (data: any) => void): (() => void) => {
|
|
108
128
|
if (!_eventListeners.has(event)) _eventListeners.set(event, new Set());
|
|
109
129
|
_eventListeners.get(event)!.add(handler);
|
|
@@ -118,217 +138,199 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
|
|
|
118
138
|
_eventListeners.get(event)?.delete(handler);
|
|
119
139
|
},
|
|
120
140
|
|
|
121
|
-
// ── Interactions
|
|
141
|
+
// ── Interactions ──────────────────────────────────────────────────────────
|
|
122
142
|
click: (selector: string, opts?: { retries?: number; timeout?: number }) =>
|
|
123
143
|
withErrScreen(() =>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
144
|
+
withTab(t =>
|
|
145
|
+
retry(name, async () => {
|
|
146
|
+
if (humanMode) await randomDelay(80, 220);
|
|
147
|
+
await client.waitForSelector(selector, opts?.timeout ?? 15000, t);
|
|
148
|
+
const ok = await client.click(selector, t);
|
|
149
|
+
if (!ok) throw new Error(`click failed: ${selector}`);
|
|
150
|
+
logger.success(`[${name}] clicked: ${selector}`);
|
|
151
|
+
return ok;
|
|
152
|
+
}, opts?.retries ?? 2)
|
|
153
|
+
),
|
|
132
154
|
`click(${selector})`
|
|
133
155
|
),
|
|
134
156
|
|
|
135
157
|
doubleClick: (selector: string) =>
|
|
136
|
-
withErrScreen(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
withErrScreen(() =>
|
|
159
|
+
withTab(async t => {
|
|
160
|
+
if (humanMode) await randomDelay(80, 200);
|
|
161
|
+
return client.doubleClick(selector, t);
|
|
162
|
+
}),
|
|
163
|
+
`dblclick(${selector})`
|
|
164
|
+
),
|
|
140
165
|
|
|
141
166
|
hover: (selector: string) =>
|
|
142
|
-
withErrScreen(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
withErrScreen(() =>
|
|
168
|
+
withTab(async t => {
|
|
169
|
+
if (humanMode) await randomDelay(50, 150);
|
|
170
|
+
return client.hover(selector, t);
|
|
171
|
+
}),
|
|
172
|
+
`hover(${selector})`
|
|
173
|
+
),
|
|
174
|
+
|
|
175
|
+
type: (selector: string, text: string, opts?: { delay?: number; retries?: number; fact?: boolean; wpm?: number }) =>
|
|
176
|
+
withErrScreen(() =>
|
|
177
|
+
withTab(async t => {
|
|
178
|
+
await client.waitForSelector(selector, 15000, t);
|
|
179
|
+
if (humanMode && !opts?.fact) {
|
|
180
|
+
const seq = humanTypeSequence(text);
|
|
181
|
+
let current = "";
|
|
182
|
+
for (const action of seq) {
|
|
183
|
+
if (action === "BACKSPACE") current = current.slice(0, -1);
|
|
184
|
+
else current += action;
|
|
185
|
+
await client.type(selector, current, t);
|
|
186
|
+
const wpm = opts?.wpm ?? 120;
|
|
187
|
+
const msPerChar = Math.round(60000 / (wpm * 5));
|
|
188
|
+
await randomDelay(msPerChar * 0.5, msPerChar * 1.8);
|
|
189
|
+
}
|
|
190
|
+
} else if (opts?.delay) {
|
|
191
|
+
for (const ch of text) {
|
|
192
|
+
await client.type(selector, ch, t);
|
|
193
|
+
await new Promise(r => setTimeout(r, opts.delay));
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
await client.type(selector, text, t);
|
|
165
197
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
198
|
+
logger.success(`[${name}] typed into: ${selector}`);
|
|
199
|
+
return true;
|
|
200
|
+
}),
|
|
201
|
+
`type(${selector})`
|
|
202
|
+
),
|
|
203
|
+
|
|
204
|
+
select: (selector: string, value: string) => withTab(t => client.select(selector, value, t)),
|
|
172
205
|
|
|
173
|
-
select: (selector: string, value: string) => client.select(selector, value, tabId),
|
|
174
206
|
evaluate: (js: string | (() => any), ...args: any[]) => {
|
|
175
|
-
const code = typeof js === "function"
|
|
176
|
-
|
|
207
|
+
const code = typeof js === "function"
|
|
208
|
+
? `(${js.toString()})(${args.map(a => JSON.stringify(a)).join(",")})`
|
|
209
|
+
: js;
|
|
210
|
+
return withTab(t => client.evaluate(code, t));
|
|
177
211
|
},
|
|
178
212
|
|
|
179
213
|
keyboard: {
|
|
180
|
-
press: (key: string) => client.keyPress(key,
|
|
181
|
-
combo: (combo: string) => client.keyCombo(combo,
|
|
214
|
+
press: (key: string) => withTab(t => client.keyPress(key, t)),
|
|
215
|
+
combo: (combo: string) => withTab(t => client.keyCombo(combo, t)),
|
|
182
216
|
},
|
|
183
217
|
|
|
184
218
|
mouse: {
|
|
185
|
-
move: (x: number, y: number) => client.mouseMove(x, y,
|
|
186
|
-
drag: (from: { x: number; y: number }, to: { x: number; y: number }) =>
|
|
219
|
+
move: (x: number, y: number) => withTab(t => client.mouseMove(x, y, t)),
|
|
220
|
+
drag: (from: { x: number; y: number }, to: { x: number; y: number }) =>
|
|
221
|
+
withTab(t => client.mouseDrag(from, to, t)),
|
|
187
222
|
},
|
|
188
223
|
|
|
189
224
|
scroll: {
|
|
190
|
-
to: (selector: string) => client.scrollTo(selector,
|
|
191
|
-
by: (px: number) => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return (async () => {
|
|
225
|
+
to: (selector: string) => withTab(t => client.scrollTo(selector, t)),
|
|
226
|
+
by: (px: number) => withTab(async t => {
|
|
227
|
+
if (humanMode) {
|
|
228
|
+
const steps = Math.ceil(Math.abs(px) / 120);
|
|
229
|
+
const chunk = px / steps;
|
|
196
230
|
for (let i = 0; i < steps; i++) {
|
|
197
|
-
await client.scrollBy(chunk,
|
|
231
|
+
await client.scrollBy(chunk, t);
|
|
198
232
|
await randomDelay(30, 80);
|
|
199
233
|
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
234
|
+
} else {
|
|
235
|
+
await client.scrollBy(px, t);
|
|
236
|
+
}
|
|
237
|
+
}) as Promise<void>,
|
|
204
238
|
},
|
|
205
239
|
|
|
206
|
-
// ── Fetch
|
|
207
|
-
fetchText: (selector: string) => client.fetchText(selector,
|
|
208
|
-
|
|
209
|
-
|
|
240
|
+
// ── Fetch ─────────────────────────────────────────────────────────────────
|
|
241
|
+
fetchText: (selector: string) => withTab(t => client.fetchText(selector, t)),
|
|
242
|
+
|
|
243
|
+
fetchLinks: async (selector: string) => {
|
|
244
|
+
const links = await withTab(t => client.fetchLinks(selector, t));
|
|
210
245
|
logger.info(`[${name}] fetchLinks(${selector}): ${links.length}`);
|
|
211
246
|
return links;
|
|
212
247
|
},
|
|
248
|
+
|
|
213
249
|
fetchImages: async (selector: string) => {
|
|
214
|
-
const imgs = await client.fetchImages(selector,
|
|
250
|
+
const imgs = await withTab(t => client.fetchImages(selector, t));
|
|
215
251
|
logger.info(`[${name}] fetchImages(${selector}): ${imgs.length}`);
|
|
216
252
|
return imgs;
|
|
217
253
|
},
|
|
218
254
|
|
|
219
255
|
search: {
|
|
220
|
-
css: (query: string) => client.searchCss(query,
|
|
221
|
-
id: (query: string) => client.searchId(query,
|
|
256
|
+
css: (query: string) => withTab(t => client.searchCss(query, t)),
|
|
257
|
+
id: (query: string) => withTab(t => client.searchId(query, t)),
|
|
222
258
|
},
|
|
223
259
|
|
|
224
|
-
// ── Screenshot / PDF
|
|
260
|
+
// ── Screenshot / PDF ──────────────────────────────────────────────────────
|
|
225
261
|
screenshot: async (filePath?: string) => {
|
|
226
|
-
const r = await client.screenshot(filePath,
|
|
262
|
+
const r = await withTab(t => client.screenshot(filePath, t));
|
|
227
263
|
logger.success(`[${name}] screenshot → ${filePath ?? "base64"}`);
|
|
228
264
|
return r;
|
|
229
265
|
},
|
|
266
|
+
|
|
230
267
|
pdf: async (filePath?: string) => {
|
|
231
|
-
const r = await client.pdf(filePath,
|
|
268
|
+
const r = await withTab(t => client.pdf(filePath, t));
|
|
232
269
|
logger.success(`[${name}] pdf → ${filePath ?? "base64"}`);
|
|
233
270
|
return r;
|
|
234
271
|
},
|
|
235
272
|
|
|
236
|
-
blockImages:
|
|
237
|
-
unblockImages:
|
|
273
|
+
blockImages: () => withTab(async t => { await client.blockImages(t); logger.info(`[${name}] images blocked`); }),
|
|
274
|
+
unblockImages: () => withTab(async t => { await client.unblockImages(t); logger.info(`[${name}] images unblocked`); }),
|
|
238
275
|
|
|
239
|
-
// ── Cookies
|
|
276
|
+
// ── Cookies ───────────────────────────────────────────────────────────────
|
|
240
277
|
cookies: {
|
|
241
278
|
set: async (cookieName: string, value: string, domain: string, path = "/") => {
|
|
242
|
-
await client.setCookie(cookieName, value, domain, path,
|
|
279
|
+
await withTab(t => client.setCookie(cookieName, value, domain, path, t));
|
|
243
280
|
logger.info(`[${name}] cookie set: ${cookieName} @ ${domain}`);
|
|
244
281
|
},
|
|
245
|
-
get:
|
|
282
|
+
get: (cookieName: string) => withTab(t => client.getCookie(cookieName, t)),
|
|
246
283
|
delete: async (cookieName: string) => {
|
|
247
|
-
await client.deleteCookie(cookieName,
|
|
284
|
+
await withTab(t => client.deleteCookie(cookieName, t));
|
|
248
285
|
logger.info(`[${name}] cookie deleted: ${cookieName}`);
|
|
249
286
|
},
|
|
250
|
-
list: () => client.listCookies(
|
|
287
|
+
list: () => withTab(t => client.listCookies(t)),
|
|
251
288
|
},
|
|
252
289
|
|
|
253
|
-
// ── Interception
|
|
290
|
+
// ── Interception ──────────────────────────────────────────────────────────
|
|
254
291
|
intercept: {
|
|
255
292
|
block: async (pattern: string) => {
|
|
256
|
-
await client.addInterceptRule("block", pattern, {},
|
|
293
|
+
await withTab(t => client.addInterceptRule("block", pattern, {}, t));
|
|
257
294
|
logger.info(`[${name}] intercept block: ${pattern}`);
|
|
258
295
|
},
|
|
259
296
|
|
|
260
297
|
redirect: async (pattern: string, redirectUrl: string) => {
|
|
261
|
-
await client.addInterceptRule("redirect", pattern, { redirectUrl },
|
|
298
|
+
await withTab(t => client.addInterceptRule("redirect", pattern, { redirectUrl }, t));
|
|
262
299
|
logger.info(`[${name}] intercept redirect: ${pattern} → ${redirectUrl}`);
|
|
263
300
|
},
|
|
264
301
|
|
|
265
302
|
headers: async (pattern: string, headers: Record<string, string>) => {
|
|
266
|
-
await client.addInterceptRule("modifyHeaders", pattern, { headers },
|
|
303
|
+
await withTab(t => client.addInterceptRule("modifyHeaders", pattern, { headers }, t));
|
|
267
304
|
logger.info(`[${name}] intercept modifyHeaders: ${pattern}`);
|
|
268
305
|
},
|
|
269
306
|
|
|
270
|
-
// ── NEW: intercept.respond ──────────────────────────────────────────────
|
|
271
|
-
// Intercepts matching requests and returns a fake response — request never
|
|
272
|
-
// leaves the browser. Works for both fetch and XHR via JS injection.
|
|
273
|
-
//
|
|
274
|
-
// Usage:
|
|
275
|
-
// await site.intercept.respond('/api/prices', (req) => ({
|
|
276
|
-
// status: 200,
|
|
277
|
-
// contentType: 'application/json',
|
|
278
|
-
// body: JSON.stringify({ price: 99 })
|
|
279
|
-
// }))
|
|
280
|
-
//
|
|
281
|
-
// // Static shorthand:
|
|
282
|
-
// await site.intercept.respond('/api/prices', {
|
|
283
|
-
// status: 200, contentType: 'application/json', body: '{"price":99}'
|
|
284
|
-
// })
|
|
285
307
|
respond: async (
|
|
286
308
|
pattern: string,
|
|
287
309
|
handlerOrResponse:
|
|
288
310
|
| { status?: number; contentType?: string; body: string }
|
|
289
311
|
| ((req: { url: string; method: string }) => { status?: number; contentType?: string; body: string })
|
|
290
312
|
) => {
|
|
291
|
-
// Static response — just inject the JS rule directly
|
|
292
313
|
const isStatic = typeof handlerOrResponse === "object";
|
|
293
|
-
const response = isStatic
|
|
294
|
-
? handlerOrResponse
|
|
295
|
-
: { status: 200, contentType: "application/json", body: "" };
|
|
296
314
|
|
|
297
315
|
if (!isStatic) {
|
|
298
|
-
// Dynamic: expose a function, call it from the injected script
|
|
299
316
|
const fnName = `__piggy_respond_${name}_${++_modifyRuleCounter}__`;
|
|
300
|
-
|
|
301
317
|
await client.exposeFunction(fnName, async (req: { url: string; method: string }) => {
|
|
302
318
|
try {
|
|
303
319
|
const result = (handlerOrResponse as Function)(req);
|
|
304
|
-
return {
|
|
305
|
-
success: true,
|
|
306
|
-
result: {
|
|
307
|
-
status: result.status ?? 200,
|
|
308
|
-
contentType: result.contentType ?? "application/json",
|
|
309
|
-
body: result.body ?? "",
|
|
310
|
-
}
|
|
311
|
-
};
|
|
320
|
+
return { success: true, result: { status: result.status ?? 200, contentType: result.contentType ?? "application/json", body: result.body ?? "" } };
|
|
312
321
|
} catch (e: any) {
|
|
313
322
|
return { success: false, error: e.message };
|
|
314
323
|
}
|
|
315
324
|
}, tabId);
|
|
316
325
|
|
|
317
|
-
// Inject a script that calls the exposed function instead of static body
|
|
318
326
|
const dynamicScript = `
|
|
319
327
|
(function() {
|
|
320
328
|
'use strict';
|
|
321
329
|
if (!window.__PIGGY_DYNAMIC_RESPOND__) window.__PIGGY_DYNAMIC_RESPOND__ = [];
|
|
322
330
|
window.__PIGGY_DYNAMIC_RESPOND__.push({ pattern: ${JSON.stringify(pattern)}, fn: ${JSON.stringify(fnName)} });
|
|
323
|
-
|
|
324
|
-
function matchUrl(url, pattern) {
|
|
325
|
-
try { return url.includes(pattern) || new RegExp(pattern).test(url); }
|
|
326
|
-
catch { return url.includes(pattern); }
|
|
327
|
-
}
|
|
328
|
-
|
|
331
|
+
function matchUrl(url, pattern) { try { return url.includes(pattern) || new RegExp(pattern).test(url); } catch { return url.includes(pattern); } }
|
|
329
332
|
if (window.__PIGGY_DYN_INSTALLED__) return;
|
|
330
333
|
window.__PIGGY_DYN_INSTALLED__ = true;
|
|
331
|
-
|
|
332
334
|
const _origFetch = window.fetch;
|
|
333
335
|
window.fetch = async function(input, init) {
|
|
334
336
|
const url = typeof input === 'string' ? input : (input?.url ?? String(input));
|
|
@@ -336,95 +338,81 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
|
|
|
336
338
|
const rules = window.__PIGGY_DYNAMIC_RESPOND__ || [];
|
|
337
339
|
for (const rule of rules) {
|
|
338
340
|
if (matchUrl(url, rule.pattern) && typeof window[rule.fn] === 'function') {
|
|
339
|
-
try {
|
|
340
|
-
const r = await window[rule.fn]({ url, method });
|
|
341
|
-
return new Response(r.body ?? '', {
|
|
342
|
-
status: r.status ?? 200,
|
|
343
|
-
headers: { 'Content-Type': r.contentType ?? 'application/json' }
|
|
344
|
-
});
|
|
345
|
-
} catch { break; }
|
|
341
|
+
try { const r = await window[rule.fn]({ url, method }); return new Response(r.body ?? '', { status: r.status ?? 200, headers: { 'Content-Type': r.contentType ?? 'application/json' } }); } catch { break; }
|
|
346
342
|
}
|
|
347
343
|
}
|
|
348
344
|
return _origFetch.apply(this, arguments);
|
|
349
345
|
};
|
|
350
346
|
})();`;
|
|
351
|
-
await
|
|
352
|
-
|
|
347
|
+
await withTab(async t => {
|
|
348
|
+
await client.addInitScript(dynamicScript, t);
|
|
349
|
+
await client.evaluate(dynamicScript, t);
|
|
350
|
+
});
|
|
353
351
|
logger.success(`[${name}] intercept.respond (dynamic): ${pattern}`);
|
|
354
352
|
return site;
|
|
355
353
|
}
|
|
356
354
|
|
|
357
|
-
|
|
358
|
-
const script = buildRespondScript(
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
);
|
|
364
|
-
await client.addInitScript(script, tabId);
|
|
365
|
-
await client.evaluate(script, tabId);
|
|
355
|
+
const response = handlerOrResponse;
|
|
356
|
+
const script = buildRespondScript(pattern, response.status ?? 200, response.contentType ?? "application/json", response.body);
|
|
357
|
+
await withTab(async t => {
|
|
358
|
+
await client.addInitScript(script, t);
|
|
359
|
+
await client.evaluate(script, t);
|
|
360
|
+
});
|
|
366
361
|
logger.success(`[${name}] intercept.respond (static): ${pattern} → ${response.status ?? 200}`);
|
|
367
362
|
return site;
|
|
368
363
|
},
|
|
369
364
|
|
|
370
|
-
// ── NEW: intercept.modifyResponse ───────────────────────────────────────
|
|
371
|
-
// Lets the request hit the network, then calls your handler with the
|
|
372
|
-
// response. Return { body?, status?, headers? } to modify, or {} to
|
|
373
|
-
// pass through unchanged.
|
|
374
|
-
//
|
|
375
|
-
// Usage:
|
|
376
|
-
// await site.intercept.modifyResponse('/api/feed', async ({ body, status }) => {
|
|
377
|
-
// const data = JSON.parse(body)
|
|
378
|
-
// data.items = data.items.slice(0, 5)
|
|
379
|
-
// return { body: JSON.stringify(data) }
|
|
380
|
-
// })
|
|
381
365
|
modifyResponse: async (
|
|
382
366
|
pattern: string,
|
|
383
367
|
handler: (response: { body: string; status: number; headers: Record<string, string> }) =>
|
|
384
368
|
Promise<{ body?: string; status?: number; headers?: Record<string, string> } | void> | void
|
|
385
369
|
) => {
|
|
386
370
|
const fnName = `__piggy_modres_${name}_${++_modifyRuleCounter}__`;
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const mod = await handler(response);
|
|
391
|
-
return { success: true, result: mod ?? {} };
|
|
392
|
-
} catch (e: any) {
|
|
393
|
-
return { success: false, error: e.message };
|
|
394
|
-
}
|
|
371
|
+
await client.exposeFunction(fnName, async (response: any) => {
|
|
372
|
+
try { const mod = await handler(response); return { success: true, result: mod ?? {} }; }
|
|
373
|
+
catch (e: any) { return { success: false, error: e.message }; }
|
|
395
374
|
}, tabId);
|
|
396
375
|
|
|
397
376
|
const script = buildModifyResponseScript(pattern, fnName);
|
|
398
|
-
await
|
|
399
|
-
|
|
377
|
+
await withTab(async t => {
|
|
378
|
+
await client.addInitScript(script, t);
|
|
379
|
+
await client.evaluate(script, t);
|
|
380
|
+
});
|
|
400
381
|
logger.success(`[${name}] intercept.modifyResponse: ${pattern}`);
|
|
401
382
|
return site;
|
|
402
383
|
},
|
|
403
384
|
|
|
404
385
|
clear: async () => {
|
|
405
|
-
await client.clearInterceptRules(
|
|
386
|
+
await withTab(t => client.clearInterceptRules(t));
|
|
406
387
|
logger.info(`[${name}] intercept rules cleared`);
|
|
407
388
|
},
|
|
408
389
|
},
|
|
409
390
|
|
|
410
|
-
// ── Network capture
|
|
391
|
+
// ── Network capture ───────────────────────────────────────────────────────
|
|
411
392
|
capture: {
|
|
412
|
-
start:
|
|
413
|
-
stop:
|
|
414
|
-
requests: () => client.captureRequests(
|
|
415
|
-
ws: () => client.captureWs(
|
|
416
|
-
cookies: () => client.captureCookies(
|
|
417
|
-
storage: () => client.captureStorage(
|
|
418
|
-
clear:
|
|
393
|
+
start: () => withTab(async t => { await client.captureStart(t); logger.info(`[${name}] capture started`); }),
|
|
394
|
+
stop: () => withTab(async t => { await client.captureStop(t); logger.info(`[${name}] capture stopped`); }),
|
|
395
|
+
requests: () => withTab(t => client.captureRequests(t)),
|
|
396
|
+
ws: () => withTab(t => client.captureWs(t)),
|
|
397
|
+
cookies: () => withTab(t => client.captureCookies(t)),
|
|
398
|
+
storage: () => withTab(t => client.captureStorage(t)),
|
|
399
|
+
clear: () => withTab(async t => { await client.captureClear(t); logger.info(`[${name}] capture cleared`); }),
|
|
419
400
|
},
|
|
420
401
|
|
|
421
|
-
// ── Session
|
|
402
|
+
// ── Session ───────────────────────────────────────────────────────────────
|
|
422
403
|
session: {
|
|
423
|
-
export: async () => {
|
|
424
|
-
|
|
404
|
+
export: async () => {
|
|
405
|
+
const data = await withTab(t => client.sessionExport(t));
|
|
406
|
+
logger.success(`[${name}] session exported`);
|
|
407
|
+
return data;
|
|
408
|
+
},
|
|
409
|
+
import: async (data: any) => {
|
|
410
|
+
await withTab(t => client.sessionImport(data, t));
|
|
411
|
+
logger.success(`[${name}] session imported`);
|
|
412
|
+
},
|
|
425
413
|
},
|
|
426
414
|
|
|
427
|
-
// ── Expose Function
|
|
415
|
+
// ── Expose Function ───────────────────────────────────────────────────────
|
|
428
416
|
exposeFunction: async (fnName: string, handler: (data: any) => Promise<any> | any) => {
|
|
429
417
|
await client.exposeFunction(fnName, handler, tabId);
|
|
430
418
|
logger.success(`[${name}] exposed function: ${fnName}`);
|
|
@@ -443,21 +431,42 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
|
|
|
443
431
|
exposeAndInject: async (fnName: string, handler: (data: any) => Promise<any> | any, injectionJs: string | ((fnName: string) => string)) => {
|
|
444
432
|
await client.exposeFunction(fnName, handler, tabId);
|
|
445
433
|
const js = typeof injectionJs === "function" ? injectionJs(fnName) : injectionJs;
|
|
446
|
-
await client.evaluate(js,
|
|
434
|
+
await withTab(t => client.evaluate(js, t));
|
|
447
435
|
logger.success(`[${name}] exposed and injected: ${fnName}`);
|
|
448
436
|
return site;
|
|
449
437
|
},
|
|
450
438
|
|
|
451
|
-
// ──
|
|
452
|
-
|
|
439
|
+
// ── Store ─────────────────────────────────────────────────────────────────
|
|
440
|
+
store: async (
|
|
441
|
+
data: Record<string, any> | Record<string, any>[],
|
|
442
|
+
schemaName?: string
|
|
443
|
+
) => {
|
|
444
|
+
const target = schemaName ?? name;
|
|
445
|
+
const result = await storeRecord(target, data);
|
|
446
|
+
logger.info(`[${name}] store → stored: ${result.stored}, skipped: ${result.skipped}`);
|
|
447
|
+
return result;
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
// ── Elysia API ────────────────────────────────────────────────────────────
|
|
451
|
+
api: (
|
|
452
|
+
path: string,
|
|
453
|
+
handler: RouteHandler,
|
|
454
|
+
opts?: {
|
|
455
|
+
ttl?: number;
|
|
456
|
+
before?: BeforeMiddleware[];
|
|
457
|
+
method?: "GET" | "POST" | "PUT" | "DELETE";
|
|
458
|
+
detail?: RouteDetail;
|
|
459
|
+
}
|
|
460
|
+
) => {
|
|
453
461
|
const key = `${name}:${path}`;
|
|
454
462
|
if (routeRegistry.has(key)) { logger.warn(`[${name}] route ${path} already registered`); return site; }
|
|
455
463
|
routeRegistry.set(key, {
|
|
456
464
|
path,
|
|
457
|
-
method:
|
|
465
|
+
method: opts?.method ?? "GET",
|
|
458
466
|
handler,
|
|
459
|
-
ttl:
|
|
460
|
-
before:
|
|
467
|
+
ttl: opts?.ttl ?? 360_000,
|
|
468
|
+
before: opts?.before ?? [],
|
|
469
|
+
detail: opts?.detail,
|
|
461
470
|
});
|
|
462
471
|
logger.info(`[${name}] api route: ${opts?.method ?? "GET"} /${name}${path}`);
|
|
463
472
|
return site;
|
|
@@ -466,9 +475,11 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
|
|
|
466
475
|
noclose: () => { keepAliveSites.add(name); logger.info(`[${name}] keep-alive`); return site; },
|
|
467
476
|
|
|
468
477
|
close: async () => {
|
|
469
|
-
_unsubNavigate();
|
|
478
|
+
_unsubNavigate();
|
|
470
479
|
keepAliveSites.delete(name);
|
|
471
|
-
if (
|
|
480
|
+
if (pool) {
|
|
481
|
+
await pool.close();
|
|
482
|
+
} else if (tabId !== "default") {
|
|
472
483
|
await client.closeTab(tabId);
|
|
473
484
|
logger.info(`[${name}] tab closed`);
|
|
474
485
|
}
|
|
@@ -478,7 +489,13 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
|
|
|
478
489
|
return site;
|
|
479
490
|
}
|
|
480
491
|
|
|
481
|
-
export
|
|
492
|
+
export type SiteObject = ReturnType<typeof createSiteObject>;
|
|
493
|
+
|
|
494
|
+
export function createExposedAPI<T extends Record<string, (data: any) => any>>(
|
|
495
|
+
site: any,
|
|
496
|
+
apiName: string,
|
|
497
|
+
handlers: T
|
|
498
|
+
): Promise<void> {
|
|
482
499
|
const wrappedHandler = async (call: any) => {
|
|
483
500
|
const { method, args } = call;
|
|
484
501
|
const handler = handlers[method as keyof T];
|