@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,82 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * Manual bandwidth measurement (gated, NOT a test). Fetches a few image-heavy
4
+ * pages through the browser tier with asset-blocking ON (the normal path) vs
5
+ * OFF (a screenshot request disables blocking) and prints bytesTransferred for
6
+ * each, to confirm the metered-proxy saving is real.
7
+ *
8
+ * ASSET_BLOCK_MEASURE=1 packages/fetcher/node_modules/.bin/tsx \
9
+ * packages/fetcher/src/asset-block-measure.ts
10
+ * (source .env.local first for IPROYAL_*). Spends a little IPRoyal egress.
11
+ *
12
+ * Robots: a crude `Disallow: /` check for the `*` group skips a host that
13
+ * forbids it; we only target bot-permissive homepages anyway.
14
+ */
15
+ import { tryCreatePatchrightAdapter } from './fetcher-patchright.js';
16
+ const URLS = [
17
+ 'https://www.theverge.com/',
18
+ 'https://www.bbc.com/',
19
+ 'https://www.nationalgeographic.com/',
20
+ 'https://www.cnn.com/',
21
+ ];
22
+ const UA = 'BlueFields-Fetcher/1.0 (+https://bluefields.dev)';
23
+ async function rootAllowed(url) {
24
+ try {
25
+ const res = await fetch(`${new URL(url).origin}/robots.txt`, {
26
+ headers: { 'user-agent': UA },
27
+ signal: AbortSignal.timeout(8_000),
28
+ });
29
+ if (!res.ok)
30
+ return true;
31
+ const txt = (await res.text()).toLowerCase();
32
+ const star = txt.split(/user-agent:/).find((b) => b.trimStart().startsWith('*'));
33
+ return !(star && /\n\s*disallow:\s*\/\s*(\n|$)/.test(`\n${star}`));
34
+ }
35
+ catch {
36
+ return true;
37
+ }
38
+ }
39
+ const kb = (n) => n == null ? ' n/a' : `${Math.round(n / 1024)}KB`.padStart(8);
40
+ async function main() {
41
+ if (process.env.ASSET_BLOCK_MEASURE !== '1') {
42
+ console.log('gated: set ASSET_BLOCK_MEASURE=1 (live; spends a little IPRoyal egress).');
43
+ return;
44
+ }
45
+ const adapter = tryCreatePatchrightAdapter(process.env);
46
+ if (!adapter) {
47
+ console.log('no Patchright/IPRoyal env (IPROYAL_GATEWAY + creds) — cannot measure.');
48
+ return;
49
+ }
50
+ console.log(`${'url'.padEnd(38)} OFF(before) ON(after) saved`);
51
+ let totBefore = 0;
52
+ let totAfter = 0;
53
+ for (const url of URLS) {
54
+ if (!(await rootAllowed(url))) {
55
+ console.log(`${url.padEnd(38)} skip (robots)`);
56
+ continue;
57
+ }
58
+ try {
59
+ // before: a screenshot request disables blocking → full asset download
60
+ const before = await adapter.fetch(url, { timeoutMs: 45_000, captureScreenshot: true });
61
+ // after: normal browser tier → image/media/font/stylesheet aborted
62
+ const after = await adapter.fetch(url, { timeoutMs: 45_000 });
63
+ const b = before.bytesTransferred ?? 0;
64
+ const a = after.bytesTransferred ?? 0;
65
+ totBefore += b;
66
+ totAfter += a;
67
+ const pct = b ? Math.round((100 * (b - a)) / b) : 0;
68
+ console.log(`${url.padEnd(38)} ${kb(b)} ${kb(a)} ${kb(b - a)} (${pct}%)`);
69
+ }
70
+ catch (err) {
71
+ console.log(`${url.padEnd(38)} ERROR ${err.message.slice(0, 50)}`);
72
+ }
73
+ await new Promise((r) => setTimeout(r, 1_000));
74
+ }
75
+ const pct = totBefore ? Math.round((100 * (totBefore - totAfter)) / totBefore) : 0;
76
+ console.log(`${'TOTAL'.padEnd(38)} ${kb(totBefore)} ${kb(totAfter)} ${kb(totBefore - totAfter)} (${pct}%)`);
77
+ }
78
+ main().catch((e) => {
79
+ console.error('measure failed:', e);
80
+ process.exit(1);
81
+ });
82
+ //# sourceMappingURL=asset-block-measure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-block-measure.js","sourceRoot":"","sources":["../src/asset-block-measure.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Bounded Chromium browser pool for the Patchright adapter.
3
+ *
4
+ * Why: launching Chromium costs ~500ms-1.5s per fetch (Patchright + IPRoyal
5
+ * proxy + headless boot). At sub-1-rps load that's tolerable; at 5+ rps it
6
+ * doubles end-to-end latency. With a pool of N browsers, only the FIRST
7
+ * fetch eats the launch cost; subsequent fetches just newContext (~30ms).
8
+ *
9
+ * Isolation: every fetch still gets a fresh `browser.newContext()` (own
10
+ * cookies, storage, fingerprint slot) so reused browsers don't leak state.
11
+ * The cookie jar / localStorage live on the context, not the browser.
12
+ *
13
+ * Crash resilience: if any context-level op fails, the slot is *disposed*
14
+ * (browser killed) and replaced on next acquire. Browsers also age out
15
+ * after MAX_USES contexts to avoid memory creep from a long-lived headless
16
+ * Chromium.
17
+ *
18
+ * Fail-open: when a launch fails, acquire throws and the caller falls
19
+ * back to per-fetch launch (the adapter wraps this with try/catch).
20
+ */
21
+ import type { ChromiumLauncher } from './fetcher-patchright.js';
22
+ export interface BrowserPoolOptions {
23
+ launcher: ChromiumLauncher;
24
+ proxy: {
25
+ server: string;
26
+ username: string;
27
+ password: string;
28
+ };
29
+ /** Number of browsers held alive concurrently. Default 2. */
30
+ size?: number;
31
+ /** Max time to wait for a free slot before rejecting. Default 30s. */
32
+ acquireTimeoutMs?: number;
33
+ }
34
+ type LaunchedBrowser = Awaited<ReturnType<ChromiumLauncher['launch']>>;
35
+ interface Slot {
36
+ browser: LaunchedBrowser;
37
+ usesRemaining: number;
38
+ inUse: boolean;
39
+ }
40
+ export declare class BrowserPool {
41
+ private readonly opts;
42
+ private readonly slots;
43
+ private readonly waiters;
44
+ private stopped;
45
+ constructor(opts: BrowserPoolOptions);
46
+ /** Pre-launch all browsers. Safe to call multiple times. */
47
+ warm(): Promise<void>;
48
+ /**
49
+ * Acquire a browser. Caller MUST call release(slot) or dispose(slot)
50
+ * exactly once when done. Throws on acquire timeout or launch failure.
51
+ */
52
+ acquire(): Promise<Slot>;
53
+ /** Return a healthy slot to the pool. Decrements its remaining lifespan. */
54
+ release(slot: Slot): void;
55
+ /** Mark a slot as crashed; kill the browser + replace. */
56
+ dispose(slot: Slot): void;
57
+ /** Close all browsers. */
58
+ stop(): Promise<void>;
59
+ /** Visible for testing. */
60
+ get size(): number;
61
+ private handoff;
62
+ private launchOne;
63
+ private replaceSlot;
64
+ }
65
+ export {};
@@ -0,0 +1,161 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * Bounded Chromium browser pool for the Patchright adapter.
4
+ *
5
+ * Why: launching Chromium costs ~500ms-1.5s per fetch (Patchright + IPRoyal
6
+ * proxy + headless boot). At sub-1-rps load that's tolerable; at 5+ rps it
7
+ * doubles end-to-end latency. With a pool of N browsers, only the FIRST
8
+ * fetch eats the launch cost; subsequent fetches just newContext (~30ms).
9
+ *
10
+ * Isolation: every fetch still gets a fresh `browser.newContext()` (own
11
+ * cookies, storage, fingerprint slot) so reused browsers don't leak state.
12
+ * The cookie jar / localStorage live on the context, not the browser.
13
+ *
14
+ * Crash resilience: if any context-level op fails, the slot is *disposed*
15
+ * (browser killed) and replaced on next acquire. Browsers also age out
16
+ * after MAX_USES contexts to avoid memory creep from a long-lived headless
17
+ * Chromium.
18
+ *
19
+ * Fail-open: when a launch fails, acquire throws and the caller falls
20
+ * back to per-fetch launch (the adapter wraps this with try/catch).
21
+ */
22
+ import { createLayerLogger } from '@bluefields/primitives';
23
+ const log = createLayerLogger('fetcher');
24
+ /** Replace a browser after this many contexts to bound memory. */
25
+ const MAX_USES_PER_BROWSER = 200;
26
+ /** Hard ceiling on acquire wait time. */
27
+ const DEFAULT_ACQUIRE_TIMEOUT_MS = 30_000;
28
+ export class BrowserPool {
29
+ opts;
30
+ slots = [];
31
+ waiters = [];
32
+ stopped = false;
33
+ constructor(opts) {
34
+ this.opts = {
35
+ launcher: opts.launcher,
36
+ proxy: opts.proxy,
37
+ size: opts.size ?? 2,
38
+ acquireTimeoutMs: opts.acquireTimeoutMs ?? DEFAULT_ACQUIRE_TIMEOUT_MS,
39
+ };
40
+ }
41
+ /** Pre-launch all browsers. Safe to call multiple times. */
42
+ async warm() {
43
+ if (this.stopped)
44
+ return;
45
+ while (this.slots.length < this.opts.size) {
46
+ const browser = await this.launchOne();
47
+ this.slots.push({ browser, usesRemaining: MAX_USES_PER_BROWSER, inUse: false });
48
+ log.info({ outcome: 'ok', pool_size: this.slots.length }, 'browser-pool: warmed slot');
49
+ }
50
+ }
51
+ /**
52
+ * Acquire a browser. Caller MUST call release(slot) or dispose(slot)
53
+ * exactly once when done. Throws on acquire timeout or launch failure.
54
+ */
55
+ async acquire() {
56
+ if (this.stopped)
57
+ throw new Error('browser-pool: stopped');
58
+ const free = this.slots.find((s) => !s.inUse);
59
+ if (free) {
60
+ free.inUse = true;
61
+ return free;
62
+ }
63
+ if (this.slots.length < this.opts.size) {
64
+ const browser = await this.launchOne();
65
+ const slot = { browser, usesRemaining: MAX_USES_PER_BROWSER, inUse: true };
66
+ this.slots.push(slot);
67
+ return slot;
68
+ }
69
+ return new Promise((resolve, reject) => {
70
+ const timer = setTimeout(() => {
71
+ const idx = this.waiters.findIndex((w) => w.timer === timer);
72
+ if (idx >= 0)
73
+ this.waiters.splice(idx, 1);
74
+ reject(new Error(`browser-pool: acquire timeout after ${this.opts.acquireTimeoutMs}ms`));
75
+ }, this.opts.acquireTimeoutMs);
76
+ this.waiters.push({ resolve, reject, timer });
77
+ });
78
+ }
79
+ /** Return a healthy slot to the pool. Decrements its remaining lifespan. */
80
+ release(slot) {
81
+ slot.usesRemaining -= 1;
82
+ if (slot.usesRemaining <= 0) {
83
+ void this.replaceSlot(slot);
84
+ return;
85
+ }
86
+ this.handoff(slot);
87
+ }
88
+ /** Mark a slot as crashed; kill the browser + replace. */
89
+ dispose(slot) {
90
+ void this.replaceSlot(slot);
91
+ }
92
+ /** Close all browsers. */
93
+ async stop() {
94
+ this.stopped = true;
95
+ for (const w of this.waiters) {
96
+ clearTimeout(w.timer);
97
+ w.reject(new Error('browser-pool: stopping'));
98
+ }
99
+ this.waiters.length = 0;
100
+ const toClose = [...this.slots];
101
+ this.slots.length = 0;
102
+ await Promise.all(toClose.map((s) => s.browser.close().catch((err) => {
103
+ log.warn({ error: err instanceof Error ? err.message : String(err) }, 'browser-pool: close failed');
104
+ })));
105
+ }
106
+ /** Visible for testing. */
107
+ get size() {
108
+ return this.slots.length;
109
+ }
110
+ // ── private ────────────────────────────────────────────────────────────
111
+ handoff(slot) {
112
+ if (this.waiters.length === 0) {
113
+ slot.inUse = false;
114
+ return;
115
+ }
116
+ const next = this.waiters.shift();
117
+ clearTimeout(next.timer);
118
+ // Slot stays inUse; just transferred to the next caller.
119
+ next.resolve(slot);
120
+ }
121
+ async launchOne() {
122
+ const browser = await this.opts.launcher.launch({
123
+ proxy: this.opts.proxy,
124
+ headless: true,
125
+ args: ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu'],
126
+ timeout: 20_000,
127
+ });
128
+ return browser;
129
+ }
130
+ async replaceSlot(slot) {
131
+ const idx = this.slots.indexOf(slot);
132
+ if (idx < 0)
133
+ return;
134
+ this.slots.splice(idx, 1);
135
+ await slot.browser.close().catch((err) => {
136
+ log.warn({ error: err instanceof Error ? err.message : String(err) }, 'browser-pool: close-on-replace failed');
137
+ });
138
+ if (this.stopped)
139
+ return;
140
+ // Replace only if we have waiters or we're below target — otherwise
141
+ // let the next acquire lazily refill.
142
+ if (this.waiters.length > 0 || this.slots.length < this.opts.size) {
143
+ try {
144
+ const browser = await this.launchOne();
145
+ const fresh = { browser, usesRemaining: MAX_USES_PER_BROWSER, inUse: false };
146
+ this.slots.push(fresh);
147
+ this.handoff(fresh);
148
+ }
149
+ catch (err) {
150
+ log.warn({ error: err instanceof Error ? err.message : String(err) }, 'browser-pool: replacement launch failed');
151
+ // Reject the head waiter so the caller can fall back to per-fetch launch.
152
+ const w = this.waiters.shift();
153
+ if (w) {
154
+ clearTimeout(w.timer);
155
+ w.reject(err instanceof Error ? err : new Error(String(err)));
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+ //# sourceMappingURL=browser-pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-pool.js","sourceRoot":"","sources":["../src/browser-pool.ts"],"names":[],"mappings":"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"}
package/dist/cost.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Fetcher cost recording (chunk 9, Review #11).
3
+ *
4
+ * Per Review #11, every fetch writes a cost_records row. The cost_type
5
+ * discriminates the source ('fetch_tier1' for Patchright; 'fetch_tier0'
6
+ * later for HTTP-only Tier 0); the cost_subtype refines it (proxy
7
+ * provider name, or 'http_only' for no-proxy fetches).
8
+ *
9
+ * v1 uses a flat $0.0014 per Tier 1 fetch (Review #4 weighted average).
10
+ * Later chunks will refine this from actual proxy bytes + Patchright
11
+ * runtime once the production fetcher ships.
12
+ */
13
+ import { type DbClient } from '@bluefields/db';
14
+ /** Per-fetch cost in micro-USD (Review #4: ~$0.0014 weighted average). */
15
+ export declare const TIER1_FIXED_COST_MICRO_USD = 1400;
16
+ /** Compute-only row when a fetch is blocked (robots disallow). */
17
+ export declare const COMPUTE_BLOCKED_COST_MICRO_USD = 50;
18
+ export type FetchCostType = 'fetch_tier0' | 'fetch_tier1' | 'fetch_tier2' | 'fetch_tier3' | 'fetch_304' | 'proxy_bandwidth' | 'compute';
19
+ export interface RecordFetchCostArgs {
20
+ db: DbClient;
21
+ customerId: string;
22
+ subscriptionId: string | null;
23
+ urlCanonical: string;
24
+ costType?: FetchCostType;
25
+ /** Subtype refinement — e.g. proxy provider name, or 'http_only'. */
26
+ costSubtype?: string | null;
27
+ costMicroUsd?: number;
28
+ /** Units recorded for analytics (bytes, ms, etc.). */
29
+ units?: string | number | null;
30
+ }
31
+ /**
32
+ * Write a cost_records row for one fetch attempt.
33
+ *
34
+ * Defaults to Tier 1's flat cost with subtype='http_only' to reflect the
35
+ * v1 default fetcher. Override `costType` / `costSubtype` when you know
36
+ * (e.g. proxy_bandwidth row after a Patchright fetch, or 'fetch_304'
37
+ * when conditional GET returned 304 once chunk M8-10 wires that).
38
+ */
39
+ export declare function recordFetchCost(args: RecordFetchCostArgs): Promise<void>;
40
+ /**
41
+ * Write a near-zero `compute` cost_records row for a fetch that was
42
+ * blocked before reaching the wire (e.g. robots.txt disallow). We still
43
+ * record the row so the customer can see the request in usage history.
44
+ */
45
+ export declare function recordBlockedFetch(args: {
46
+ db: DbClient;
47
+ customerId: string;
48
+ subscriptionId: string | null;
49
+ urlCanonical: string;
50
+ reason: string;
51
+ }): Promise<void>;
package/dist/cost.js ADDED
@@ -0,0 +1,54 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * Fetcher cost recording (chunk 9, Review #11).
4
+ *
5
+ * Per Review #11, every fetch writes a cost_records row. The cost_type
6
+ * discriminates the source ('fetch_tier1' for Patchright; 'fetch_tier0'
7
+ * later for HTTP-only Tier 0); the cost_subtype refines it (proxy
8
+ * provider name, or 'http_only' for no-proxy fetches).
9
+ *
10
+ * v1 uses a flat $0.0014 per Tier 1 fetch (Review #4 weighted average).
11
+ * Later chunks will refine this from actual proxy bytes + Patchright
12
+ * runtime once the production fetcher ships.
13
+ */
14
+ import { costRecords } from '@bluefields/db';
15
+ /** Per-fetch cost in micro-USD (Review #4: ~$0.0014 weighted average). */
16
+ export const TIER1_FIXED_COST_MICRO_USD = 1_400;
17
+ /** Compute-only row when a fetch is blocked (robots disallow). */
18
+ export const COMPUTE_BLOCKED_COST_MICRO_USD = 50;
19
+ /**
20
+ * Write a cost_records row for one fetch attempt.
21
+ *
22
+ * Defaults to Tier 1's flat cost with subtype='http_only' to reflect the
23
+ * v1 default fetcher. Override `costType` / `costSubtype` when you know
24
+ * (e.g. proxy_bandwidth row after a Patchright fetch, or 'fetch_304'
25
+ * when conditional GET returned 304 once chunk M8-10 wires that).
26
+ */
27
+ export async function recordFetchCost(args) {
28
+ await args.db.insert(costRecords).values({
29
+ customerId: args.customerId,
30
+ subscriptionId: args.subscriptionId,
31
+ urlCanonical: args.urlCanonical,
32
+ costType: args.costType ?? 'fetch_tier1',
33
+ costSubtype: args.costSubtype ?? 'http_only',
34
+ costMicroUsd: args.costMicroUsd ?? TIER1_FIXED_COST_MICRO_USD,
35
+ units: args.units !== undefined ? String(args.units) : null,
36
+ });
37
+ }
38
+ /**
39
+ * Write a near-zero `compute` cost_records row for a fetch that was
40
+ * blocked before reaching the wire (e.g. robots.txt disallow). We still
41
+ * record the row so the customer can see the request in usage history.
42
+ */
43
+ export async function recordBlockedFetch(args) {
44
+ await args.db.insert(costRecords).values({
45
+ customerId: args.customerId,
46
+ subscriptionId: args.subscriptionId,
47
+ urlCanonical: args.urlCanonical,
48
+ costType: 'compute',
49
+ costSubtype: args.reason,
50
+ costMicroUsd: COMPUTE_BLOCKED_COST_MICRO_USD,
51
+ units: null,
52
+ });
53
+ }
54
+ //# sourceMappingURL=cost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cost.js","sourceRoot":"","sources":["../src/cost.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C;;;;;;;;;;;GAWG;AAEH,OAAO,EAAiB,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE5D,0EAA0E;AAC1E,MAAM,CAAC,MAAM,0BAA0B,GAAG,KAAK,CAAC;AAEhD,kEAAkE;AAClE,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,CAAC;AAwBjD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAyB;IAC7D,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC;QACvC,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,aAAa;QACxC,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,WAAW;QAC5C,YAAY,EAAE,IAAI,CAAC,YAAY,IAAI,0BAA0B;QAC7D,KAAK,EAAE,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI;KAC5D,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,IAMxC;IACC,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC;QACvC,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,QAAQ,EAAE,SAAS;QACnB,WAAW,EAAE,IAAI,CAAC,MAAM;QACxB,YAAY,EAAE,8BAA8B;QAC5C,KAAK,EAAE,IAAI;KACZ,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Bright Data Web Unlocker fetcher adapter (Tier 2).
3
+ *
4
+ * Tier 2 of the coverage ladder: a managed anti-bot "unlocker" endpoint
5
+ * that solves challenges (Cloudflare, PerimeterX, DataDome, …) server-side
6
+ * and returns the final HTML. Roughly 10-20x the per-request cost of the
7
+ * Tier-1 browser path, so it is escalation-only — Tier 0 (HTTP) and Tier 1
8
+ * (Patchright + residential proxy) run first; we only reach for the
9
+ * unlocker when a page is STILL shell/challenge/blocked.
10
+ *
11
+ * Env-gated exactly like the Patchright adapter: without BRIGHT_DATA_TOKEN
12
+ * + BRIGHT_DATA_ZONE, `tryCreateBrightDataAdapter` returns null and callers
13
+ * stay on the cheaper tiers.
14
+ *
15
+ * Fetch-only by design (v1): no page actions, no screenshots. The
16
+ * unlocker's /request endpoint returns the final HTML body; it does not
17
+ * expose the interactive page surface that actions/screenshots need.
18
+ * Callers passing `actions` get an explicit rejection (mirrors the HTTP
19
+ * adapter's stance on browser-only features). The unlocker also manages
20
+ * its own UA / fingerprint, so a caller-supplied `userAgent` does not reach
21
+ * the target — we report a managed-UA sentinel rather than pretend.
22
+ *
23
+ * Security: the TARGET url is SSRF-validated (HTTPS-only, no internal /
24
+ * metadata hosts) before we hand it to the unlocker — the managed egress
25
+ * must not become an open SSRF relay. The POST to api.brightdata.com itself
26
+ * goes through ssrfSafeFetch like every other external call.
27
+ */
28
+ import type { FetcherAdapter } from './types.js';
29
+ export declare const BRIGHT_DATA_REQUIRED_ENV: readonly ["BRIGHT_DATA_TOKEN", "BRIGHT_DATA_ZONE"];
30
+ export interface BrightDataAdapterConfig {
31
+ /** Bright Data API token, sent as `Authorization: Bearer <token>`. */
32
+ token: string;
33
+ /** Web Unlocker zone name. */
34
+ zone: string;
35
+ /** Override the API endpoint (default https://api.brightdata.com/request). */
36
+ endpoint?: string;
37
+ /** Test seam: override the HTTP fetch. */
38
+ fetchImpl?: typeof fetch;
39
+ /** Hard cap (bytes) on the response body. Default `DEFAULT_MAX_BODY_BYTES` (5 MiB). */
40
+ maxBodyBytes?: number;
41
+ }
42
+ /**
43
+ * Returns the env-gated Bright Data adapter when BRIGHT_DATA_TOKEN +
44
+ * BRIGHT_DATA_ZONE are both set; null otherwise. Callers (the coverage
45
+ * runner) fall back to the lower tiers when this returns null.
46
+ */
47
+ export declare function tryCreateBrightDataAdapter(env?: NodeJS.ProcessEnv): FetcherAdapter | null;
48
+ /**
49
+ * Direct constructor — bypasses the env gate. Used by the factory above and
50
+ * by tests (which inject `fetchImpl`).
51
+ */
52
+ export declare function createBrightDataAdapter(config: BrightDataAdapterConfig): FetcherAdapter;
@@ -0,0 +1,135 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * Bright Data Web Unlocker fetcher adapter (Tier 2).
4
+ *
5
+ * Tier 2 of the coverage ladder: a managed anti-bot "unlocker" endpoint
6
+ * that solves challenges (Cloudflare, PerimeterX, DataDome, …) server-side
7
+ * and returns the final HTML. Roughly 10-20x the per-request cost of the
8
+ * Tier-1 browser path, so it is escalation-only — Tier 0 (HTTP) and Tier 1
9
+ * (Patchright + residential proxy) run first; we only reach for the
10
+ * unlocker when a page is STILL shell/challenge/blocked.
11
+ *
12
+ * Env-gated exactly like the Patchright adapter: without BRIGHT_DATA_TOKEN
13
+ * + BRIGHT_DATA_ZONE, `tryCreateBrightDataAdapter` returns null and callers
14
+ * stay on the cheaper tiers.
15
+ *
16
+ * Fetch-only by design (v1): no page actions, no screenshots. The
17
+ * unlocker's /request endpoint returns the final HTML body; it does not
18
+ * expose the interactive page surface that actions/screenshots need.
19
+ * Callers passing `actions` get an explicit rejection (mirrors the HTTP
20
+ * adapter's stance on browser-only features). The unlocker also manages
21
+ * its own UA / fingerprint, so a caller-supplied `userAgent` does not reach
22
+ * the target — we report a managed-UA sentinel rather than pretend.
23
+ *
24
+ * Security: the TARGET url is SSRF-validated (HTTPS-only, no internal /
25
+ * metadata hosts) before we hand it to the unlocker — the managed egress
26
+ * must not become an open SSRF relay. The POST to api.brightdata.com itself
27
+ * goes through ssrfSafeFetch like every other external call.
28
+ */
29
+ import { envInt, ssrfSafeFetch, validateUrlForExternalRequest } from '@bluefields/primitives';
30
+ import { fetchWithRetry, readBodyCapped } from './fetcher-http.js';
31
+ // The unlocker may sit on a challenge for tens of seconds while it solves
32
+ // it server-side; 30s (the HTTP default) is too tight. Callers can still
33
+ // override via options.timeoutMs (the coverage runner passes a longer one).
34
+ const DEFAULT_TIMEOUT_MS = envInt('BLUEFIELDS_UNLOCKER_TIMEOUT_MS', 60_000);
35
+ const DEFAULT_ENDPOINT = 'https://api.brightdata.com/request';
36
+ const FETCHER_ID = 'brightdata-v1';
37
+ /** The unlocker fetches from its own managed egress; we never see that IP. */
38
+ const MANAGED_EGRESS_PLACEHOLDER_IP = '0.0.0.0';
39
+ /** The unlocker picks + rotates its own UA; the caller's never reaches the target. */
40
+ const MANAGED_UA = 'brightdata-web-unlocker (managed fingerprint)';
41
+ /** Hard cap on the unlocker response body buffered into memory (mirrors tier-0). */
42
+ const DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024; // 5 MiB
43
+ export const BRIGHT_DATA_REQUIRED_ENV = ['BRIGHT_DATA_TOKEN', 'BRIGHT_DATA_ZONE'];
44
+ /**
45
+ * Returns the env-gated Bright Data adapter when BRIGHT_DATA_TOKEN +
46
+ * BRIGHT_DATA_ZONE are both set; null otherwise. Callers (the coverage
47
+ * runner) fall back to the lower tiers when this returns null.
48
+ */
49
+ export function tryCreateBrightDataAdapter(env = process.env) {
50
+ for (const key of BRIGHT_DATA_REQUIRED_ENV) {
51
+ if (!env[key] || env[key]?.length === 0)
52
+ return null;
53
+ }
54
+ return createBrightDataAdapter({
55
+ token: env.BRIGHT_DATA_TOKEN ?? '',
56
+ zone: env.BRIGHT_DATA_ZONE ?? '',
57
+ ...(env.BRIGHT_DATA_ENDPOINT ? { endpoint: env.BRIGHT_DATA_ENDPOINT } : {}),
58
+ });
59
+ }
60
+ /**
61
+ * Direct constructor — bypasses the env gate. Used by the factory above and
62
+ * by tests (which inject `fetchImpl`).
63
+ */
64
+ export function createBrightDataAdapter(config) {
65
+ const endpoint = config.endpoint ?? DEFAULT_ENDPOINT;
66
+ // Tests inject config.fetchImpl; production uses ssrfSafeFetch (undici's
67
+ // fetch with the SSRF-safe dispatcher), which also validates that
68
+ // api.brightdata.com resolves to a public IP.
69
+ const fetchImpl = (config.fetchImpl ?? ssrfSafeFetch);
70
+ const maxBodyBytes = config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
71
+ return {
72
+ async fetch(url, options = {}) {
73
+ if (options.actions && options.actions.length > 0) {
74
+ throw new Error('Bright Data unlocker is fetch-only; page actions require the Patchright fetcher');
75
+ }
76
+ // SSRF-validate the TARGET (not just our vendor endpoint) so the
77
+ // managed egress can't be steered at internal / metadata hosts.
78
+ await validateUrlForExternalRequest(url);
79
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
80
+ const now = options.nowFn ?? (() => new Date());
81
+ const controller = new AbortController();
82
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
83
+ const startedAt = Date.now();
84
+ const fetchedAt = now();
85
+ // `format: 'raw'` → the response body IS the target's HTML. Bright Data
86
+ // proxies the upstream status through res.status (a still-blocked or
87
+ // unsolvable page surfaces as a non-2xx, which the block-detector maps
88
+ // honestly rather than counting as success).
89
+ const requestBody = JSON.stringify({ zone: config.zone, url, format: 'raw' });
90
+ let res;
91
+ try {
92
+ res = await fetchWithRetry(fetchImpl, endpoint, {
93
+ method: 'POST',
94
+ headers: {
95
+ authorization: `Bearer ${config.token}`,
96
+ 'content-type': 'application/json',
97
+ },
98
+ body: requestBody,
99
+ signal: controller.signal,
100
+ redirect: 'follow',
101
+ });
102
+ }
103
+ finally {
104
+ clearTimeout(timeout);
105
+ }
106
+ // Bound the body read by the request deadline (the unlocker can stream a
107
+ // large solved page slowly; res.text() after clearTimeout was unbounded).
108
+ const html = await readBodyCapped(res, maxBodyBytes, { deadlineMs: startedAt + timeoutMs });
109
+ const fetchDurationMs = Math.max(1, Date.now() - startedAt);
110
+ const responseHeaders = {};
111
+ res.headers.forEach((value, key) => {
112
+ responseHeaders[key.toLowerCase()] = value;
113
+ });
114
+ return {
115
+ html,
116
+ responseStatus: res.status,
117
+ responseHeaders,
118
+ fetchedAt,
119
+ fetchDurationMs,
120
+ fetcherId: FETCHER_ID,
121
+ // Managed egress: the real exit IP belongs to Bright Data and isn't
122
+ // exposed to us, so we report the same placeholder as the no-proxy path.
123
+ proxyEgressIp: MANAGED_EGRESS_PLACEHOLDER_IP,
124
+ userAgent: MANAGED_UA,
125
+ rawHtmlSizeBytes: Buffer.byteLength(html, 'utf8'),
126
+ // Fetch-only: the unlocker can't render-and-screenshot in this mode.
127
+ screenshot: null,
128
+ // Raw mode doesn't return a trustworthy post-redirect URL header;
129
+ // report the requested URL.
130
+ finalUrl: url,
131
+ };
132
+ },
133
+ };
134
+ }
135
+ //# sourceMappingURL=fetcher-brightdata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetcher-brightdata.js","sourceRoot":"","sources":["../src/fetcher-brightdata.ts"],"names":[],"mappings":"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"}