@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,231 @@
1
+ /**
2
+ * Fetcher + storage adapter contracts (chunk 8).
3
+ *
4
+ * The fetcher returns raw HTML + metadata. The storage adapter persists
5
+ * the bytes content-addressed in R2 (Review #2). Both have an in-memory
6
+ * test implementation and an env-gated production implementation.
7
+ *
8
+ * Per the four-layer rule: the fetcher returns raw bytes +
9
+ * metadata, nothing more. It does NOT extract, diff, or notify.
10
+ */
11
+ /**
12
+ * What to wait for before considering the page "ready" for content
13
+ * extraction. Only honored by browser-based adapters (Patchright);
14
+ * HTTP adapter ignores this and returns as soon as the response body
15
+ * arrives.
16
+ */
17
+ export type WaitCondition =
18
+ /** Wait until `document.readyState === 'complete'` (default for browser). */
19
+ {
20
+ type: 'load';
21
+ timeoutMs?: number;
22
+ }
23
+ /** Wait until network is idle for 500ms (Patchright `networkidle`). */
24
+ | {
25
+ type: 'networkIdle';
26
+ timeoutMs?: number;
27
+ }
28
+ /** Wait for a CSS selector to appear in the DOM. */
29
+ | {
30
+ type: 'selector';
31
+ selector: string;
32
+ timeoutMs?: number;
33
+ }
34
+ /** Wait a fixed amount of time after navigation. */
35
+ | {
36
+ type: 'timeout';
37
+ ms: number;
38
+ };
39
+ export interface RequestCookie {
40
+ name: string;
41
+ value: string;
42
+ domain?: string;
43
+ path?: string;
44
+ expires?: number;
45
+ httpOnly?: boolean;
46
+ secure?: boolean;
47
+ sameSite?: 'Strict' | 'Lax' | 'None';
48
+ }
49
+ /**
50
+ * Steps run inside the browser between `goto` + `waitFor` and content
51
+ * extraction. Required for sites that need interaction (cookie banners,
52
+ * "load more" buttons, login forms, infinite scroll). Browser-only —
53
+ * the HTTP fetcher rejects when actions are present.
54
+ *
55
+ * Execution is sequential; one failure short-circuits and surfaces the
56
+ * action index in the error. Each action has its own timeoutMs where
57
+ * applicable; the overall request timeout still applies.
58
+ */
59
+ export type PageAction =
60
+ /** Sleep for `milliseconds`, or wait for `selector` to appear. */
61
+ {
62
+ type: 'wait';
63
+ milliseconds?: number;
64
+ selector?: string;
65
+ timeoutMs?: number;
66
+ }
67
+ /** Click a CSS selector. */
68
+ | {
69
+ type: 'click';
70
+ selector: string;
71
+ timeoutMs?: number;
72
+ }
73
+ /**
74
+ * Type `text` into the page. When `selector` is provided, fills that
75
+ * element directly (replaces its value); otherwise types into whatever
76
+ * is focused — pair with a prior `click` action.
77
+ */
78
+ | {
79
+ type: 'write';
80
+ text: string;
81
+ selector?: string;
82
+ }
83
+ /** Press a keyboard key — `Enter`, `Tab`, `Escape`, `ArrowDown`, etc. */
84
+ | {
85
+ type: 'press';
86
+ key: string;
87
+ }
88
+ /** Scroll the viewport. `amount` defaults to one viewport. */
89
+ | {
90
+ type: 'scroll';
91
+ direction: 'up' | 'down';
92
+ amount?: number;
93
+ }
94
+ /** Capture a screenshot at this point in the sequence. */
95
+ | {
96
+ type: 'screenshot';
97
+ fullPage?: boolean;
98
+ }
99
+ /** Capture the current page HTML at this point in the sequence. */
100
+ | {
101
+ type: 'scrape';
102
+ }
103
+ /** Run JavaScript in the page context and capture its return value. */
104
+ | {
105
+ type: 'executeJavascript';
106
+ script: string;
107
+ };
108
+ /**
109
+ * Results from result-yielding actions (screenshot, scrape,
110
+ * executeJavascript), collected in execution order. Arrays are empty when
111
+ * no such action ran. Surfaced to /scrape callers under `actions`.
112
+ */
113
+ export interface ActionResults {
114
+ /** Base64-encoded PNG bytes, one entry per `screenshot` action. */
115
+ screenshots: string[];
116
+ /** Page HTML captured by each `scrape` action. */
117
+ scrapes: {
118
+ html: string;
119
+ }[];
120
+ /** Return value of each `executeJavascript` action. */
121
+ javascriptReturns: {
122
+ value: unknown;
123
+ }[];
124
+ }
125
+ export interface FetchOptions {
126
+ /** Hard timeout (ms). Default 30s per Review #4. */
127
+ timeoutMs?: number;
128
+ /** User-Agent string to send. */
129
+ userAgent?: string;
130
+ /** Test seam — override Date.now() for `fetchedAt`. */
131
+ nowFn?: () => Date;
132
+ /**
133
+ * Wait condition before reading page content. Browser adapters honor
134
+ * this; HTTP adapter ignores (returns as soon as response body arrives).
135
+ * Default: `{ type: 'load' }` (browser) or N/A (HTTP).
136
+ */
137
+ waitFor?: WaitCondition;
138
+ /**
139
+ * Capture a screenshot alongside the HTML. Only browser adapters
140
+ * (Patchright) can do this — HTTP adapter returns `screenshot: null`.
141
+ */
142
+ captureScreenshot?: boolean;
143
+ /**
144
+ * Extra HTTP request headers — Authorization, custom UA overrides, etc.
145
+ * `user-agent` here takes precedence over the top-level `userAgent`.
146
+ */
147
+ headers?: Record<string, string>;
148
+ /**
149
+ * Cookies sent with the request (Phase G). Browser adapters apply via
150
+ * BrowserContext.addCookies; HTTP adapter serializes them into a
151
+ * `Cookie:` header (domain/path filtering matches the request URL).
152
+ */
153
+ cookies?: RequestCookie[];
154
+ /**
155
+ * Sequential interaction steps run between navigation and content
156
+ * extraction (Phase F). Browser-only — the HTTP fetcher throws when
157
+ * actions are present.
158
+ */
159
+ actions?: PageAction[];
160
+ }
161
+ export interface FetchResult {
162
+ /** Raw HTML response body. */
163
+ html: string;
164
+ responseStatus: number;
165
+ /** Map of lowercased response header name → value. */
166
+ responseHeaders: Record<string, string>;
167
+ fetchedAt: Date;
168
+ fetchDurationMs: number;
169
+ /** Identifies the fetcher implementation that produced this row. */
170
+ fetcherId: string;
171
+ /** Egress IP of the fetcher (proxy or local depending on adapter). */
172
+ proxyEgressIp: string;
173
+ userAgent: string;
174
+ rawHtmlSizeBytes: number;
175
+ /**
176
+ * Bytes actually transferred over the wire (sum of response
177
+ * Content-Length) — a proxy for metered-egress cost. Browser adapters
178
+ * that abort image/media/font downloads count only what they kept;
179
+ * aborted requests never produce a response, so they're excluded.
180
+ * Optional: adapters that don't measure it leave it undefined.
181
+ */
182
+ bytesTransferred?: number;
183
+ /**
184
+ * Full-page PNG screenshot bytes (browser adapters only when
185
+ * `captureScreenshot: true`). null otherwise.
186
+ */
187
+ screenshot: Buffer | null;
188
+ /**
189
+ * Final URL after redirects. Useful for /scrape to surface to
190
+ * customers when the requested URL bounced. Same as the input URL
191
+ * for non-redirecting fetches.
192
+ */
193
+ finalUrl: string;
194
+ /**
195
+ * Results from result-yielding page actions (screenshot, scrape,
196
+ * executeJavascript). Present only when such actions ran.
197
+ */
198
+ actionResults?: ActionResults;
199
+ }
200
+ export interface FetcherAdapter {
201
+ fetch(url: string, options?: FetchOptions): Promise<FetchResult>;
202
+ }
203
+ /**
204
+ * A fetcher whose lazy resources (e.g. the Patchright browser pool) can be eagerly
205
+ * initialized at boot so the FIRST customer fetch doesn't pay the setup cost (perf
206
+ * round, L4 — a cold Chromium launch is ~0.5–1.5s of pure tail). `warmUp` is optional
207
+ * and a no-op for adapters with nothing to warm; call it once at app boot,
208
+ * fire-and-forget. A plain `FetcherAdapter` consumer that never calls it is unaffected.
209
+ */
210
+ export interface WarmableFetcherAdapter extends FetcherAdapter {
211
+ warmUp?(): Promise<void>;
212
+ }
213
+ /** Output of `storeSnapshot`. */
214
+ export interface StoredSnapshot {
215
+ contentHash: string;
216
+ /** Object key — Review #2: `content/<first2-of-sha>/<sha256>`. */
217
+ r2Key: string;
218
+ }
219
+ export interface StorageAdapter {
220
+ /** Persist `content`; idempotent (same content → same key). */
221
+ storeSnapshot(content: string): Promise<StoredSnapshot>;
222
+ /** Retrieve previously-stored content by hash. Throws on miss. */
223
+ getSnapshot(contentHash: string): Promise<string>;
224
+ /** True if the content has already been stored. */
225
+ hasSnapshot(contentHash: string): Promise<boolean>;
226
+ }
227
+ /**
228
+ * Content-addressed key per Review #2: 2-char prefix shard for filesystem
229
+ * fan-out, then the full sha256 hex.
230
+ */
231
+ export declare function buildR2Key(contentHash: string): string;
package/dist/types.js ADDED
@@ -0,0 +1,22 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /**
3
+ * Fetcher + storage adapter contracts (chunk 8).
4
+ *
5
+ * The fetcher returns raw HTML + metadata. The storage adapter persists
6
+ * the bytes content-addressed in R2 (Review #2). Both have an in-memory
7
+ * test implementation and an env-gated production implementation.
8
+ *
9
+ * Per the four-layer rule: the fetcher returns raw bytes +
10
+ * metadata, nothing more. It does NOT extract, diff, or notify.
11
+ */
12
+ /**
13
+ * Content-addressed key per Review #2: 2-char prefix shard for filesystem
14
+ * fan-out, then the full sha256 hex.
15
+ */
16
+ export function buildR2Key(contentHash) {
17
+ if (!/^[0-9a-f]{64}$/.test(contentHash)) {
18
+ throw new Error(`contentHash must be a 64-char lowercase hex string, got ${contentHash}`);
19
+ }
20
+ return `content/${contentHash.slice(0, 2)}/${contentHash}`;
21
+ }
22
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C;;;;;;;;;GASG;AAuLH;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,WAAmB;IAC5C,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,2DAA2D,WAAW,EAAE,CAAC,CAAC;IAC5F,CAAC;IACD,OAAO,WAAW,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC;AAC7D,CAAC"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@bluefields/fetcher",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "dependencies": {
7
+ "drizzle-orm": "0.36.4",
8
+ "patchright": "^1.60.0",
9
+ "@bluefields/db": "0.2.0",
10
+ "@bluefields/primitives": "0.2.0",
11
+ "@bluefields/attest": "0.2.0"
12
+ },
13
+ "devDependencies": {
14
+ "@types/node": "22.9.0",
15
+ "vitest": "2.1.4"
16
+ },
17
+ "description": "Bluefield fetcher — tiered HTML capture (HTTP → headless browser) with SSRF guard and signed provenance.",
18
+ "license": "AGPL-3.0-or-later",
19
+ "types": "dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ }
25
+ },
26
+ "sideEffects": false,
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "keywords": [
36
+ "scraping",
37
+ "fetch",
38
+ "browser",
39
+ "bluefield"
40
+ ],
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/danielvhofmann/bluefield.git",
44
+ "directory": "packages/fetcher"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "scripts": {
50
+ "typecheck": "tsc --noEmit -p .",
51
+ "test": "vitest run",
52
+ "build": "tsc -p tsconfig.build.json"
53
+ }
54
+ }