@hunt-labs/bytekit-sdk 0.3.0 → 0.4.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/CHANGELOG.md +66 -0
- package/README.md +27 -3
- package/dist/client.d.ts +66 -11
- package/dist/client.js +159 -45
- package/dist/generated/api.d.ts +17 -7
- package/dist/index.d.ts +3 -3
- package/dist/resources/account.d.ts +14 -9
- package/dist/resources/account.js +20 -15
- package/dist/resources/bulk.d.ts +11 -6
- package/dist/resources/bulk.js +14 -9
- package/dist/resources/fetch.d.ts +44 -15
- package/dist/resources/fetch.js +43 -22
- package/dist/resources/monitors.d.ts +36 -17
- package/dist/resources/monitors.js +12 -12
- package/dist/resources/scrape.d.ts +20 -5
- package/dist/resources/scrape.js +10 -5
- package/dist/resources/screenshots.d.ts +31 -3
- package/dist/resources/screenshots.js +35 -4
- package/dist/resources/search.d.ts +8 -3
- package/dist/resources/search.js +8 -3
- package/dist/resources/sitemap.d.ts +35 -6
- package/dist/resources/sitemap.js +23 -5
- package/dist/resources/usage.d.ts +10 -5
- package/dist/resources/usage.js +12 -7
- package/dist/resources/webhooks.d.ts +9 -4
- package/dist/resources/webhooks.js +10 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,72 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
This file records only the delta between versions. For how to use the package
|
|
9
9
|
today, see [README.md](./README.md), which remains the authoritative usage guide.
|
|
10
10
|
|
|
11
|
+
## [0.4.0] - 2026-07-24
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `RequestOptions` (per-call `requestTimeoutMs` + caller `signal`) is now accepted by
|
|
16
|
+
**every** resource method, not just `scrape.create` / `scrape.get`. It is always a
|
|
17
|
+
trailing optional parameter — on `screenshots.create` it is the third positional, after
|
|
18
|
+
`params` — so every existing call site compiles and serializes byte-identically. Per-call
|
|
19
|
+
options continue to beat the constructor's `timeoutMs` default; omitting them leaves the
|
|
20
|
+
constructor default in force exactly as before. (#2547)
|
|
21
|
+
- `ApiResponse<T>` (`{ data, headers, status }`), a generic `client.requestWithResponse<T>()`
|
|
22
|
+
transport, and three typed siblings — `sitemap.getWithResponse`,
|
|
23
|
+
`screenshots.createWithResponse`, `screenshots.getWithResponse` — make the documented
|
|
24
|
+
`X-Raw-Bytes`, `X-Billable-Bytes`, `X-Billing-Multiplier` and `X-Screenshot-*` response
|
|
25
|
+
headers reachable. This surface is purely **additive**: it issues the identical request
|
|
26
|
+
with the identical options as the base method, and no existing return type changes.
|
|
27
|
+
(#2547)
|
|
28
|
+
- `MonitorStatus` and `ScrapeCookie` are exported from the package entrypoint. (#2547)
|
|
29
|
+
|
|
30
|
+
### Changed (**BREAKING** at compile time)
|
|
31
|
+
|
|
32
|
+
- `ScrapeOpts.cookies` is now `ScrapeCookie[]`, derived from the spec's `Cookie` schema
|
|
33
|
+
(`name` and `value` required; `domain`, `path`, `secure`, `httpOnly` optional), instead of
|
|
34
|
+
an open bag of untyped records. A cookie missing `name` used to compile and fail as a 422
|
|
35
|
+
on the wire; it is a compile error now. Serialization is unchanged. (#2547)
|
|
36
|
+
- `ListMonitorsParams.status` is now the spec enum
|
|
37
|
+
(`'active' | 'processing' | 'paused' | 'cancelled' | 'suspended'`), derived from the
|
|
38
|
+
generated `listMonitors` query type, instead of `string`. A typo like `'activee'` used to
|
|
39
|
+
compile and 422; it is a compile error now. The serialized query string is unchanged.
|
|
40
|
+
(#2547)
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- `timeoutMs` / `requestTimeoutMs` (and a caller-supplied `signal`) now cover the
|
|
45
|
+
**response body read**, not just the time to first byte of headers. The abort budget was
|
|
46
|
+
armed for `fetch` and cleared the moment headers arrived, after which `request()`'s
|
|
47
|
+
`response.text()`, `requestRaw()`'s `response.arrayBuffer()`, and the error-body
|
|
48
|
+
`response.json()` on a non-2xx all read the body unbounded — so a server that answered
|
|
49
|
+
with headers and then stalled its body hung the caller forever. A mid-body abort now
|
|
50
|
+
surfaces as `ByteKitConnectionError` with `code: 'timeout'` (budget) or `'aborted'`
|
|
51
|
+
(caller signal) instead of a bare `TypeError: terminated`.
|
|
52
|
+
|
|
53
|
+
The default is **unchanged at 120 s**, and the precedence table for
|
|
54
|
+
`signal` / `requestTimeoutMs` / `timeoutMs` is untouched. One consequence worth planning
|
|
55
|
+
for: a legitimately large body (a multi-MB `/v1/fetch` document) now counts against that
|
|
56
|
+
same budget, where previously only the wait for headers did. (#2546)
|
|
57
|
+
|
|
58
|
+
- The README's webhook-retry sample used an invalid delivery id prefix (`whd_`) that its own
|
|
59
|
+
branded type rejects. The sample is corrected, and a new test compiles every TypeScript
|
|
60
|
+
sample in the README against the SDK's sources so a non-compiling sample can no longer
|
|
61
|
+
ship. (#2547)
|
|
62
|
+
|
|
63
|
+
### Fixed (**BREAKING** at compile time)
|
|
64
|
+
|
|
65
|
+
- `fetch.get` no longer accepts the eleven markdown-tuning options (`markdown_mode`,
|
|
66
|
+
`markdown_query`, `markdown_links`, `markdown_images`, `with_links_summary`,
|
|
67
|
+
`with_images_summary`, `markdown_compact`, `markdown_filter_images`,
|
|
68
|
+
`markdown_include_media`, `markdown_include_warnings`, `markdown_include_stats`).
|
|
69
|
+
The spec's `GET /v1/fetch` declares only `url`, `format`, `country`, `timeout_ms`
|
|
70
|
+
and `cache_ttl`, so those options were silently dropped by the query builder — a
|
|
71
|
+
caller asking for `markdown_mode: 'llm'` compiled and received default article-mode
|
|
72
|
+
markdown. They are now compile errors instead of silent no-ops; use `fetch.create`
|
|
73
|
+
(POST), which accepts and serializes all of them unchanged. `FetchGetOpts` is now
|
|
74
|
+
derived from the generated `getFetch` query type, so future spec drift fails
|
|
75
|
+
`tsc --noEmit` rather than reaching production. (#2539)
|
|
76
|
+
|
|
11
77
|
## [0.3.0] - 2026-07-21
|
|
12
78
|
|
|
13
79
|
### Removed (**BREAKING**)
|
package/README.md
CHANGED
|
@@ -155,13 +155,14 @@ await client.monitors.delete('mon_123abc');
|
|
|
155
155
|
### Sitemap crawl
|
|
156
156
|
|
|
157
157
|
```ts
|
|
158
|
+
// `url` is the only required field — every other option can be omitted.
|
|
159
|
+
const minimal = await client.sitemap.create({ url: 'https://example.com' });
|
|
160
|
+
|
|
158
161
|
const crawl = await client.sitemap.create({
|
|
159
162
|
url: 'https://example.com',
|
|
160
163
|
strategy: 'standard',
|
|
161
164
|
max_depth: 2,
|
|
162
165
|
max_urls: 5000,
|
|
163
|
-
process: null,
|
|
164
|
-
webhook_secret: null,
|
|
165
166
|
cache_ttl: '30d',
|
|
166
167
|
compact: false,
|
|
167
168
|
});
|
|
@@ -181,7 +182,7 @@ const byEndpoint = await client.usage.byEndpoint();
|
|
|
181
182
|
|
|
182
183
|
// Webhook deliveries
|
|
183
184
|
const deliveries = await client.webhooks.list();
|
|
184
|
-
await client.webhooks.retry('
|
|
185
|
+
await client.webhooks.retry('wh_123abc');
|
|
185
186
|
```
|
|
186
187
|
|
|
187
188
|
> **Note:** `client.account.*` (including `client.account.apiKeys.*`) targets the
|
|
@@ -191,6 +192,29 @@ await client.webhooks.retry('whd_123abc');
|
|
|
191
192
|
> `x-sdk-scope: internal` in the spec, so they are intentionally excluded from the
|
|
192
193
|
> v0.1 Bearer-key parity guarantee.
|
|
193
194
|
|
|
195
|
+
### Response headers
|
|
196
|
+
|
|
197
|
+
The API carries billing and capture metadata in `X-*` response headers alongside the
|
|
198
|
+
JSON body — `X-Raw-Bytes`, `X-Billable-Bytes`, `X-Billing-Multiplier`, and the
|
|
199
|
+
`X-Screenshot-*` family. Every resource method returns just the parsed body, so use a
|
|
200
|
+
`*WithResponse` sibling when you also need the headers:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const { data, headers, status } = await client.sitemap.getWithResponse('sm_123abc');
|
|
204
|
+
|
|
205
|
+
console.log(data.status); // exactly what sitemap.get('sm_123abc') resolves to
|
|
206
|
+
console.log(headers.get('X-Raw-Bytes')); // e.g. "12345"
|
|
207
|
+
console.log(status); // 200
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`client.screenshots.createWithResponse()` and `client.screenshots.getWithResponse()`
|
|
211
|
+
work the same way. Any other endpoint's headers are reachable through the generic
|
|
212
|
+
transport, `client.requestWithResponse('GET', '/v1/scrape/sc_123abc')`.
|
|
213
|
+
|
|
214
|
+
These siblings are purely additive: they issue the identical request and accept the
|
|
215
|
+
identical options as their base methods, and `data` is exactly what the base method
|
|
216
|
+
resolves to — no existing return type changes.
|
|
217
|
+
|
|
194
218
|
## Error handling
|
|
195
219
|
|
|
196
220
|
Any 4xx/5xx response throws a `ByteKitError` carrying the HTTP status and
|
package/dist/client.d.ts
CHANGED
|
@@ -13,7 +13,9 @@ export interface ByteKitOptions {
|
|
|
13
13
|
baseUrl?: string;
|
|
14
14
|
/**
|
|
15
15
|
* Default CLIENT-SIDE request timeout in ms — the abort budget for the whole HTTP
|
|
16
|
-
* round-trip made by this SDK
|
|
16
|
+
* round-trip made by this SDK, covering time-to-headers **and** the response body
|
|
17
|
+
* read (so a large legitimate body counts against it too). `0` disables the SDK-side
|
|
18
|
+
* timeout. Default 120000.
|
|
17
19
|
*
|
|
18
20
|
* NOT the same thing as `ScrapeOpts.timeout_ms`, which is the SERVER-side budget the
|
|
19
21
|
* gateway applies to the scrape itself. Override per call with
|
|
@@ -26,12 +28,14 @@ export interface RequestOptions {
|
|
|
26
28
|
/**
|
|
27
29
|
* Caller-owned abort signal. When supplied WITHOUT a per-call `requestTimeoutMs`, it
|
|
28
30
|
* fully replaces the constructor timeout — supply `requestTimeoutMs` alongside it to
|
|
29
|
-
* keep a bound (the request then aborts on whichever fires first).
|
|
31
|
+
* keep a bound (the request then aborts on whichever fires first). Firing it aborts an
|
|
32
|
+
* in-flight response body read as well, not only the wait for headers.
|
|
30
33
|
*/
|
|
31
34
|
signal?: AbortSignal;
|
|
32
35
|
/**
|
|
33
|
-
* Per-call CLIENT-SIDE abort budget in ms
|
|
34
|
-
* `0` disables the SDK-side timeout for
|
|
36
|
+
* Per-call CLIENT-SIDE abort budget in ms, covering headers **and** the response body
|
|
37
|
+
* read. Wins over the constructor's `timeoutMs`. `0` disables the SDK-side timeout for
|
|
38
|
+
* this call.
|
|
35
39
|
*
|
|
36
40
|
* Deliberately NOT named `timeoutMs`: it rides on the same call as `ScrapeOpts.timeout_ms`
|
|
37
41
|
* (the SERVER-side scrape budget), and the two mean different things —
|
|
@@ -40,6 +44,29 @@ export interface RequestOptions {
|
|
|
40
44
|
*/
|
|
41
45
|
requestTimeoutMs?: number;
|
|
42
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* A JSON response together with its transport metadata (#2547).
|
|
49
|
+
*
|
|
50
|
+
* Returned by {@link ByteKit.requestWithResponse} and by the `*WithResponse` sibling
|
|
51
|
+
* methods. `data` is exactly what the corresponding base method resolves to, so a caller
|
|
52
|
+
* can migrate from `x = await r.get(id)` to `{ data: x, headers } = await
|
|
53
|
+
* r.getWithResponse(id)` with no change to how `x` is used.
|
|
54
|
+
*
|
|
55
|
+
* This is an ADDITIVE surface: it never changes the return type, the request bytes, or the
|
|
56
|
+
* abort semantics of any existing method.
|
|
57
|
+
*/
|
|
58
|
+
export interface ApiResponse<T> {
|
|
59
|
+
/** The parsed JSON body — identical to what the base method returns. `undefined` on 204/empty. */
|
|
60
|
+
data: T;
|
|
61
|
+
/**
|
|
62
|
+
* The full RESPONSE headers, e.g. `X-Raw-Bytes`, `X-Billable-Bytes`,
|
|
63
|
+
* `X-Billing-Multiplier`. The `Authorization` header lives on the REQUEST and is never
|
|
64
|
+
* reachable here.
|
|
65
|
+
*/
|
|
66
|
+
headers: Headers;
|
|
67
|
+
/** The HTTP status code of the successful response (2xx; non-2xx already threw). */
|
|
68
|
+
status: number;
|
|
69
|
+
}
|
|
43
70
|
/** Why a request never produced an HTTP response. */
|
|
44
71
|
export type ConnectionErrorCode = 'timeout' | 'aborted' | 'connection';
|
|
45
72
|
/** Thrown when the API responds with a 4xx or 5xx status code. */
|
|
@@ -91,17 +118,28 @@ export declare class ByteKit {
|
|
|
91
118
|
*/
|
|
92
119
|
private resolveSignal;
|
|
93
120
|
/**
|
|
94
|
-
* Perform the authenticated round-trip and return the
|
|
95
|
-
* already thrown for the two
|
|
121
|
+
* Perform the authenticated round-trip and return whatever the caller's `readBody`
|
|
122
|
+
* continuation makes of the successful response, having already thrown for the two
|
|
123
|
+
* non-body failure modes:
|
|
96
124
|
*
|
|
97
125
|
* - `ByteKitConnectionError` when no HTTP response was produced at all
|
|
98
|
-
* (DNS/socket/TLS failure, client-side timeout, or caller abort)
|
|
126
|
+
* (DNS/socket/TLS failure, client-side timeout, or caller abort) — including one
|
|
127
|
+
* that fires while the body is streaming.
|
|
99
128
|
* - `ByteKitError` on any 4xx/5xx, carrying the API's `code`/`message`.
|
|
100
129
|
*
|
|
101
|
-
* The body is
|
|
102
|
-
* `request()`, raw
|
|
103
|
-
*
|
|
104
|
-
*
|
|
130
|
+
* The body is read through a continuation each caller supplies — JSON text for
|
|
131
|
+
* `request()`, raw bytes for `requestRaw()`. This is the single place that owns auth,
|
|
132
|
+
* the abort budget, and the error taxonomy; the two public transports differ only in
|
|
133
|
+
* how they read a successful body.
|
|
134
|
+
*
|
|
135
|
+
* The abort budget spans headers **and** body (#2546). Returning the `Response` used to
|
|
136
|
+
* end the guarded region at the headers boundary, leaving `response.text()` /
|
|
137
|
+
* `response.arrayBuffer()` — and the `!response.ok` branch's `response.json()` —
|
|
138
|
+
* unbounded: a server that sent headers and then stalled its body hung the caller
|
|
139
|
+
* forever. `readBody` must therefore ONLY consume the stream; `JSON.parse` and
|
|
140
|
+
* `TextDecoder` run in the callers, outside the guard, so a malformed body still
|
|
141
|
+
* surfaces its own parse error rather than being remapped to a connection error.
|
|
142
|
+
* `cleanup()` still runs exactly once per call, now after the body completes.
|
|
105
143
|
*/
|
|
106
144
|
private send;
|
|
107
145
|
/**
|
|
@@ -135,4 +173,21 @@ export declare class ByteKit {
|
|
|
135
173
|
headers: Headers;
|
|
136
174
|
status: number;
|
|
137
175
|
}>;
|
|
176
|
+
/**
|
|
177
|
+
* Make an authenticated JSON request and return the parsed body TOGETHER WITH the
|
|
178
|
+
* response headers and status (#2547).
|
|
179
|
+
*
|
|
180
|
+
* Identical to {@link ByteKit.request} in every observable respect — same method, path,
|
|
181
|
+
* body, `RequestOptions` precedence, error taxonomy, and empty-body handling (both drain
|
|
182
|
+
* the body through {@link readSuccessText} and parse it through {@link parseJsonBody}). It
|
|
183
|
+
* differs only in what it returns, so nothing about an existing call changes by switching
|
|
184
|
+
* to it.
|
|
185
|
+
*
|
|
186
|
+
* Use it to read the documented `X-*` metadata the API emits alongside a JSON envelope —
|
|
187
|
+
* `X-Raw-Bytes`, `X-Billable-Bytes`, `X-Billing-Multiplier`, the `X-Screenshot-*` family
|
|
188
|
+
* — for any endpoint. `sitemap.getWithResponse`, `screenshots.createWithResponse` and
|
|
189
|
+
* `screenshots.getWithResponse` are typed shorthands over this for the endpoints whose
|
|
190
|
+
* headers are documented in the API reference.
|
|
191
|
+
*/
|
|
192
|
+
requestWithResponse<T>(method: string, path: string, body?: unknown, options?: RequestOptions): Promise<ApiResponse<T>>;
|
|
138
193
|
}
|
package/dist/client.js
CHANGED
|
@@ -124,12 +124,59 @@ function composeSignals(caller, timeout) {
|
|
|
124
124
|
}
|
|
125
125
|
return { signal: controller.signal, cleanup };
|
|
126
126
|
}
|
|
127
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Read a successful response body as text INSIDE the transport's abort-guarded region
|
|
129
|
+
* (#2546), short-circuiting a 204 to `undefined`. The `JSON.parse` step is deliberately
|
|
130
|
+
* kept OUT of the guard — see {@link parseJsonBody} — so a malformed body still surfaces
|
|
131
|
+
* its own SyntaxError rather than being remapped to a `ByteKitConnectionError`.
|
|
132
|
+
*
|
|
133
|
+
* Shared as the `readBody` continuation of `request()` and (wrapped) `requestWithResponse()`
|
|
134
|
+
* so empty-body/204 handling can never diverge between the two (#2547).
|
|
135
|
+
*/
|
|
136
|
+
async function readSuccessText(response) {
|
|
137
|
+
return response.status === 204 ? undefined : await response.text();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Parse an already-read success body (see {@link readSuccessText}) into its JSON value.
|
|
141
|
+
*
|
|
142
|
+
* A 204 or otherwise-empty body resolves to `undefined` — this is how a successful
|
|
143
|
+
* `revoke()` (DELETE → 204) resolves instead of throwing a JSON-parse error. Runs OUTSIDE
|
|
144
|
+
* the transport's abort-guarded region (#2546), so `JSON.parse` failures stay ordinary
|
|
145
|
+
* SyntaxErrors. Shared by `request()` and `requestWithResponse()` (#2547).
|
|
146
|
+
*/
|
|
147
|
+
function parseJsonBody(text) {
|
|
148
|
+
if (text === undefined || text.trim().length === 0) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
return JSON.parse(text);
|
|
152
|
+
}
|
|
153
|
+
/** The `name` of an error-shaped value, or `undefined` when it carries none. */
|
|
154
|
+
const nameOf = (e) => typeof e === 'object' && e !== null && 'name' in e ? String(e.name) : undefined;
|
|
155
|
+
/** The `cause` of an error-shaped value, or `undefined` when it carries none. */
|
|
156
|
+
const causeOf = (e) => typeof e === 'object' && e !== null && 'cause' in e ? e.cause : undefined;
|
|
157
|
+
/**
|
|
158
|
+
* The abort-taxonomy name of a rejection, seeing through undici's mid-body wrapper.
|
|
159
|
+
*
|
|
160
|
+
* When a request is aborted while its BODY is streaming, undici does not reject with the abort
|
|
161
|
+
* reason itself — it rejects with `TypeError: terminated` carrying the reason on `.cause` (#2546).
|
|
162
|
+
* A name-only check would read that as a plain connection failure. Nothing that reached this
|
|
163
|
+
* function before #2546 carried a `.cause`, so every previously-asserted classification is
|
|
164
|
+
* unchanged; a `.cause` whose own name is not abort-shaped still falls through to `'connection'`.
|
|
165
|
+
*/
|
|
166
|
+
function abortNameOf(err) {
|
|
167
|
+
const top = nameOf(err);
|
|
168
|
+
if (top === 'TimeoutError' || top === 'AbortError')
|
|
169
|
+
return top;
|
|
170
|
+
return nameOf(causeOf(err));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Map a `fetch` or body-read rejection onto the connection-error taxonomy. Never leaks the API key.
|
|
174
|
+
*/
|
|
128
175
|
function toConnectionError(err, method, url, timeoutSignal) {
|
|
129
176
|
// undici rejects an aborted fetch with the signal's own reason, so the thrown value's
|
|
130
|
-
// `name` is the primary discriminator (`TimeoutError` / `AbortError`)
|
|
131
|
-
|
|
132
|
-
const name =
|
|
177
|
+
// `name` is the primary discriminator (`TimeoutError` / `AbortError`) — read through
|
|
178
|
+
// undici's mid-body `TypeError: terminated` wrapper when there is one.
|
|
179
|
+
const name = abortNameOf(err) ?? '';
|
|
133
180
|
let code;
|
|
134
181
|
if (name === 'TimeoutError') {
|
|
135
182
|
code = 'timeout';
|
|
@@ -225,19 +272,30 @@ export class ByteKit {
|
|
|
225
272
|
};
|
|
226
273
|
}
|
|
227
274
|
/**
|
|
228
|
-
* Perform the authenticated round-trip and return the
|
|
229
|
-
* already thrown for the two
|
|
275
|
+
* Perform the authenticated round-trip and return whatever the caller's `readBody`
|
|
276
|
+
* continuation makes of the successful response, having already thrown for the two
|
|
277
|
+
* non-body failure modes:
|
|
230
278
|
*
|
|
231
279
|
* - `ByteKitConnectionError` when no HTTP response was produced at all
|
|
232
|
-
* (DNS/socket/TLS failure, client-side timeout, or caller abort)
|
|
280
|
+
* (DNS/socket/TLS failure, client-side timeout, or caller abort) — including one
|
|
281
|
+
* that fires while the body is streaming.
|
|
233
282
|
* - `ByteKitError` on any 4xx/5xx, carrying the API's `code`/`message`.
|
|
234
283
|
*
|
|
235
|
-
* The body is
|
|
236
|
-
* `request()`, raw
|
|
237
|
-
*
|
|
238
|
-
*
|
|
284
|
+
* The body is read through a continuation each caller supplies — JSON text for
|
|
285
|
+
* `request()`, raw bytes for `requestRaw()`. This is the single place that owns auth,
|
|
286
|
+
* the abort budget, and the error taxonomy; the two public transports differ only in
|
|
287
|
+
* how they read a successful body.
|
|
288
|
+
*
|
|
289
|
+
* The abort budget spans headers **and** body (#2546). Returning the `Response` used to
|
|
290
|
+
* end the guarded region at the headers boundary, leaving `response.text()` /
|
|
291
|
+
* `response.arrayBuffer()` — and the `!response.ok` branch's `response.json()` —
|
|
292
|
+
* unbounded: a server that sent headers and then stalled its body hung the caller
|
|
293
|
+
* forever. `readBody` must therefore ONLY consume the stream; `JSON.parse` and
|
|
294
|
+
* `TextDecoder` run in the callers, outside the guard, so a malformed body still
|
|
295
|
+
* surfaces its own parse error rather than being remapped to a connection error.
|
|
296
|
+
* `cleanup()` still runs exactly once per call, now after the body completes.
|
|
239
297
|
*/
|
|
240
|
-
async send(method, path, body, options) {
|
|
298
|
+
async send(method, path, body, options, readBody) {
|
|
241
299
|
const url = `${this.baseUrl}${path}`;
|
|
242
300
|
const { signal, timeoutSignal, cleanup } = this.resolveSignal(options);
|
|
243
301
|
const init = {
|
|
@@ -254,32 +312,58 @@ export class ByteKit {
|
|
|
254
312
|
if (signal !== undefined) {
|
|
255
313
|
init.signal = signal;
|
|
256
314
|
}
|
|
257
|
-
let response;
|
|
258
315
|
try {
|
|
259
|
-
response
|
|
260
|
-
}
|
|
261
|
-
catch (err) {
|
|
262
|
-
throw toConnectionError(err, method, url, timeoutSignal);
|
|
263
|
-
}
|
|
264
|
-
finally {
|
|
265
|
-
// Runs on success, on HTTP error, and on abort alike — no dangling timer,
|
|
266
|
-
// no listener left behind on the caller's (long-lived) signal.
|
|
267
|
-
cleanup();
|
|
268
|
-
}
|
|
269
|
-
if (!response.ok) {
|
|
270
|
-
let code = 'internal_error';
|
|
271
|
-
let message = response.statusText;
|
|
316
|
+
let response;
|
|
272
317
|
try {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
318
|
+
response = await fetch(url, init);
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
throw toConnectionError(err, method, url, timeoutSignal);
|
|
276
322
|
}
|
|
277
|
-
|
|
278
|
-
|
|
323
|
+
if (!response.ok) {
|
|
324
|
+
let code = 'internal_error';
|
|
325
|
+
let message = response.statusText;
|
|
326
|
+
try {
|
|
327
|
+
const json = (await response.json());
|
|
328
|
+
// Canonical unified envelope. Tested first, so it always wins: the flat
|
|
329
|
+
// form below is strictly a fallback and can never override an envelope
|
|
330
|
+
// (issue #2541). The explicit `!== null` guard matters — `typeof null`
|
|
331
|
+
// is `'object'`.
|
|
332
|
+
if (typeof json.error === 'object' && json.error !== null) {
|
|
333
|
+
code = json.error.code ?? code;
|
|
334
|
+
message = json.error.message ?? message;
|
|
335
|
+
}
|
|
336
|
+
else if (typeof json.error === 'string' && json.error.length > 0) {
|
|
337
|
+
// Flat vendor-masked shape, e.g. /v1/search 502
|
|
338
|
+
// {"error":"search_provider_error"} (docs/api/openapi.yaml §/v1/search
|
|
339
|
+
// 502). The string IS the error code. `message` falls back to it only
|
|
340
|
+
// when the response carried no statusText, so the thrown error is
|
|
341
|
+
// never message-empty.
|
|
342
|
+
code = json.error;
|
|
343
|
+
message = message || json.error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// leave defaults — including when the error body itself stalls and the
|
|
348
|
+
// budget aborts the read (#2546), so a stuck 500 still surfaces its status.
|
|
349
|
+
}
|
|
350
|
+
throw new ByteKitError(response.status, code, message);
|
|
279
351
|
}
|
|
280
|
-
|
|
352
|
+
try {
|
|
353
|
+
return await readBody(response);
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
// A body read that never completes is aborted by the same budget; the runtime
|
|
357
|
+
// then errors the stream (#2546). Map it into the same taxonomy the headers
|
|
358
|
+
// phase uses, so a mid-body timeout/abort is not a bare `TypeError: terminated`.
|
|
359
|
+
throw toConnectionError(err, method, url, timeoutSignal);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
// Runs on success (body read included), on HTTP error, and on abort alike — no
|
|
364
|
+
// dangling timer, no listener left behind on the caller's (long-lived) signal.
|
|
365
|
+
cleanup();
|
|
281
366
|
}
|
|
282
|
-
return response;
|
|
283
367
|
}
|
|
284
368
|
/**
|
|
285
369
|
* Make an authenticated JSON request to the ByteKit API.
|
|
@@ -289,21 +373,18 @@ export class ByteKit {
|
|
|
289
373
|
* timeout, or caller abort).
|
|
290
374
|
*/
|
|
291
375
|
async request(method, path, body, options) {
|
|
292
|
-
const response = await this.send(method, path, body, options);
|
|
293
376
|
// A 204 No Content has an empty body, and a genuinely empty body can arrive on
|
|
294
377
|
// other success codes too (defensive). Calling response.json() on an empty body
|
|
295
378
|
// rejects with a SyntaxError — which is how a successful revoke() (DELETE → 204)
|
|
296
379
|
// used to throw instead of resolving. Read the body once as text and short-circuit
|
|
297
380
|
// to `undefined` when it is empty; a JSON-bearing response (scrape.create,
|
|
298
381
|
// apiKeys.create/get, …) still parses and returns unchanged.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
return JSON.parse(text);
|
|
382
|
+
//
|
|
383
|
+
// Only the read runs inside the transport's guarded region (#2546) — `JSON.parse`
|
|
384
|
+
// stays out here (in `parseJsonBody`), so a malformed body still throws its own
|
|
385
|
+
// SyntaxError rather than being remapped to a ByteKitConnectionError.
|
|
386
|
+
const text = await this.send(method, path, body, options, readSuccessText);
|
|
387
|
+
return parseJsonBody(text);
|
|
307
388
|
}
|
|
308
389
|
/**
|
|
309
390
|
* Make an authenticated request whose response body IS the content, not a JSON
|
|
@@ -323,9 +404,42 @@ export class ByteKit {
|
|
|
323
404
|
* content is text.
|
|
324
405
|
*/
|
|
325
406
|
async requestRaw(method, path, body, options) {
|
|
326
|
-
|
|
327
|
-
|
|
407
|
+
// As in `request()`: the continuation only drains the stream, so the abort budget
|
|
408
|
+
// covers the read (#2546); the UTF-8 decode happens outside the guarded region.
|
|
409
|
+
const { bytes, headers, status } = await this.send(method, path, body, options, async (response) => ({
|
|
410
|
+
bytes: new Uint8Array(await response.arrayBuffer()),
|
|
411
|
+
headers: response.headers,
|
|
412
|
+
status: response.status,
|
|
413
|
+
}));
|
|
328
414
|
const text = new TextDecoder('utf-8').decode(bytes);
|
|
329
|
-
return { body: text, bytes, headers
|
|
415
|
+
return { body: text, bytes, headers, status };
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Make an authenticated JSON request and return the parsed body TOGETHER WITH the
|
|
419
|
+
* response headers and status (#2547).
|
|
420
|
+
*
|
|
421
|
+
* Identical to {@link ByteKit.request} in every observable respect — same method, path,
|
|
422
|
+
* body, `RequestOptions` precedence, error taxonomy, and empty-body handling (both drain
|
|
423
|
+
* the body through {@link readSuccessText} and parse it through {@link parseJsonBody}). It
|
|
424
|
+
* differs only in what it returns, so nothing about an existing call changes by switching
|
|
425
|
+
* to it.
|
|
426
|
+
*
|
|
427
|
+
* Use it to read the documented `X-*` metadata the API emits alongside a JSON envelope —
|
|
428
|
+
* `X-Raw-Bytes`, `X-Billable-Bytes`, `X-Billing-Multiplier`, the `X-Screenshot-*` family
|
|
429
|
+
* — for any endpoint. `sitemap.getWithResponse`, `screenshots.createWithResponse` and
|
|
430
|
+
* `screenshots.getWithResponse` are typed shorthands over this for the endpoints whose
|
|
431
|
+
* headers are documented in the API reference.
|
|
432
|
+
*/
|
|
433
|
+
async requestWithResponse(method, path, body, options) {
|
|
434
|
+
// Same continuation shape as request()/requestRaw() (#2546): the readBody continuation
|
|
435
|
+
// ONLY drains the stream (and captures headers/status) inside the abort-guarded region,
|
|
436
|
+
// so a mid-body stall counts against the budget. `JSON.parse` runs afterwards, outside
|
|
437
|
+
// the guard, in `parseJsonBody` — a malformed body stays a SyntaxError.
|
|
438
|
+
const { text, headers, status } = await this.send(method, path, body, options, async (response) => ({
|
|
439
|
+
text: await readSuccessText(response),
|
|
440
|
+
headers: response.headers,
|
|
441
|
+
status: response.status,
|
|
442
|
+
}));
|
|
443
|
+
return { data: parseJsonBody(text), headers, status };
|
|
330
444
|
}
|
|
331
445
|
}
|
package/dist/generated/api.d.ts
CHANGED
|
@@ -2021,9 +2021,13 @@ export interface components {
|
|
|
2021
2021
|
[key: string]: string;
|
|
2022
2022
|
};
|
|
2023
2023
|
/** @description Screenshot monitor options (device, viewport, format, etc.). */
|
|
2024
|
-
options?:
|
|
2024
|
+
options?: {
|
|
2025
|
+
[key: string]: unknown;
|
|
2026
|
+
};
|
|
2025
2027
|
/** @description Scrape monitor options (formats, country, headers, etc.). Only for type=scrape. */
|
|
2026
|
-
scrape_options?:
|
|
2028
|
+
scrape_options?: {
|
|
2029
|
+
[key: string]: unknown;
|
|
2030
|
+
};
|
|
2027
2031
|
metadata?: components["schemas"]["Metadata"];
|
|
2028
2032
|
};
|
|
2029
2033
|
MonitorUpdateRequest: {
|
|
@@ -2046,8 +2050,12 @@ export interface components {
|
|
|
2046
2050
|
};
|
|
2047
2051
|
/** @enum {string} */
|
|
2048
2052
|
status?: "active" | "paused";
|
|
2049
|
-
options?:
|
|
2050
|
-
|
|
2053
|
+
options?: {
|
|
2054
|
+
[key: string]: unknown;
|
|
2055
|
+
};
|
|
2056
|
+
scrape_options?: {
|
|
2057
|
+
[key: string]: unknown;
|
|
2058
|
+
};
|
|
2051
2059
|
metadata?: components["schemas"]["Metadata"];
|
|
2052
2060
|
};
|
|
2053
2061
|
MonitorResponse: {
|
|
@@ -2136,7 +2144,9 @@ export interface components {
|
|
|
2136
2144
|
/** @default null */
|
|
2137
2145
|
process: {
|
|
2138
2146
|
action?: string;
|
|
2139
|
-
options?:
|
|
2147
|
+
options?: {
|
|
2148
|
+
[key: string]: unknown;
|
|
2149
|
+
};
|
|
2140
2150
|
} | null;
|
|
2141
2151
|
/**
|
|
2142
2152
|
* Format: uri
|
|
@@ -3343,7 +3353,7 @@ export interface operations {
|
|
|
3343
3353
|
};
|
|
3344
3354
|
delay_ms?: number;
|
|
3345
3355
|
timeout_ms?: number;
|
|
3346
|
-
metadata?:
|
|
3356
|
+
metadata?: components["schemas"]["Metadata"];
|
|
3347
3357
|
/**
|
|
3348
3358
|
* @description Per-item override for LLM markdown cleaning. When
|
|
3349
3359
|
* `true` and `markdown` is in `formats`, the converted
|
|
@@ -3737,7 +3747,7 @@ export interface operations {
|
|
|
3737
3747
|
*/
|
|
3738
3748
|
webhook_url: string;
|
|
3739
3749
|
webhook_secret?: string;
|
|
3740
|
-
metadata?:
|
|
3750
|
+
metadata?: components["schemas"]["Metadata"];
|
|
3741
3751
|
};
|
|
3742
3752
|
};
|
|
3743
3753
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export { ByteKit, ByteKitError, ByteKitConnectionError } from './client.js';
|
|
2
|
-
export type { ByteKitOptions, RequestOptions, ConnectionErrorCode } from './client.js';
|
|
2
|
+
export type { ByteKitOptions, RequestOptions, ApiResponse, ConnectionErrorCode } from './client.js';
|
|
3
3
|
export { V01_METHODS } from './allowlist.js';
|
|
4
4
|
export type { V01Method } from './allowlist.js';
|
|
5
5
|
export type { ScrapeId, ScreenshotId, BulkId, MonitorId, SitemapId, ApiKeyId, WebhookDeliveryId, SearchType, SearchDateRange, SearchOpts, SearchResult, SearchResultImage, SearchResponse, } from './types.js';
|
|
6
|
-
export type { ScrapeCreateResponse, ScrapeGetResponse, ScrapeBulkCreateRequest, ScrapeBulkCreateResponse, ScrapeBulkGetResponse, } from './resources/scrape.js';
|
|
6
|
+
export type { ScrapeCookie, ScrapeCreateResponse, ScrapeGetResponse, ScrapeBulkCreateRequest, ScrapeBulkCreateResponse, ScrapeBulkGetResponse, } from './resources/scrape.js';
|
|
7
7
|
export type { ScreenshotCreateRequest, ScreenshotCreateResponse, ScreenshotGetResponse, } from './resources/screenshots.js';
|
|
8
8
|
export type { FetchOpts, FetchGetOpts, FetchResult, FetchCacheStatus, FetchBulkCreateRequest, FetchBulkCreateResponse, FetchBulkGetResponse, } from './resources/fetch.js';
|
|
9
|
-
export type { MonitorCreateRequest, MonitorCreateResponse, ListMonitorsParams, ListMonitorsResponse, MonitorResponse, MonitorUpdateRequest, UpdateMonitorResponse, ListMonitorCapturesResponse, } from './resources/monitors.js';
|
|
9
|
+
export type { MonitorCreateRequest, MonitorCreateResponse, ListMonitorsParams, MonitorStatus, ListMonitorsResponse, MonitorResponse, MonitorUpdateRequest, UpdateMonitorResponse, ListMonitorCapturesResponse, } from './resources/monitors.js';
|
|
10
10
|
export type { SitemapCreateRequest, SitemapCreateResponse, SitemapGetResponse, } from './resources/sitemap.js';
|
|
11
11
|
export type { BulkCreateRequest, BulkCreateResponse, BulkGetResponse, BulkCancelResponse, ListBulkScreenshotsResponse, } from './resources/bulk.js';
|
|
12
12
|
export type { GetAccountResponse, ListApiKeysResponse, CreateApiKeyRequest, CreateApiKeyResponse, GetApiKeyResponse, UpdateApiKeyRequest, UpdateApiKeyResponse, RevealApiKeyResponse, } from './resources/account.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ByteKit } from '../client.js';
|
|
1
|
+
import type { ByteKit, RequestOptions } from '../client.js';
|
|
2
2
|
import type { operations } from '../generated/api.js';
|
|
3
3
|
import type { ApiKeyId } from '../types.js';
|
|
4
4
|
/**
|
|
@@ -37,8 +37,13 @@ export declare class AccountResource {
|
|
|
37
37
|
private readonly client;
|
|
38
38
|
readonly apiKeys: ApiKeysResource;
|
|
39
39
|
constructor(client: ByteKit);
|
|
40
|
-
/**
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* GET /v1/account — get the authenticated account.
|
|
42
|
+
*
|
|
43
|
+
* `requestOptions` carries the CLIENT-side abort budget (`requestTimeoutMs`) and an
|
|
44
|
+
* optional caller `signal`; both override the constructor's `timeoutMs` default.
|
|
45
|
+
*/
|
|
46
|
+
get(requestOptions?: RequestOptions): Promise<GetAccountResponse>;
|
|
42
47
|
}
|
|
43
48
|
declare class ApiKeysResource {
|
|
44
49
|
private readonly client;
|
|
@@ -49,13 +54,13 @@ declare class ApiKeysResource {
|
|
|
49
54
|
* `opts` is optional: the spec declares the request body itself as optional,
|
|
50
55
|
* so `create()` with no arguments is valid and sends no body.
|
|
51
56
|
*/
|
|
52
|
-
create(opts?: CreateApiKeyRequest): Promise<CreateApiKeyResponse>;
|
|
57
|
+
create(opts?: CreateApiKeyRequest, requestOptions?: RequestOptions): Promise<CreateApiKeyResponse>;
|
|
53
58
|
/** GET /v1/account/api-keys — list all API keys for the account */
|
|
54
|
-
list(): Promise<ListApiKeysResponse>;
|
|
59
|
+
list(requestOptions?: RequestOptions): Promise<ListApiKeysResponse>;
|
|
55
60
|
/** GET /v1/account/api-keys/{id} — get an API key by ID */
|
|
56
|
-
get(id: ApiKeyId): Promise<GetApiKeyResponse>;
|
|
61
|
+
get(id: ApiKeyId, requestOptions?: RequestOptions): Promise<GetApiKeyResponse>;
|
|
57
62
|
/** PATCH /v1/account/api-keys/{id} — update an API key */
|
|
58
|
-
update(id: ApiKeyId, opts: UpdateApiKeyRequest): Promise<UpdateApiKeyResponse>;
|
|
63
|
+
update(id: ApiKeyId, opts: UpdateApiKeyRequest, requestOptions?: RequestOptions): Promise<UpdateApiKeyResponse>;
|
|
59
64
|
/**
|
|
60
65
|
* DELETE /v1/account/api-keys/{id} — revoke an API key (idempotent).
|
|
61
66
|
*
|
|
@@ -67,8 +72,8 @@ declare class ApiKeysResource {
|
|
|
67
72
|
* to `undefined` before it would parse the body as JSON (#2450), so this
|
|
68
73
|
* method resolves cleanly on the gateway's empty 204 body.
|
|
69
74
|
*/
|
|
70
|
-
revoke(id: ApiKeyId): Promise<void>;
|
|
75
|
+
revoke(id: ApiKeyId, requestOptions?: RequestOptions): Promise<void>;
|
|
71
76
|
/** POST /v1/account/api-keys/{id}/reveal — reveal the plaintext API key value */
|
|
72
|
-
reveal(id: ApiKeyId): Promise<RevealApiKeyResponse>;
|
|
77
|
+
reveal(id: ApiKeyId, requestOptions?: RequestOptions): Promise<RevealApiKeyResponse>;
|
|
73
78
|
}
|
|
74
79
|
export {};
|