@ultimat3/scraping 21.0.0 → 22.1.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.
- package/README.md +9 -0
- package/package.json +5 -5
- package/src/cdp-arm.ts +5 -2
- package/src/hosts.ts +4 -70
- package/src/index.ts +13 -38
package/README.md
CHANGED
|
@@ -298,6 +298,15 @@ Every code carries a cause, a runnable `fix:` and a retry classification — see
|
|
|
298
298
|
`X_SCRAPE_YIELD_COLLAPSED`, `X_SCRAPE_AUTH_FAILED`, `X_SCRAPE_BLOCKED` and `X_SCRAPE_PAGE_CRASHED`
|
|
299
299
|
are the four worth knowing by heart.
|
|
300
300
|
|
|
301
|
+
### Error classes
|
|
302
|
+
|
|
303
|
+
Every error class `src/index.ts` exports, for `instanceof` inside one process. Across a wire or
|
|
304
|
+
a job boundary the class is gone and the `code` is what survives — match on that.
|
|
305
|
+
|
|
306
|
+
| Class | Code | Declared in |
|
|
307
|
+
|---|---|---|
|
|
308
|
+
| `ScrapeError` | any `ScrapeErrorCode` — `SCRAPE_ERROR_CODES` | `src/errors.ts` |
|
|
309
|
+
|
|
301
310
|
## Boundary
|
|
302
311
|
|
|
303
312
|
Tier 5. May import tiers 0-4 only — enforced by `bun run scripts/boundaries.ts`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/scraping",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "22.1.0",
|
|
4
4
|
"description": "Browser automation as a job: scrape() returns a JobHandle",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"test": "bun test"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@ultimat3/core": "
|
|
34
|
-
"@ultimat3/jobs": "
|
|
35
|
-
"@ultimat3/schema": "
|
|
36
|
-
"@ultimat3/storage": "
|
|
33
|
+
"@ultimat3/core": "22.1.0",
|
|
34
|
+
"@ultimat3/jobs": "22.1.0",
|
|
35
|
+
"@ultimat3/schema": "22.1.0",
|
|
36
|
+
"@ultimat3/storage": "22.1.0"
|
|
37
37
|
}
|
|
38
38
|
}
|
package/src/cdp-arm.ts
CHANGED
|
@@ -135,11 +135,14 @@ export async function arm(init: CdpArmInit, sinks: CdpSinks): Promise<void> {
|
|
|
135
135
|
const at = init.clock.now().getTime();
|
|
136
136
|
if (verdict === 'allow') {
|
|
137
137
|
network.push({ method, url, resourceType: type, at });
|
|
138
|
-
|
|
138
|
+
// Caught, never floated: both reject when the target closed mid-request (and on any other
|
|
139
|
+
// protocol error), and a floated rejection is one Bun ends the process on. The request is
|
|
140
|
+
// already recorded; there is nothing left to do for a target that is gone.
|
|
141
|
+
request.continue().catch(() => undefined);
|
|
139
142
|
return;
|
|
140
143
|
}
|
|
141
144
|
network.push(refusalEntry(url, type, verdict, at, method));
|
|
142
|
-
|
|
145
|
+
request.abort().catch(() => undefined);
|
|
143
146
|
});
|
|
144
147
|
init.page.on('console', (payload) => {
|
|
145
148
|
console_.push({
|
package/src/hosts.ts
CHANGED
|
@@ -1,70 +1,4 @@
|
|
|
1
|
-
// `allowHosts
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export type HostRule = string;
|
|
7
|
-
|
|
8
|
-
/** The one spelling that means "every host", written out so it is visible in review. */
|
|
9
|
-
export const ANY_HOST: HostRule = '*';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Schemes with no host to match. `about:blank` is where every browser starts, `data:` and `blob:`
|
|
13
|
-
* never leave the process — refusing them would refuse the first page load of every run.
|
|
14
|
-
*/
|
|
15
|
-
const HOSTLESS_SCHEMES = new Set(['about:', 'data:', 'blob:']);
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Hostless AND refused, which is why it is not on the line above — it sat there until 2026-09.
|
|
19
|
-
* `javascript:` has no host for the same reason `data:` has none, and that is the whole
|
|
20
|
-
* resemblance: the other three are inert content this process renders, while this one is code
|
|
21
|
-
* EXECUTED in the current document's origin, with the session's cookies and the session's
|
|
22
|
-
* `localStorage` already in scope. `allowHosts` cannot say anything about a URL with no host to
|
|
23
|
-
* name, so "no host, therefore allowed" was the allow list opting itself out of the one navigation
|
|
24
|
-
* that needs no host to exfiltrate through — `javascript:fetch('/admin').then(post_elsewhere)` is
|
|
25
|
-
* a same-origin read on an allow-listed site. Fail closed; there is no legitimate scrape verb that
|
|
26
|
-
* needs it (`page.eval` is the declared seam).
|
|
27
|
-
*/
|
|
28
|
-
const REFUSED_SCHEMES = new Set(['javascript:']);
|
|
29
|
-
|
|
30
|
-
export interface HostDecision {
|
|
31
|
-
readonly allowed: boolean;
|
|
32
|
-
/** The host the URL resolved to, `''` for a hostless scheme. */
|
|
33
|
-
readonly host: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* `example.com` matches that host EXACTLY. `*.example.com` matches any subdomain and NOT the
|
|
38
|
-
* apex — the two are written separately on purpose: an allow list that silently included every
|
|
39
|
-
* subdomain would let a `cdn-user-content.example.com` (whose contents somebody else controls)
|
|
40
|
-
* through a rule an author wrote for the apex.
|
|
41
|
-
*/
|
|
42
|
-
export function hostMatches(host: string, rule: HostRule): boolean {
|
|
43
|
-
if (rule === ANY_HOST) return true;
|
|
44
|
-
const normalised = host.toLowerCase();
|
|
45
|
-
const cleaned = rule.trim().toLowerCase();
|
|
46
|
-
if (cleaned.startsWith('*.')) {
|
|
47
|
-
const suffix = cleaned.slice(1);
|
|
48
|
-
return normalised.endsWith(suffix) && normalised.length > suffix.length;
|
|
49
|
-
}
|
|
50
|
-
return normalised === cleaned;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Fails CLOSED: a URL that cannot be parsed is refused. A driver handed a malformed request has
|
|
55
|
-
* no way to know where it would have gone, and "we could not tell, so we let it through" is the
|
|
56
|
-
* decision that makes the whole list advisory.
|
|
57
|
-
*/
|
|
58
|
-
export function hostDecision(url: string, allowHosts: readonly HostRule[]): HostDecision {
|
|
59
|
-
const scheme = url.slice(0, Math.max(0, url.indexOf(':') + 1)).toLowerCase();
|
|
60
|
-
if (REFUSED_SCHEMES.has(scheme)) return { allowed: false, host: '' };
|
|
61
|
-
if (HOSTLESS_SCHEMES.has(scheme)) return { allowed: true, host: '' };
|
|
62
|
-
let host: string;
|
|
63
|
-
try {
|
|
64
|
-
host = new URL(url).hostname;
|
|
65
|
-
} catch {
|
|
66
|
-
return { allowed: false, host: '' };
|
|
67
|
-
}
|
|
68
|
-
if (host === '') return { allowed: false, host };
|
|
69
|
-
return { allowed: allowHosts.some((rule) => hostMatches(host, rule)), host };
|
|
70
|
-
}
|
|
1
|
+
// `allowHosts` matching is `@ultimat3/core`'s (`host-rules.ts`), shared with `x shot`'s browser;
|
|
2
|
+
// re-exported here so this package's own modules and its public names are unchanged.
|
|
3
|
+
export type { HostDecision, HostRule } from '@ultimat3/core';
|
|
4
|
+
export { ANY_HOST, hostDecision, hostMatches } from '@ultimat3/core';
|
package/src/index.ts
CHANGED
|
@@ -3,12 +3,10 @@
|
|
|
3
3
|
// the seam is not a seam.
|
|
4
4
|
|
|
5
5
|
export type { ActionabilityState, ActionabilityWait } from './actionability';
|
|
6
|
-
export {
|
|
6
|
+
export { awaitActionable } from './actionability';
|
|
7
7
|
export type { ArtifactRef, ArtifactWriter, ArtifactWriterInit } from './artifacts';
|
|
8
8
|
export {
|
|
9
|
-
contentTypeFor,
|
|
10
9
|
createArtifactWriter,
|
|
11
|
-
DEFAULT_ARTIFACT_PREFIX,
|
|
12
10
|
DEFAULT_CONTENT_TYPE,
|
|
13
11
|
} from './artifacts';
|
|
14
12
|
export type {
|
|
@@ -21,7 +19,6 @@ export { burnSession, createPrompt, ensureAuthenticated, restorableSession } fro
|
|
|
21
19
|
export { browserRecord } from './browser-record';
|
|
22
20
|
export type { CaptureClip, CaptureFraming } from './capture-clip';
|
|
23
21
|
export { assertCaptureFraming } from './capture-clip';
|
|
24
|
-
export { axNodesFor } from './cdp-a11y';
|
|
25
22
|
export type {
|
|
26
23
|
CdpBrowserLike,
|
|
27
24
|
CdpFrameLike,
|
|
@@ -32,24 +29,14 @@ export type {
|
|
|
32
29
|
CdpScreenshotOptions,
|
|
33
30
|
CdpSessionLike,
|
|
34
31
|
} from './cdp-port';
|
|
35
|
-
export {
|
|
36
|
-
clearExpression,
|
|
37
|
-
focusExpression,
|
|
38
|
-
parseSnapshots,
|
|
39
|
-
snapshotExpression,
|
|
40
|
-
} from './cdp-snapshot';
|
|
32
|
+
export { snapshotExpression } from './cdp-snapshot';
|
|
41
33
|
export type { CdpTargetInit } from './cdp-target';
|
|
42
34
|
export { CDP_DRIVER, cdpTarget } from './cdp-target';
|
|
43
35
|
export type { Deadline, ScrapeClock, TestScrapeClock } from './clock';
|
|
44
36
|
export { deadline, systemScrapeClock, testClock, throwIfAborted } from './clock';
|
|
45
37
|
export type { ColorScheme } from './color-scheme';
|
|
46
|
-
export {
|
|
47
|
-
export {
|
|
48
|
-
cookieDomainMatches,
|
|
49
|
-
cookieHeaderFor,
|
|
50
|
-
cookiePathMatches,
|
|
51
|
-
cookiesForUrl,
|
|
52
|
-
} from './cookie-scope';
|
|
38
|
+
export { COLOR_SCHEMES, isColorScheme } from './color-scheme';
|
|
39
|
+
export { cookieHeaderFor } from './cookie-scope';
|
|
53
40
|
export type { ScrapeDriver, ScrapeSession, SessionInit } from './driver';
|
|
54
41
|
export { resetScrapeDriver, scrapeDriver, setScrapeDriver } from './driver';
|
|
55
42
|
export type { BrowserOptions, LocalBrowserOptions, RemoteBrowserOptions } from './driver-cdp';
|
|
@@ -57,7 +44,7 @@ export { localBrowser, remoteBrowser } from './driver-cdp';
|
|
|
57
44
|
export type { FakeBrowserOptions, FakePageOptions, FakePages } from './driver-fake';
|
|
58
45
|
export { FAKE_DRIVER, FAKE_PAGE_URL, fakeBrowser, fakePage, recordingsOf } from './driver-fake';
|
|
59
46
|
export type { FixtureBrowserOptions } from './driver-fixture';
|
|
60
|
-
export { FIXTURE_DRIVER, fixtureBrowser
|
|
47
|
+
export { FIXTURE_DRIVER, fixtureBrowser } from './driver-fixture';
|
|
61
48
|
export {
|
|
62
49
|
authFailed,
|
|
63
50
|
blocked,
|
|
@@ -100,32 +87,28 @@ export {
|
|
|
100
87
|
ScrapeError,
|
|
101
88
|
} from './errors';
|
|
102
89
|
export type { ScrapeEventFields, StepEvent } from './events';
|
|
103
|
-
export { scrapeLogger, withStepEvent } from './events';
|
|
104
90
|
export type { YieldCheck, YieldExpectation, YieldGuardInput, YieldHistory } from './expect';
|
|
105
91
|
export {
|
|
106
|
-
DEFAULT_YIELD_WINDOW,
|
|
107
92
|
guardYield,
|
|
108
93
|
MIN_BASELINE_RUNS,
|
|
109
94
|
median,
|
|
110
95
|
memoryYieldHistory,
|
|
111
|
-
yieldProblem,
|
|
112
96
|
} from './expect';
|
|
113
|
-
export { BURNS_SESSION,
|
|
97
|
+
export { BURNS_SESSION, errorCode, NEVER_RETRIED } from './failures';
|
|
114
98
|
export type { HostDecision, HostRule } from './hosts';
|
|
115
99
|
export { ANY_HOST, hostDecision, hostMatches } from './hosts';
|
|
116
|
-
export {
|
|
100
|
+
export { queryHtml } from './html-query';
|
|
117
101
|
export type { MarkupRequest } from './html-requests';
|
|
118
|
-
export { markupRequests } from './html-requests';
|
|
119
102
|
export type { HtmlTargetInit, RecordingLookup } from './html-target';
|
|
120
103
|
export { htmlTarget } from './html-target';
|
|
121
104
|
export type { HttpRequestInit, HttpTransportInit, ScrapeHttp, ScrapeResponse } from './http';
|
|
122
105
|
export { DEFAULT_HTTP_MAX_BYTES, httpOverFetch, responseOver } from './http';
|
|
123
106
|
export type { HttpRecordingLookup, RecordedHttpInit } from './http-recorded';
|
|
124
|
-
export {
|
|
107
|
+
export { httpRecordingsOf, recordedHttp } from './http-recorded';
|
|
125
108
|
export type { RedirectHop } from './http-redirect';
|
|
126
|
-
export { MAX_REDIRECT_HOPS
|
|
109
|
+
export { MAX_REDIRECT_HOPS } from './http-redirect';
|
|
127
110
|
export type { InterceptRules, InterceptVerdict } from './intercept';
|
|
128
|
-
export { interceptVerdict
|
|
111
|
+
export { interceptVerdict } from './intercept';
|
|
129
112
|
export type { KeyChord, KeyModifier } from './key-chord';
|
|
130
113
|
export { KEY_MODIFIERS, parseKeyChord } from './key-chord';
|
|
131
114
|
export type { OfflineSessionInit } from './offline-session';
|
|
@@ -139,18 +122,16 @@ export type {
|
|
|
139
122
|
ScrapePage,
|
|
140
123
|
WaitOptions,
|
|
141
124
|
} from './page';
|
|
142
|
-
export { DEFAULT_ACCESSIBILITY_MAX } from './page';
|
|
143
125
|
export type { PageContext } from './page-over-target';
|
|
144
126
|
export { pageOverTarget } from './page-over-target';
|
|
145
127
|
export type { Pacer } from './rate';
|
|
146
|
-
export { createPacer
|
|
128
|
+
export { createPacer } from './rate';
|
|
147
129
|
export type { HttpRecording, PageRecording } from './recording';
|
|
148
130
|
export {
|
|
149
131
|
httpRecordingSchema,
|
|
150
132
|
pageRecordingSchema,
|
|
151
133
|
parseHttpRecording,
|
|
152
134
|
parseRecording,
|
|
153
|
-
splitDownload,
|
|
154
135
|
} from './recording';
|
|
155
136
|
export type { AgentRecovery, Recovery, RecoveryAttempt, RecoveryHook } from './recover';
|
|
156
137
|
export { runRecovery } from './recover';
|
|
@@ -166,13 +147,12 @@ export type {
|
|
|
166
147
|
} from './rings';
|
|
167
148
|
export {
|
|
168
149
|
createRing,
|
|
169
|
-
DEFAULT_RING_CAPACITY,
|
|
170
150
|
MAX_PAGE_ERROR_CHARS,
|
|
171
151
|
pageErrorEntry,
|
|
172
152
|
RESOURCE_TYPES,
|
|
173
153
|
} from './rings';
|
|
174
154
|
export type { RobotsFetch, RobotsGate, RobotsGateInit, RobotsPolicy, RobotsRules } from './robots';
|
|
175
|
-
export { createRobotsGate,
|
|
155
|
+
export { createRobotsGate, parseRobots, robotsAllows } from './robots';
|
|
176
156
|
export type { RobotsFetchInit } from './robots-fetch';
|
|
177
157
|
export {
|
|
178
158
|
DEFAULT_ROBOTS_MAX_BYTES,
|
|
@@ -189,11 +169,9 @@ export { scrape } from './scrape';
|
|
|
189
169
|
export { DEFAULT_PAGE_TIMEOUT_MS, runScrape } from './scrape-run';
|
|
190
170
|
export type { ScrapeSecrets, SecretResolver } from './secrets';
|
|
191
171
|
export {
|
|
192
|
-
blankPasswordFields,
|
|
193
172
|
createSecretBag,
|
|
194
173
|
MIN_REDACTABLE_LENGTH,
|
|
195
174
|
redactSecrets,
|
|
196
|
-
SECRET_PLACEHOLDER,
|
|
197
175
|
safeConsole,
|
|
198
176
|
safeHtml,
|
|
199
177
|
safeNetwork,
|
|
@@ -201,8 +179,6 @@ export {
|
|
|
201
179
|
} from './secrets';
|
|
202
180
|
export type { ScrapeSessionStore, SessionSnapshot, SessionState } from './session-state';
|
|
203
181
|
export {
|
|
204
|
-
DEFAULT_SESSION_PREFIX,
|
|
205
|
-
EMPTY_SESSION,
|
|
206
182
|
memorySessionStore,
|
|
207
183
|
parseSessionState,
|
|
208
184
|
sessionDigest,
|
|
@@ -220,6 +196,5 @@ export type {
|
|
|
220
196
|
ScrapeDownloadFile,
|
|
221
197
|
ScrapeTarget,
|
|
222
198
|
} from './target';
|
|
223
|
-
export { ROOT_SELECTOR } from './target';
|
|
224
199
|
export type { WedgeGuard, WedgeGuardInit } from './watchdog';
|
|
225
|
-
export {
|
|
200
|
+
export { DEFAULT_GRACE_MS } from './watchdog';
|