@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 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('whd_123abc');
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. `0` disables the SDK-side timeout. Default 120000.
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. Wins over the constructor's `timeoutMs`.
34
- * `0` disables the SDK-side timeout for this call.
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 raw `Response`, having
95
- * already thrown for the two non-body failure modes:
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 left unread so each caller can decode it as it needs — JSON for
102
- * `request()`, raw text + headers for `requestRaw()`. This is the single place
103
- * that owns auth, the abort budget, and the error taxonomy; the two public
104
- * transports differ only in how they read a successful body.
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
- /** Map a `fetch` rejection onto the connection-error taxonomy. Never leaks the API key. */
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
- const rawName = typeof err === 'object' && err !== null && 'name' in err ? err.name : undefined;
132
- const name = typeof rawName === 'string' ? rawName : '';
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 raw `Response`, having
229
- * already thrown for the two non-body failure modes:
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 left unread so each caller can decode it as it needs — JSON for
236
- * `request()`, raw text + headers for `requestRaw()`. This is the single place
237
- * that owns auth, the abort budget, and the error taxonomy; the two public
238
- * transports differ only in how they read a successful body.
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 = await fetch(url, init);
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
- const json = (await response.json());
274
- code = json.error?.code ?? code;
275
- message = json.error?.message ?? message;
318
+ response = await fetch(url, init);
319
+ }
320
+ catch (err) {
321
+ throw toConnectionError(err, method, url, timeoutSignal);
276
322
  }
277
- catch {
278
- // leave defaults
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
- throw new ByteKitError(response.status, code, message);
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
- if (response.status === 204) {
300
- return undefined;
301
- }
302
- const text = await response.text();
303
- if (text.trim().length === 0) {
304
- return undefined;
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
- const response = await this.send(method, path, body, options);
327
- const bytes = new Uint8Array(await response.arrayBuffer());
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: response.headers, status: response.status };
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
  }
@@ -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?: Record<string, never>;
2024
+ options?: {
2025
+ [key: string]: unknown;
2026
+ };
2025
2027
  /** @description Scrape monitor options (formats, country, headers, etc.). Only for type=scrape. */
2026
- scrape_options?: Record<string, never>;
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?: Record<string, never>;
2050
- scrape_options?: Record<string, never>;
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?: Record<string, never>;
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?: Record<string, never>;
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?: Record<string, never>;
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
- /** GET /v1/account — get the authenticated account */
41
- get(): Promise<GetAccountResponse>;
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 {};