@bluefields/fetcher 0.2.0

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.
Files changed (44) hide show
  1. package/LICENSE +661 -0
  2. package/dist/asset-block-measure.d.ts +14 -0
  3. package/dist/asset-block-measure.js +82 -0
  4. package/dist/asset-block-measure.js.map +1 -0
  5. package/dist/browser-pool.d.ts +65 -0
  6. package/dist/browser-pool.js +161 -0
  7. package/dist/browser-pool.js.map +1 -0
  8. package/dist/cost.d.ts +51 -0
  9. package/dist/cost.js +54 -0
  10. package/dist/cost.js.map +1 -0
  11. package/dist/fetcher-brightdata.d.ts +52 -0
  12. package/dist/fetcher-brightdata.js +135 -0
  13. package/dist/fetcher-brightdata.js.map +1 -0
  14. package/dist/fetcher-escalating.d.ts +100 -0
  15. package/dist/fetcher-escalating.js +489 -0
  16. package/dist/fetcher-escalating.js.map +1 -0
  17. package/dist/fetcher-http.d.ts +81 -0
  18. package/dist/fetcher-http.js +395 -0
  19. package/dist/fetcher-http.js.map +1 -0
  20. package/dist/fetcher-patchright.d.ts +212 -0
  21. package/dist/fetcher-patchright.js +505 -0
  22. package/dist/fetcher-patchright.js.map +1 -0
  23. package/dist/fetcher-robots-gate.d.ts +60 -0
  24. package/dist/fetcher-robots-gate.js +87 -0
  25. package/dist/fetcher-robots-gate.js.map +1 -0
  26. package/dist/index.d.ts +93 -0
  27. package/dist/index.js +219 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/internal-options.d.ts +59 -0
  30. package/dist/internal-options.js +16 -0
  31. package/dist/internal-options.js.map +1 -0
  32. package/dist/perf-harness.d.ts +105 -0
  33. package/dist/perf-harness.js +176 -0
  34. package/dist/perf-harness.js.map +1 -0
  35. package/dist/storage-memory.d.ts +18 -0
  36. package/dist/storage-memory.js +45 -0
  37. package/dist/storage-memory.js.map +1 -0
  38. package/dist/storage-r2.d.ts +44 -0
  39. package/dist/storage-r2.js +209 -0
  40. package/dist/storage-r2.js.map +1 -0
  41. package/dist/types.d.ts +231 -0
  42. package/dist/types.js +22 -0
  43. package/dist/types.js.map +1 -0
  44. package/package.json +54 -0
@@ -0,0 +1,505 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * Patchright fetcher adapter (chunks 8 + Phase A real wiring).
4
+ *
5
+ * Patchright is a Chromium fork with CDP-leak patches that defeat many
6
+ * naive bot-detection signals (navigator.webdriver, console.debug
7
+ * fingerprinting, etc.). API is Playwright-compatible — drop-in.
8
+ *
9
+ * Production wiring is gated on IPROYAL_* env vars: when they're set,
10
+ * the factory (`tryCreatePatchrightAdapter`) returns a real adapter
11
+ * that launches Chromium with the IPRoyal residential proxy. Without
12
+ * them, callers fall back to the HTTP adapter (`fetcher-http.ts`).
13
+ *
14
+ * Per-fetch lifecycle: launch fresh browser → newContext → newPage →
15
+ * goto → wait → content + (optional) screenshot → close. One browser
16
+ * per fetch keeps fingerprint state isolated; pooling can ship later
17
+ * if launch overhead becomes load-bearing (~500ms per fetch today).
18
+ *
19
+ * Fingerprint rotation: v1 uses a small pool of (UA, viewport, locale,
20
+ * timezone) tuples picked randomly per fetch. Full diversity (Review #4)
21
+ * comes later — we just need basic variation to defeat per-tuple
22
+ * fingerprint blocklists.
23
+ *
24
+ * Egress IP: deferred. IPRoyal residential proxies rotate per-session;
25
+ * resolving the actual egress IP requires an extra eval to api.ipify.org
26
+ * which we don't want to pay on every fetch. Logged as a placeholder for
27
+ * now; revisit in M14-17 when attestation needs it (Review #3 manifest).
28
+ */
29
+ import { createLayerLogger, envInt } from '@bluefields/primitives';
30
+ import { BrowserPool } from './browser-pool.js';
31
+ const log = createLayerLogger('fetcher');
32
+ // Through a residential proxy a single page load can take 15-45s
33
+ // (proxy hop + per-asset proxy auth + slow CDN paths). 30s was too
34
+ // aggressive — bump to 60s.
35
+ const DEFAULT_TIMEOUT_MS = envInt('BLUEFIELDS_BROWSER_TIMEOUT_MS', 60_000);
36
+ const FETCHER_ID = 'patchright-v1';
37
+ // ── Full-render settle tuning (see fullyRenderPage) ──────────────────────────
38
+ // All bounds are belt-and-suspenders: the loop stops at the FIRST of (content
39
+ // looks rendered), (DOM stopped growing), (max steps), or (deadline). Worst-case
40
+ // added wall-clock ≈ networkidle cap + maxSteps*stepMs, always clamped to the
41
+ // request's timeoutMs so full render can never hang past the caller's deadline.
42
+ /** Best-effort `networkidle` wait cap (ms) — well under the default request timeout. */
43
+ const FULL_RENDER_NETWORKIDLE_MS = 15_000;
44
+ /** Pause after each scroll so lazy/XHR content can paint before we re-measure. */
45
+ const FULL_RENDER_STEP_MS = 300;
46
+ /** Hard cap on settle iterations — primary bound so a stubbed clock can't spin. */
47
+ const FULL_RENDER_MAX_STEPS = 10;
48
+ /** Once the page has at least this much visible text, treat it as rendered. */
49
+ const FULL_RENDER_MIN_CHARS = 500;
50
+ // Only the gateway is strictly required — IPROYAL_USERNAME/PASSWORD are
51
+ // optional. If the proxy uses IP allowlist auth (Fly egress IP whitelisted
52
+ // on IPRoyal), no credentials are needed. The Chromium ↔ HTTP-proxy auth
53
+ // dance is broken anyway, so allowlist is the only path that works.
54
+ export const PATCHRIGHT_REQUIRED_ENV = ['IPROYAL_GATEWAY'];
55
+ const FINGERPRINT_POOL = [
56
+ {
57
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
58
+ viewport: { width: 1440, height: 900 },
59
+ locale: 'en-US',
60
+ timezoneId: 'America/New_York',
61
+ },
62
+ {
63
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
64
+ viewport: { width: 1920, height: 1080 },
65
+ locale: 'en-US',
66
+ timezoneId: 'America/Chicago',
67
+ },
68
+ {
69
+ userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
70
+ viewport: { width: 1366, height: 768 },
71
+ locale: 'en-GB',
72
+ timezoneId: 'Europe/London',
73
+ },
74
+ ];
75
+ function pickFingerprint(rand = Math.random) {
76
+ const idx = Math.floor(rand() * FINGERPRINT_POOL.length);
77
+ return FINGERPRINT_POOL[idx];
78
+ }
79
+ /**
80
+ * Resource types whose bytes we discard. Images/video/audio/fonts are the bulk
81
+ * of page weight and we only keep DOM text, so aborting them on the metered
82
+ * residential proxy is pure savings. We NEVER block document/script/xhr/fetch —
83
+ * the page wouldn't render. Stylesheets are usually safe to drop too (we read
84
+ * text, not layout); that one is gated behind BLOCK_STYLESHEET so it's a
85
+ * one-line flip if it ever regresses a JS-render stratum.
86
+ */
87
+ const BLOCKED_RESOURCE_TYPES = new Set(['image', 'media', 'font']);
88
+ /** Drop stylesheets too (default ON). Flip to `false` if it regresses rendering. */
89
+ const BLOCK_STYLESHEET = true;
90
+ /**
91
+ * Pure predicate: should the browser tier abort this request's download?
92
+ * Extracted so it's unit-tested without a live browser. The caller is
93
+ * responsible for skipping interception entirely when a screenshot is
94
+ * requested (a screenshot needs the full visual render).
95
+ */
96
+ export function shouldBlockResource(resourceType, opts = {}) {
97
+ if (BLOCKED_RESOURCE_TYPES.has(resourceType))
98
+ return true;
99
+ if ((opts.blockStylesheet ?? BLOCK_STYLESHEET) && resourceType === 'stylesheet')
100
+ return true;
101
+ return false;
102
+ }
103
+ /**
104
+ * Returns the env-gated Patchright adapter when the required env vars
105
+ * are set; null otherwise. Callers (the factory in `index.ts`) fall
106
+ * back to the HTTP adapter when this returns null.
107
+ */
108
+ export function tryCreatePatchrightAdapter(env = process.env) {
109
+ for (const key of PATCHRIGHT_REQUIRED_ENV) {
110
+ if (!env[key] || env[key]?.length === 0)
111
+ return null;
112
+ }
113
+ // Pass username/password ONLY if they're set. Production path uses
114
+ // IP allowlist auth (Fly egress IP whitelisted on IPRoyal) — no
115
+ // creds needed, no Chromium auth dance, no broken CONNECT tunnels.
116
+ // Local dev with personal IP whitelisted works the same way.
117
+ // If creds ARE set (legacy callers), pass them through — Patchright
118
+ // will try the CDP challenge handshake; works on most proxies, fails
119
+ // on IPRoyal residential.
120
+ const poolSize = parseIntEnv(env.PATCHRIGHT_POOL ?? '', 0);
121
+ return createPatchrightAdapter({
122
+ proxy: {
123
+ server: normalizeProxyServer(env.IPROYAL_GATEWAY ?? ''),
124
+ username: env.IPROYAL_USERNAME ?? '',
125
+ password: env.IPROYAL_PASSWORD ?? '',
126
+ },
127
+ ...(poolSize > 0 ? { pool: poolSize } : {}),
128
+ });
129
+ }
130
+ function parseIntEnv(raw, fallback) {
131
+ const n = Number.parseInt(raw, 10);
132
+ return Number.isFinite(n) && n > 0 ? n : fallback;
133
+ }
134
+ /**
135
+ * Normalize the proxy server URL — Playwright requires a protocol
136
+ * scheme (`http://host:port`), but IPRoyal's docs typically show the
137
+ * gateway as bare `host:port`. Prepend `http://` if missing so a
138
+ * naively-pasted value still works.
139
+ */
140
+ function normalizeProxyServer(raw) {
141
+ const trimmed = raw.trim();
142
+ if (!trimmed)
143
+ return '';
144
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed))
145
+ return trimmed;
146
+ return `http://${trimmed}`;
147
+ }
148
+ /**
149
+ * Direct constructor — bypass the env gate. Useful in tests + when the
150
+ * caller has the proxy creds from somewhere other than env vars.
151
+ */
152
+ export function createPatchrightAdapter(config) {
153
+ let cachedLauncher = config.chromiumImpl ?? null;
154
+ let pool = null;
155
+ let poolInitPromise = null;
156
+ const getLauncher = async () => {
157
+ if (cachedLauncher)
158
+ return cachedLauncher;
159
+ // Dynamic import so missing chromium binary / missing patchright
160
+ // package doesn't fail at module-load time.
161
+ const mod = (await import('patchright'));
162
+ cachedLauncher = mod.chromium;
163
+ return cachedLauncher;
164
+ };
165
+ const getPool = async () => {
166
+ if (!config.pool || config.pool <= 0)
167
+ return null;
168
+ if (pool)
169
+ return pool;
170
+ if (!poolInitPromise) {
171
+ poolInitPromise = (async () => {
172
+ const launcher = await getLauncher();
173
+ pool = new BrowserPool({ launcher, proxy: config.proxy, size: config.pool });
174
+ try {
175
+ await pool.warm();
176
+ }
177
+ catch (err) {
178
+ log.warn({ error: err instanceof Error ? err.message : String(err) }, 'browser-pool warm failed; falling back to per-fetch launch');
179
+ pool = null;
180
+ }
181
+ })();
182
+ }
183
+ await poolInitPromise;
184
+ return pool;
185
+ };
186
+ return {
187
+ /**
188
+ * L4: eagerly build + warm the browser pool so the first customer fetch doesn't
189
+ * pay the ~0.5–1.5s cold Chromium launch. No-op when `config.pool` is unset/0
190
+ * (getPool returns null). Best-effort — getPool already swallows a warm failure
191
+ * and falls back to per-fetch launch.
192
+ */
193
+ async warmUp() {
194
+ await getPool();
195
+ },
196
+ async fetch(url, options = {}) {
197
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
198
+ const now = options.nowFn ?? (() => new Date());
199
+ const fingerprint = pickFingerprint(config.rand);
200
+ const userAgent = options.userAgent ?? config.defaultUserAgent ?? fingerprint.userAgent;
201
+ const startedAt = Date.now();
202
+ const fetchedAt = now();
203
+ const activePool = await getPool();
204
+ let slot = null;
205
+ let browser;
206
+ if (activePool) {
207
+ try {
208
+ slot = await activePool.acquire();
209
+ browser = slot.browser;
210
+ }
211
+ catch (err) {
212
+ // Pool exhaustion / launch failure → fall back to per-fetch launch.
213
+ log.warn({ error: err instanceof Error ? err.message : String(err) }, 'browser-pool acquire failed; per-fetch launch fallback');
214
+ const launcher = await getLauncher();
215
+ browser = await launcher.launch({
216
+ proxy: config.proxy,
217
+ headless: true,
218
+ args: ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu'],
219
+ timeout: 20_000,
220
+ });
221
+ }
222
+ }
223
+ else {
224
+ const launcher = await getLauncher();
225
+ browser = await launcher.launch({
226
+ proxy: config.proxy,
227
+ headless: true,
228
+ args: ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu'],
229
+ timeout: 20_000,
230
+ });
231
+ }
232
+ let crashed = false;
233
+ let context = null;
234
+ // Abandonment hook: the escalating fetcher aborts this when it gives up on
235
+ // this tier's wall-clock budget. Closing the context makes any in-flight
236
+ // goto / full-render reject so the finally frees the browser — without it an
237
+ // abandoned browser fetch leaks headless Chromium (and the Node process).
238
+ const abortSignal = options.__abortSignal;
239
+ const onAbort = () => {
240
+ void context?.close?.().catch(() => undefined);
241
+ };
242
+ if (abortSignal) {
243
+ if (abortSignal.aborted)
244
+ onAbort();
245
+ else
246
+ abortSignal.addEventListener('abort', onAbort, { once: true });
247
+ }
248
+ try {
249
+ context = await browser.newContext({
250
+ userAgent,
251
+ viewport: fingerprint.viewport,
252
+ locale: fingerprint.locale,
253
+ timezoneId: fingerprint.timezoneId,
254
+ ...(options.headers ? { extraHTTPHeaders: options.headers } : {}),
255
+ });
256
+ if (options.cookies && options.cookies.length > 0) {
257
+ await context.addCookies(options.cookies);
258
+ }
259
+ const page = await context.newPage();
260
+ // Cut metered proxy egress: abort the image/media/font (+ stylesheet)
261
+ // downloads we never use — we only keep DOM text — and sum the bytes we
262
+ // DO transfer. Aborted requests never fire 'response', so the counter
263
+ // measures exactly what we kept. Skipped entirely when a screenshot is
264
+ // requested (it needs the full visual render). Both APIs are guarded
265
+ // because the narrow test mock omits them.
266
+ let bytesTransferred = 0;
267
+ if (typeof page.on === 'function') {
268
+ page.on('response', (resp) => {
269
+ const len = Number(resp.headers()['content-length'] ?? 0);
270
+ if (Number.isFinite(len) && len > 0)
271
+ bytesTransferred += len;
272
+ });
273
+ }
274
+ if (!options.captureScreenshot && typeof page.route === 'function') {
275
+ await page.route('**/*', async (route) => {
276
+ try {
277
+ if (shouldBlockResource(route.request().resourceType()))
278
+ await route.abort();
279
+ else
280
+ await route.continue();
281
+ }
282
+ catch {
283
+ // route already handled / page closed mid-flight — ignore.
284
+ }
285
+ });
286
+ }
287
+ // Default to 'domcontentloaded' — much faster than 'load'
288
+ // (no waiting for images/fonts/iframes) and almost always gives
289
+ // us the rendered HTML. Customers can override via waitFor.
290
+ const goStart = Date.now();
291
+ const response = await page.goto(url, {
292
+ waitUntil: gotoWaitUntil(options.waitFor),
293
+ timeout: timeoutMs,
294
+ });
295
+ log.info({ outcome: 'partial', url, goto_ms: Date.now() - goStart }, 'patchright goto finished');
296
+ if (options.waitFor?.type === 'selector') {
297
+ await page.waitForSelector(options.waitFor.selector, {
298
+ timeout: options.waitFor.timeoutMs ?? 5_000,
299
+ });
300
+ }
301
+ else if (options.waitFor?.type === 'timeout') {
302
+ await page.waitForTimeout(options.waitFor.ms);
303
+ }
304
+ else if (options.__fullRender) {
305
+ // Escalation re-fetch of a JS shell with no customer waitFor: fully
306
+ // render the SPA (best-effort networkidle + autoscroll + content
307
+ // settle) so we read hydrated content instead of the same empty shell
308
+ // tier-0 returned. Clamped to the request deadline — never hangs.
309
+ await fullyRenderPage(page, { deadlineMs: startedAt + timeoutMs });
310
+ }
311
+ let actionResults;
312
+ if (options.actions && options.actions.length > 0) {
313
+ actionResults = await runActions(page, options.actions);
314
+ }
315
+ const html = await page.content();
316
+ const screenshot = options.captureScreenshot
317
+ ? await page.screenshot({ fullPage: true })
318
+ : null;
319
+ const responseStatus = response?.status() ?? 0;
320
+ const responseHeaders = response?.headers() ?? {};
321
+ const finalUrl = page.url();
322
+ const fetchDurationMs = Math.max(1, Date.now() - startedAt);
323
+ log.info({
324
+ outcome: 'ok',
325
+ url,
326
+ final_url: finalUrl,
327
+ response_status: responseStatus,
328
+ duration_ms: fetchDurationMs,
329
+ screenshot: !!screenshot,
330
+ }, 'patchright fetch ok');
331
+ return {
332
+ html,
333
+ responseStatus,
334
+ responseHeaders,
335
+ fetchedAt,
336
+ fetchDurationMs,
337
+ fetcherId: FETCHER_ID,
338
+ proxyEgressIp: '0.0.0.0',
339
+ userAgent,
340
+ rawHtmlSizeBytes: Buffer.byteLength(html, 'utf8'),
341
+ bytesTransferred,
342
+ screenshot,
343
+ finalUrl,
344
+ ...(actionResults ? { actionResults } : {}),
345
+ };
346
+ }
347
+ catch (err) {
348
+ crashed = true;
349
+ throw err;
350
+ }
351
+ finally {
352
+ if (abortSignal)
353
+ abortSignal.removeEventListener('abort', onAbort);
354
+ // Close the per-fetch context (and its pages) FIRST. A pooled browser is
355
+ // released (kept warm), not closed, so without this it would accumulate
356
+ // one BrowserContext per fetch — a steady memory leak. Best-effort and
357
+ // harmless on the per-fetch-launch path where browser.close() also tears
358
+ // it down. Fetcher-layer cleanup only; no behavior change to the result.
359
+ if (context?.close) {
360
+ await context.close().catch((err) => {
361
+ log.warn({ error: err instanceof Error ? err.message : String(err) }, 'context.close() failed');
362
+ });
363
+ }
364
+ if (slot && activePool) {
365
+ if (crashed)
366
+ activePool.dispose(slot);
367
+ else
368
+ activePool.release(slot);
369
+ }
370
+ else {
371
+ await browser.close().catch((err) => {
372
+ log.warn({ error: err instanceof Error ? err.message : String(err) }, 'browser.close() failed');
373
+ });
374
+ }
375
+ }
376
+ },
377
+ };
378
+ }
379
+ /**
380
+ * Map the customer waitFor to the goto() `waitUntil`. Only `networkIdle` uses
381
+ * Playwright 'networkidle'; everything else — the default, plus the selector /
382
+ * timeout conditions (which wait AFTER navigation) — uses the fast
383
+ * 'domcontentloaded' (much quicker than 'load' and enough to read rendered HTML).
384
+ */
385
+ function gotoWaitUntil(wait) {
386
+ return wait?.type === 'networkIdle' ? 'networkidle' : 'domcontentloaded';
387
+ }
388
+ /**
389
+ * Read visible-text length + scroll height in one page eval. Returns {0,0} on any
390
+ * failure (eval rejected, non-string result, unparseable) so callers never throw.
391
+ */
392
+ async function readRenderMetric(page) {
393
+ try {
394
+ const raw = await page.evaluate('JSON.stringify({t:(document.body&&document.body.innerText||"").length,h:document.body?document.body.scrollHeight:0})');
395
+ if (typeof raw !== 'string')
396
+ return { textLen: 0, scrollHeight: 0 };
397
+ const parsed = JSON.parse(raw);
398
+ return { textLen: Number(parsed.t) || 0, scrollHeight: Number(parsed.h) || 0 };
399
+ }
400
+ catch {
401
+ return { textLen: 0, scrollHeight: 0 };
402
+ }
403
+ }
404
+ /**
405
+ * Fully render a JS app before its content is read: a best-effort `networkidle`
406
+ * wait, then a bounded autoscroll + content-settle loop. Used on escalation
407
+ * re-fetches of a SPA shell (no customer waitFor) so we capture hydrated content
408
+ * instead of the empty mount point tier-0 returned.
409
+ *
410
+ * Hard-bounded so it can never hang: the loop stops
411
+ * at the FIRST of — the page looks rendered (>= FULL_RENDER_MIN_CHARS visible text
412
+ * AND the DOM stopped growing), `maxSteps` reached, or the `deadlineMs` passed.
413
+ * Every page call is best-effort (errors swallowed): a flaky settle must never
414
+ * fail an otherwise-good fetch. Fetcher-layer only — it issues no new outbound
415
+ * request of ours; sub-resource loads happen inside the already-proxied browser,
416
+ * so the SSRF posture is unchanged.
417
+ */
418
+ export async function fullyRenderPage(page, opts) {
419
+ const now = opts.nowMs ?? Date.now;
420
+ const maxSteps = opts.maxSteps ?? FULL_RENDER_MAX_STEPS;
421
+ const remaining = () => opts.deadlineMs - now();
422
+ // 1. Best-effort networkidle — the common "SPA finished its XHRs" signal.
423
+ // Capped under the request timeout; a page that never goes idle (sockets,
424
+ // polling) just times out here and we fall through to the scroll loop.
425
+ if (page.waitForLoadState && remaining() > 0) {
426
+ const timeout = Math.min(FULL_RENDER_NETWORKIDLE_MS, remaining());
427
+ await page.waitForLoadState('networkidle', { timeout }).catch(() => undefined);
428
+ }
429
+ // 2. Autoscroll + content settle. Scroll to the bottom to trigger lazy /
430
+ // infinite content, pause to let it paint, then re-measure. Stop once the
431
+ // page looks rendered AND the DOM has stopped growing (two equal heights).
432
+ let lastHeight = Number.NaN;
433
+ for (let step = 0; step < maxSteps && remaining() > 0; step++) {
434
+ await page
435
+ .evaluate('window.scrollTo(0, document.body ? document.body.scrollHeight : 0)')
436
+ .catch(() => undefined);
437
+ const pause = Math.min(FULL_RENDER_STEP_MS, Math.max(0, remaining()));
438
+ await page.waitForTimeout(pause).catch(() => undefined);
439
+ const metric = await readRenderMetric(page);
440
+ if (metric.textLen >= FULL_RENDER_MIN_CHARS && metric.scrollHeight === lastHeight)
441
+ break;
442
+ lastHeight = metric.scrollHeight;
443
+ }
444
+ }
445
+ /**
446
+ * Execute Phase F actions sequentially against an open page. Throws on
447
+ * the first failure with the action index included for easier debugging
448
+ * — the route surfaces this in the 502 response detail.
449
+ */
450
+ export async function runActions(page, actions) {
451
+ const results = { screenshots: [], scrapes: [], javascriptReturns: [] };
452
+ for (let i = 0; i < actions.length; i++) {
453
+ const a = actions[i];
454
+ try {
455
+ if (a.type === 'wait') {
456
+ if (typeof a.milliseconds === 'number' && a.milliseconds > 0) {
457
+ await page.waitForTimeout(a.milliseconds);
458
+ }
459
+ else if (a.selector) {
460
+ await page.waitForSelector(a.selector, { timeout: a.timeoutMs ?? 5_000 });
461
+ }
462
+ }
463
+ else if (a.type === 'click') {
464
+ await page.click(a.selector, a.timeoutMs ? { timeout: a.timeoutMs } : undefined);
465
+ }
466
+ else if (a.type === 'write') {
467
+ if (a.selector) {
468
+ await page.fill(a.selector, a.text);
469
+ }
470
+ else {
471
+ await page.keyboard.type(a.text);
472
+ }
473
+ }
474
+ else if (a.type === 'press') {
475
+ await page.keyboard.press(a.key);
476
+ }
477
+ else if (a.type === 'scroll') {
478
+ const sign = a.direction === 'up' ? -1 : 1;
479
+ // String form so TS doesn't see `window` in server-side code.
480
+ const expr = typeof a.amount === 'number' && a.amount > 0
481
+ ? `window.scrollBy(0, ${sign * a.amount})`
482
+ : `window.scrollBy(0, ${sign} * window.innerHeight)`;
483
+ await page.evaluate(expr);
484
+ }
485
+ else if (a.type === 'screenshot') {
486
+ const shot = await page.screenshot({ fullPage: a.fullPage ?? true });
487
+ results.screenshots.push(Buffer.from(shot).toString('base64'));
488
+ }
489
+ else if (a.type === 'scrape') {
490
+ results.scrapes.push({ html: await page.content() });
491
+ }
492
+ else if (a.type === 'executeJavascript') {
493
+ // Runs in the fetched page's context (not our server). Result must
494
+ // be JSON-serializable; Playwright enforces this.
495
+ results.javascriptReturns.push({ value: await page.evaluate(a.script) });
496
+ }
497
+ }
498
+ catch (err) {
499
+ const msg = err instanceof Error ? err.message : String(err);
500
+ throw new Error(`action[${i}] (${a.type}) failed: ${msg}`);
501
+ }
502
+ }
503
+ return results;
504
+ }
505
+ //# sourceMappingURL=fetcher-patchright.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetcher-patchright.js","sourceRoot":"","sources":["../src/fetcher-patchright.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Robots-gated fetcher — the central robots.txt choke point (see ROBOTS_POLICY.md).
3
+ *
4
+ * Wraps any FetcherAdapter so every route that fetches through it gets uniform
5
+ * robots handling (instead of the per-path drift the audit found). When
6
+ * robots-respect is enabled it:
7
+ * - refuses URLs the target's robots.txt disallows for our product token →
8
+ * throws `RobotsDisallowedError` (routes map it to 403 `robots_disallowed`);
9
+ * - paces same-host requests by `max(Crawl-delay, politeness floor)`.
10
+ *
11
+ * Authorized opt-out: when the request carries the internal
12
+ * `__robotsBypassAuthorized` flag (resolved UPSTREAM at the API layer from the
13
+ * customer's `ignoreRobots` + `robotsAttestation` AND an account capability), the
14
+ * Disallow check is skipped and the bypass is logged for audit. The gate never
15
+ * decides authorization itself — that's an API/control concern (four-layer rule).
16
+ *
17
+ * Phase-in: respect is OFF by default; enable per-environment via
18
+ * `ROBOTS_RESPECT=1` or the explicit `respectRobots` option. When off, the gate
19
+ * is a transparent pass-through (zero behavior change). robots.txt evaluation is
20
+ * fail-open (a target's robots outage never blocks the fetch) — see
21
+ * `evaluateRobots`. robots.txt URLs are SSRF-validated inside `evaluateRobots`;
22
+ * this gate adds no new trust-boundary crossing.
23
+ */
24
+ import { evaluateRobots } from '@bluefields/primitives';
25
+ import type { FetcherAdapter } from './types.js';
26
+ /** Audit record for an honored opt-out — the API layer adds customer/attestation context. */
27
+ export interface RobotsBypassAudit {
28
+ url: string;
29
+ host: string;
30
+ /** Wall-clock ms (Date.now) when the bypass was honored. */
31
+ atMs: number;
32
+ }
33
+ export interface RobotsGateOptions {
34
+ /** The wrapped fetcher (e.g. the escalating adapter). */
35
+ inner: FetcherAdapter;
36
+ /**
37
+ * Master switch (phase-in). Default `env.ROBOTS_RESPECT === '1'` (OFF until an
38
+ * operator enables it). When false the gate is a pass-through.
39
+ */
40
+ respectRobots?: boolean;
41
+ /** Minimum same-host request spacing. Default `DEFAULT_POLITENESS_FLOOR_MS`. */
42
+ politenessFloorMs?: number;
43
+ /** Env used for the default `respectRobots` flag. Default `process.env`. */
44
+ env?: NodeJS.ProcessEnv;
45
+ /** Invoked when an authorized opt-out is honored (for the API-level audit log). */
46
+ onBypass?: (audit: RobotsBypassAudit) => void;
47
+ /** robots.txt fetch override (forwarded to `evaluateRobots`). */
48
+ robotsFetchImpl?: typeof fetch;
49
+ /** Override the robots evaluator. */
50
+ evaluate?: typeof evaluateRobots;
51
+ /** Clock for politeness pacing. Default `Date.now`. */
52
+ nowMsFn?: () => number;
53
+ /** Sleep for politeness pacing. Default real `setTimeout`. */
54
+ sleepFn?: (ms: number) => Promise<void>;
55
+ }
56
+ /**
57
+ * Build a robots-gated fetcher wrapping `opts.inner`. Drop-in `FetcherAdapter`,
58
+ * so adopting it is just wrapping the fetcher a route already constructs.
59
+ */
60
+ export declare function createRobotsGatedFetcher(opts: RobotsGateOptions): FetcherAdapter;