@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,212 @@
1
+ /**
2
+ * Patchright fetcher adapter (chunks 8 + Phase A real wiring).
3
+ *
4
+ * Patchright is a Chromium fork with CDP-leak patches that defeat many
5
+ * naive bot-detection signals (navigator.webdriver, console.debug
6
+ * fingerprinting, etc.). API is Playwright-compatible — drop-in.
7
+ *
8
+ * Production wiring is gated on IPROYAL_* env vars: when they're set,
9
+ * the factory (`tryCreatePatchrightAdapter`) returns a real adapter
10
+ * that launches Chromium with the IPRoyal residential proxy. Without
11
+ * them, callers fall back to the HTTP adapter (`fetcher-http.ts`).
12
+ *
13
+ * Per-fetch lifecycle: launch fresh browser → newContext → newPage →
14
+ * goto → wait → content + (optional) screenshot → close. One browser
15
+ * per fetch keeps fingerprint state isolated; pooling can ship later
16
+ * if launch overhead becomes load-bearing (~500ms per fetch today).
17
+ *
18
+ * Fingerprint rotation: v1 uses a small pool of (UA, viewport, locale,
19
+ * timezone) tuples picked randomly per fetch. Full diversity (Review #4)
20
+ * comes later — we just need basic variation to defeat per-tuple
21
+ * fingerprint blocklists.
22
+ *
23
+ * Egress IP: deferred. IPRoyal residential proxies rotate per-session;
24
+ * resolving the actual egress IP requires an extra eval to api.ipify.org
25
+ * which we don't want to pay on every fetch. Logged as a placeholder for
26
+ * now; revisit in M14-17 when attestation needs it (Review #3 manifest).
27
+ */
28
+ import type { ActionResults, PageAction, RequestCookie, WarmableFetcherAdapter } from './types.js';
29
+ export declare const PATCHRIGHT_REQUIRED_ENV: readonly ["IPROYAL_GATEWAY"];
30
+ /** Subset of a Playwright Route we use to abort/continue intercepted requests. */
31
+ export interface PatchrightRoute {
32
+ request(): {
33
+ resourceType(): string;
34
+ };
35
+ abort(): Promise<void>;
36
+ continue(): Promise<void>;
37
+ }
38
+ /** Subset of a Playwright Response we read for transferred-byte accounting. */
39
+ export interface PatchrightResponse {
40
+ headers(): Record<string, string>;
41
+ }
42
+ /**
43
+ * Pure predicate: should the browser tier abort this request's download?
44
+ * Extracted so it's unit-tested without a live browser. The caller is
45
+ * responsible for skipping interception entirely when a screenshot is
46
+ * requested (a screenshot needs the full visual render).
47
+ */
48
+ export declare function shouldBlockResource(resourceType: string, opts?: {
49
+ blockStylesheet?: boolean;
50
+ }): boolean;
51
+ /**
52
+ * Subset of Playwright's Page surface we actually call. Kept narrow so
53
+ * the test mock stays small.
54
+ */
55
+ export interface PatchrightPage {
56
+ goto(url: string, opts: {
57
+ waitUntil: 'load' | 'networkidle' | 'domcontentloaded';
58
+ timeout: number;
59
+ }): Promise<{
60
+ status(): number;
61
+ headers(): Record<string, string>;
62
+ url(): string;
63
+ } | null>;
64
+ waitForSelector(selector: string, opts: {
65
+ timeout: number;
66
+ }): Promise<unknown>;
67
+ waitForTimeout(ms: number): Promise<void>;
68
+ /**
69
+ * Wait for a Playwright load state (we use 'networkidle'). Optional: the real
70
+ * Playwright Page always has it, but the test mock may omit it, and the
71
+ * full-render path calls it best-effort (skips when absent, swallows timeout).
72
+ */
73
+ waitForLoadState?(state: 'load' | 'domcontentloaded' | 'networkidle', opts?: {
74
+ timeout?: number;
75
+ }): Promise<void>;
76
+ content(): Promise<string>;
77
+ screenshot(opts: {
78
+ fullPage: boolean;
79
+ }): Promise<Buffer>;
80
+ url(): string;
81
+ /** Click a CSS selector (Phase F actions). */
82
+ click(selector: string, opts?: {
83
+ timeout?: number;
84
+ }): Promise<void>;
85
+ /** Fill a selector with text (Phase F actions). */
86
+ fill(selector: string, text: string): Promise<void>;
87
+ /** Keyboard namespace — type free-form text or press a single key. */
88
+ keyboard: {
89
+ type(text: string): Promise<void>;
90
+ press(key: string): Promise<void>;
91
+ };
92
+ /** Evaluate JS in the page context — used for scroll. */
93
+ evaluate(fn: string | ((arg?: unknown) => unknown), arg?: unknown): Promise<unknown>;
94
+ /**
95
+ * Intercept network requests (optional — real Playwright always has it; the
96
+ * test mock may omit it, so callers guard with a typeof check). We use it to
97
+ * abort image/media/font downloads we discard, cutting metered proxy egress.
98
+ */
99
+ route?(urlPattern: string, handler: (route: PatchrightRoute) => Promise<void> | void): Promise<void>;
100
+ /** Subscribe to page events (optional). We use `response` to sum bytes. */
101
+ on?(event: 'response', handler: (response: PatchrightResponse) => void): void;
102
+ }
103
+ /**
104
+ * Subset of a Playwright BrowserContext we use. `close()` is optional only so
105
+ * the narrow test mock can omit it; the adapter always calls it best-effort in
106
+ * `finally` so a pooled (reused) browser never accumulates one context per fetch.
107
+ */
108
+ export interface PatchrightBrowserContext {
109
+ addCookies(cookies: RequestCookie[]): Promise<void>;
110
+ newPage(): Promise<PatchrightPage>;
111
+ /** Close the context and all its pages. */
112
+ close?(): Promise<void>;
113
+ }
114
+ /**
115
+ * Minimal subset of patchright/playwright's chromium namespace that we
116
+ * actually use. Tests inject a mock that implements just these methods.
117
+ */
118
+ export interface ChromiumLauncher {
119
+ launch(opts: {
120
+ proxy: {
121
+ server: string;
122
+ username: string;
123
+ password: string;
124
+ };
125
+ headless?: boolean;
126
+ /**
127
+ * Chromium command-line args. We pass --no-sandbox in Docker because
128
+ * Chromium's setuid sandbox can't initialize when the process is
129
+ * root and seccomp isn't configured — the launch silently hangs
130
+ * otherwise.
131
+ */
132
+ args?: string[];
133
+ /** Hard timeout for the launch step. Default 30s in Playwright. */
134
+ timeout?: number;
135
+ }): Promise<{
136
+ newContext(opts: {
137
+ userAgent: string;
138
+ viewport: {
139
+ width: number;
140
+ height: number;
141
+ };
142
+ locale: string;
143
+ timezoneId: string;
144
+ extraHTTPHeaders?: Record<string, string>;
145
+ }): Promise<PatchrightBrowserContext>;
146
+ close(): Promise<void>;
147
+ }>;
148
+ }
149
+ export interface PatchrightAdapterConfig {
150
+ proxy: {
151
+ server: string;
152
+ username: string;
153
+ password: string;
154
+ };
155
+ /** Test seam: inject a mock chromium launcher. */
156
+ chromiumImpl?: ChromiumLauncher;
157
+ /** Test seam: deterministic random for fingerprint picking. */
158
+ rand?: () => number;
159
+ /** Override the default user agent (rare — fingerprint pool usually wins). */
160
+ defaultUserAgent?: string;
161
+ /**
162
+ * Number of pooled browsers to keep warm. When 0 (default), every fetch
163
+ * launches and closes a fresh browser (~500ms-1.5s overhead). When > 0,
164
+ * the first `pool` fetches each take the launch hit, subsequent fetches
165
+ * reuse a browser and only pay newContext (~30ms).
166
+ *
167
+ * Each browser still gets a fresh `newContext` per fetch, so reused
168
+ * browsers don't leak cookies / localStorage / fingerprint state across
169
+ * customers.
170
+ */
171
+ pool?: number;
172
+ }
173
+ /**
174
+ * Returns the env-gated Patchright adapter when the required env vars
175
+ * are set; null otherwise. Callers (the factory in `index.ts`) fall
176
+ * back to the HTTP adapter when this returns null.
177
+ */
178
+ export declare function tryCreatePatchrightAdapter(env?: NodeJS.ProcessEnv): WarmableFetcherAdapter | null;
179
+ /**
180
+ * Direct constructor — bypass the env gate. Useful in tests + when the
181
+ * caller has the proxy creds from somewhere other than env vars.
182
+ */
183
+ export declare function createPatchrightAdapter(config: PatchrightAdapterConfig): WarmableFetcherAdapter;
184
+ export interface FullRenderOptions {
185
+ /** Absolute wall-clock deadline (ms, Date.now-based). Full render never runs past it. */
186
+ deadlineMs: number;
187
+ /** Test seam: clock. Default Date.now. */
188
+ nowMs?: () => number;
189
+ /** Override the hard iteration cap (test seam). Default FULL_RENDER_MAX_STEPS. */
190
+ maxSteps?: number;
191
+ }
192
+ /**
193
+ * Fully render a JS app before its content is read: a best-effort `networkidle`
194
+ * wait, then a bounded autoscroll + content-settle loop. Used on escalation
195
+ * re-fetches of a SPA shell (no customer waitFor) so we capture hydrated content
196
+ * instead of the empty mount point tier-0 returned.
197
+ *
198
+ * Hard-bounded so it can never hang: the loop stops
199
+ * at the FIRST of — the page looks rendered (>= FULL_RENDER_MIN_CHARS visible text
200
+ * AND the DOM stopped growing), `maxSteps` reached, or the `deadlineMs` passed.
201
+ * Every page call is best-effort (errors swallowed): a flaky settle must never
202
+ * fail an otherwise-good fetch. Fetcher-layer only — it issues no new outbound
203
+ * request of ours; sub-resource loads happen inside the already-proxied browser,
204
+ * so the SSRF posture is unchanged.
205
+ */
206
+ export declare function fullyRenderPage(page: PatchrightPage, opts: FullRenderOptions): Promise<void>;
207
+ /**
208
+ * Execute Phase F actions sequentially against an open page. Throws on
209
+ * the first failure with the action index included for easier debugging
210
+ * — the route surfaces this in the 502 response detail.
211
+ */
212
+ export declare function runActions(page: PatchrightPage, actions: PageAction[]): Promise<ActionResults>;