@hunt-labs/bytekit-sdk 0.9.3 → 0.10.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 +96 -0
- package/README.md +26 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.js +61 -3
- package/dist/generated/api.d.ts +358 -69
- package/dist/index.d.ts +1 -1
- package/dist/resources/account.d.ts +2 -2
- package/dist/resources/account.js +2 -2
- package/dist/resources/bulk.d.ts +14 -2
- package/dist/resources/bulk.js +14 -3
- package/dist/resources/fetch.d.ts +5 -2
- package/dist/resources/fetch.js +16 -5
- package/dist/resources/monitors.d.ts +2 -2
- package/dist/resources/monitors.js +2 -2
- package/dist/resources/scrape.d.ts +43 -6
- package/dist/resources/scrape.js +30 -5
- package/dist/resources/screenshots.d.ts +12 -7
- package/dist/resources/screenshots.js +6 -6
- package/dist/resources/sitemap.d.ts +1 -1
- package/dist/resources/sitemap.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,102 @@ today, see [README.md](./README.md), which remains the authoritative usage guide
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.10.0] - 2026-09-11
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **A terminal `POST /v1/scrape` failure now throws instead of resolving (#1870).** The
|
|
18
|
+
gateway used to report an upstream fetch failure — unreachable host, refused connection,
|
|
19
|
+
or bot protection that defeated every proxy tier — as HTTP 503; it now answers **HTTP 200**
|
|
20
|
+
with the canonical `failed` envelope (`id: null`), because a definitive upstream outcome is
|
|
21
|
+
a result, not a transport error. `!response.ok` was this SDK's only failure seam, so without
|
|
22
|
+
this change that body would resolve like a success and silence every caller's `catch`.
|
|
23
|
+
`scrape.create` (and any `request` / `requestWithResponse` call to `POST /v1/scrape`) now
|
|
24
|
+
raises `ByteKitError` carrying the envelope's `code` and `message`, the **actual** status
|
|
25
|
+
`200`, the response headers, and `error.details` when one is supplied. A legacy 503 from a
|
|
26
|
+
not-yet-upgraded gateway still throws through the unchanged non-2xx decoder, so the SDK
|
|
27
|
+
works against both. **Upgrade note:** if you inspected `result.status === 'failed'` after
|
|
28
|
+
`scrape.create`, move that handling into a `catch`.
|
|
29
|
+
- Everything else is untouched: `scrape.get(id)`, bulk item results, and every other endpoint
|
|
30
|
+
still deliver `failed` as data; `requestRaw` (`/v1/fetch`) never parses JSON and is
|
|
31
|
+
unaffected; 204 and empty-body handling are unchanged. `request()` now delegates to
|
|
32
|
+
`requestWithResponse()` so the guard has a single application site — identical in every
|
|
33
|
+
observable respect but the return value, as both were already documented.
|
|
34
|
+
|
|
35
|
+
## [0.9.4] - 2026-09-06 [NEVER PUBLISHED]
|
|
36
|
+
|
|
37
|
+
**This version was cut but NEVER PUBLISHED — npm `latest` goes from 0.9.3 straight to 0.10.0.**
|
|
38
|
+
Publication is triggered by CI on `main` from the version the manifest declares, and 0.10.0
|
|
39
|
+
(#1870) superseded 0.9.4 in the tree before any release reached `main`. Everything below
|
|
40
|
+
reaches consumers in the **0.10.0** artifact, which contains all of it; nothing is installable
|
|
41
|
+
at 0.9.4: `npm i @hunt-labs/bytekit-sdk@0.9.4` fails.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- Bulk fetch, scrape, and mixed-result reads now accept optional cursor, limit, and status
|
|
46
|
+
page parameters while preserving the existing `(id, requestOptions?)` call shape (#3717).
|
|
47
|
+
Generated response types publish stable item identities, page cursors and completeness,
|
|
48
|
+
whole-job counters, exact mixed artifact keys, artifact warnings, bounded webhook recovery
|
|
49
|
+
metadata, and the `artifact_unavailable` error contract.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- Corrected the artifact-URL fallback documentation on `GET /v1/bulk/{id}/screenshots`
|
|
54
|
+
(`image_url`, `content_url`, `markdown_url`) and `GET /v1/logs` (`artifact_url`) (#3717).
|
|
55
|
+
Those fields were documented as null-plus-`artifact_unavailable` when no signature can be
|
|
56
|
+
minted; the servers have always returned the row's stored value unchanged instead — `null`
|
|
57
|
+
while nothing is stored, and otherwise the private unsigned storage URL, which answers `401`
|
|
58
|
+
— and raise no warning. `GET /v1/fetch/bulk/{id}` does null the field and report
|
|
59
|
+
`artifact_unavailable`, and its documentation is unchanged. No server behaviour changed and
|
|
60
|
+
no type moved; the descriptions now match what the endpoints return.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- Regenerated `src/generated/api.d.ts` against the corrected `topup_balance_bytes` description
|
|
65
|
+
on `GET /v1/usage` (#4251). The description read "Remaining purchased top-up bytes", which
|
|
66
|
+
contradicted its own "Equal to `topup_bytes`" clause: the value covers purchased top-up lots
|
|
67
|
+
and the one-time starter grant, which is what the endpoint has always returned. The clause
|
|
68
|
+
recording that the figure is already net of consumed overage, and the "Present only in
|
|
69
|
+
dual/bytes billing mode" clause, are both unchanged and still true. Types are unchanged and
|
|
70
|
+
no server value changed; only the documentation on them moved.
|
|
71
|
+
- Regenerated `src/generated/api.d.ts` against the clarified `cache_age_s` description on
|
|
72
|
+
`ScrapeSuccessEnvelope` (#4237). The description now records that `GET /v1/scrape/{id}`
|
|
73
|
+
reports `cache: hit` without `cache_age_s`: the age is a property of the replay that happened
|
|
74
|
+
at completion time, is not persisted on the row, and cannot be derived honestly afterwards.
|
|
75
|
+
The one-directional "Present only on `cache: hit`" clause is unchanged and still true. Types
|
|
76
|
+
are unchanged; only the documentation on them moved.
|
|
77
|
+
- Regenerated `src/generated/api.d.ts` against the completed `cache` presence list on
|
|
78
|
+
`ScrapeSuccessEnvelope` (#4203). The field's description now names `GET /v1/scrape/{id}` as a
|
|
79
|
+
third delivery path, alongside the synchronous response and webhook delivery: a polled scrape
|
|
80
|
+
now reports the cache disposition its row recorded, and omits the field — rather than
|
|
81
|
+
defaulting it to `miss` — for a row that recorded none. Types are unchanged; only the
|
|
82
|
+
documentation on them moved.
|
|
83
|
+
- Regenerated `src/generated/api.d.ts` against the corrected `PUT /v1/billing/auto-topup`
|
|
84
|
+
request body (#3984). `monthly_cap_cents` is now typed `number | null`: an explicit `null` is
|
|
85
|
+
the wire representation of "no monthly cap", and it is what the endpoint has always accepted —
|
|
86
|
+
the request schema simply never documented it, while the 200 response schema already did.
|
|
87
|
+
Omitting the key while `enabled` is `true` is still rejected with `422`, so send `null`
|
|
88
|
+
rather than dropping the field. No server behaviour changes; the type widens to match what
|
|
89
|
+
the API already accepted.
|
|
90
|
+
- Regenerated `src/generated/api.d.ts` against the corrected monitor `scrape_options`
|
|
91
|
+
documentation (#4024). The description no longer lists `headers` among the accepted
|
|
92
|
+
fields — `POST /v1/monitors` never honoured a caller-supplied `headers` option and now
|
|
93
|
+
rejects it with the same 400 every other capture endpoint returns for the field. Types are
|
|
94
|
+
unchanged; only the documentation on them moved.
|
|
95
|
+
- Regenerated `src/generated/api.d.ts` against the completed `X-Scrape-Proxy-Tier-Label`
|
|
96
|
+
omission list (#4030). The header's description now names a third case in which no tier is
|
|
97
|
+
reported: a response replayed from stored bytes after this request's own cache lookup missed,
|
|
98
|
+
which arrives as `X-Scrape-Cache: miss` and likewise ran no proxy tier. The two cases already
|
|
99
|
+
documented — a cache hit, and responses the fast path did not serve — are unchanged. Types are
|
|
100
|
+
unchanged; only the documentation on them moved.
|
|
101
|
+
- Regenerated `src/generated/api.d.ts` against the qualified `X-Scrape-Proxy-Tier-Label`
|
|
102
|
+
omission list (#4132). The third case that list records — a response replayed from stored bytes
|
|
103
|
+
after this request's own cache lookup missed — is not reachable through this API: every caller
|
|
104
|
+
of the internal fetch path closes the handler branch that produces it, and the branch survives
|
|
105
|
+
only as a deploy-skew safeguard. The description now says so rather than presenting the case as
|
|
106
|
+
ordinary behaviour. The two reachable cases — a cache hit, and responses the fast path did not
|
|
107
|
+
serve — are unchanged. Types are unchanged; only the documentation on them moved.
|
|
108
|
+
|
|
13
109
|
## [0.9.3] - 2026-09-04
|
|
14
110
|
|
|
15
111
|
### Changed
|
package/README.md
CHANGED
|
@@ -300,6 +300,32 @@ try {
|
|
|
300
300
|
}
|
|
301
301
|
```
|
|
302
302
|
|
|
303
|
+
### A failed scrape throws, even though it arrives on a 200
|
|
304
|
+
|
|
305
|
+
`POST /v1/scrape` reports a terminal upstream fetch failure — an unreachable
|
|
306
|
+
host, a refused connection, or a target whose bot protection defeats every proxy
|
|
307
|
+
tier — as **HTTP 200** carrying the canonical `failed` envelope, because a
|
|
308
|
+
definitive upstream outcome is a result rather than a transport error. The SDK
|
|
309
|
+
turns that one body into the same `ByteKitError` you already catch, so a failed
|
|
310
|
+
scrape can never be mistaken for a successful one:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
import { ByteKit, ByteKitError } from '@hunt-labs/bytekit-sdk';
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
await client.scrape.create({ url: 'https://does-not-resolve.invalid' });
|
|
317
|
+
} catch (err) {
|
|
318
|
+
if (err instanceof ByteKitError) {
|
|
319
|
+
// 200 'upstream_error' 'Failed to fetch URL.' (or 'blocked')
|
|
320
|
+
console.error(err.status, err.code, err.message);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`err.status` is the real transport status, `200` — nothing is synthesized. This
|
|
326
|
+
applies to `scrape.create` only. `scrape.get(id)` and bulk item results deliver
|
|
327
|
+
`failed` as **data**, exactly as before: narrow on `result.status` there.
|
|
328
|
+
|
|
303
329
|
### Reading which field was rejected
|
|
304
330
|
|
|
305
331
|
`err.details` carries the API envelope's structured rejection reason when the
|
package/dist/client.d.ts
CHANGED
|
@@ -17,8 +17,8 @@ export interface ByteKitOptions {
|
|
|
17
17
|
* read (so a large legitimate body counts against it too). `0` disables the SDK-side
|
|
18
18
|
* timeout. Default 120000.
|
|
19
19
|
*
|
|
20
|
-
* NOT the same thing as `ScrapeOpts.timeout_ms`, which is the SERVER-side budget
|
|
21
|
-
*
|
|
20
|
+
* NOT the same thing as `ScrapeOpts.timeout_ms`, which is the SERVER-side budget
|
|
21
|
+
* ByteKit applies to the scrape itself. Override per call with
|
|
22
22
|
* `RequestOptions.requestTimeoutMs`.
|
|
23
23
|
*/
|
|
24
24
|
timeoutMs?: number;
|
package/dist/client.js
CHANGED
|
@@ -214,6 +214,53 @@ function parseJsonBody(text) {
|
|
|
214
214
|
}
|
|
215
215
|
return JSON.parse(text);
|
|
216
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Throw a `ByteKitError` for the one 2xx body that is a FAILURE: the terminal
|
|
219
|
+
* `status: 'failed'` envelope `POST /v1/scrape` delivers on HTTP 200 (#1870).
|
|
220
|
+
*
|
|
221
|
+
* The gateway used to report a terminal upstream fetch failure as HTTP 503, and
|
|
222
|
+
* `!response.ok` in {@link ByteKit.send} was this SDK's only failure seam. Now that the
|
|
223
|
+
* same outcome arrives as an intentional 200 — a definitive upstream result is not a
|
|
224
|
+
* transport error, and `id` is `null` because the fetch failed before any job existed —
|
|
225
|
+
* that seam alone would let the failure resolve like a success and silence every caller's
|
|
226
|
+
* `catch`. This restores the exception integrators already write code for, with the
|
|
227
|
+
* envelope's own `code` and `message`.
|
|
228
|
+
*
|
|
229
|
+
* Scope is deliberately narrow — `POST` to exactly `/v1/scrape`:
|
|
230
|
+
*
|
|
231
|
+
* - `GET /v1/scrape/{id}`, bulk item results, and the `scrape.failed` webhook payload
|
|
232
|
+
* have always delivered `failed` as DATA, and a caller polling a job must keep
|
|
233
|
+
* receiving it as a value;
|
|
234
|
+
* - `requestRaw` (`/v1/fetch`) never parses JSON and is not routed through here at all.
|
|
235
|
+
*
|
|
236
|
+
* `status` is the ACTUAL transport status (200). The envelope's optional
|
|
237
|
+
* `error.http_status` is the TARGET's status when one was received, never ours, so it is
|
|
238
|
+
* not substituted here — and the old 503 is not synthesized either.
|
|
239
|
+
*
|
|
240
|
+
* Runs on the already-parsed body, outside `send`'s connection-error catch, so it can
|
|
241
|
+
* never be remapped into a `ByteKitConnectionError`.
|
|
242
|
+
*/
|
|
243
|
+
function throwIfScrapeFailureEnvelope(method, path, data, status, headers) {
|
|
244
|
+
if (method.toUpperCase() !== 'POST' || path !== '/v1/scrape')
|
|
245
|
+
return;
|
|
246
|
+
if (typeof data !== 'object' || data === null)
|
|
247
|
+
return;
|
|
248
|
+
const envelope = data;
|
|
249
|
+
if (envelope.status !== 'failed')
|
|
250
|
+
return;
|
|
251
|
+
const error = typeof envelope.error === 'object' && envelope.error !== null
|
|
252
|
+
? envelope.error
|
|
253
|
+
: {};
|
|
254
|
+
const code = typeof error.code === 'string' ? error.code : 'internal_error';
|
|
255
|
+
const message = typeof error.message === 'string' ? error.message : 'Scrape failed.';
|
|
256
|
+
// Same discipline as the non-2xx decoder above: arrays and scalars are rejected,
|
|
257
|
+
// because `ByteKitErrorDetails` promises something indexable.
|
|
258
|
+
const rawDetails = error.details;
|
|
259
|
+
const details = typeof rawDetails === 'object' && rawDetails !== null && !Array.isArray(rawDetails)
|
|
260
|
+
? rawDetails
|
|
261
|
+
: undefined;
|
|
262
|
+
throw new ByteKitError(status, code, message, details, headers);
|
|
263
|
+
}
|
|
217
264
|
/** The `name` of an error-shaped value, or `undefined` when it carries none. */
|
|
218
265
|
const nameOf = (e) => typeof e === 'object' && e !== null && 'name' in e ? String(e.name) : undefined;
|
|
219
266
|
/** The `cause` of an error-shaped value, or `undefined` when it carries none. */
|
|
@@ -477,8 +524,14 @@ export class ByteKit {
|
|
|
477
524
|
// Only the read runs inside the transport's guarded region (#2546) — `JSON.parse`
|
|
478
525
|
// stays out here (in `parseJsonBody`), so a malformed body still throws its own
|
|
479
526
|
// SyntaxError rather than being remapped to a ByteKitConnectionError.
|
|
480
|
-
|
|
481
|
-
|
|
527
|
+
//
|
|
528
|
+
// #1870: delegated to `requestWithResponse` so the terminal-scrape-failure guard has
|
|
529
|
+
// exactly ONE application site across both public JSON transports, rather than a copy
|
|
530
|
+
// per entrypoint that could drift. The two were already documented as identical in
|
|
531
|
+
// every observable respect but the return value, and the header/status pair the
|
|
532
|
+
// delegate captures is what the thrown `ByteKitError` needs anyway.
|
|
533
|
+
const { data } = await this.requestWithResponse(method, path, body, options);
|
|
534
|
+
return data;
|
|
482
535
|
}
|
|
483
536
|
/**
|
|
484
537
|
* Make an authenticated request whose response body IS the content, not a JSON
|
|
@@ -536,6 +589,11 @@ export class ByteKit {
|
|
|
536
589
|
headers: response.headers,
|
|
537
590
|
status: response.status,
|
|
538
591
|
}));
|
|
539
|
-
|
|
592
|
+
const data = parseJsonBody(text);
|
|
593
|
+
// #1870: the one 2xx body that is a failure. Placed after the parse and outside
|
|
594
|
+
// `send`'s guarded region, so the throw is a ByteKitError and never a remapped
|
|
595
|
+
// connection error.
|
|
596
|
+
throwIfScrapeFailureEnvelope(method, path, data, status, headers);
|
|
597
|
+
return { data, headers, status };
|
|
540
598
|
}
|
|
541
599
|
}
|