@hunt-labs/bytekit-sdk 0.5.0 → 0.6.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,107 @@ 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.6.0] - 2026-08-07
12
+
13
+ ### Added
14
+
15
+ - **`ByteKitError.details`** — the API error envelope's structured rejection reason is now
16
+ reachable from the thrown error. A `422 validation_error` carries
17
+ `{ formErrors, fieldErrors }`, so a caller can finally read WHICH request field was
18
+ rejected rather than only that the request was invalid; a `400 invalid_url` carries a flat
19
+ `{ url, field, reason }`. Typed as `ByteKitErrorDetails | undefined` where
20
+ `ByteKitErrorDetails = Record<string, unknown>` (also exported), deliberately loose because
21
+ the contents vary by error code — narrow the member you read. Strictly ADDITIVE: `details`
22
+ is an optional FOURTH constructor argument, so existing three-argument
23
+ `new ByteKitError(status, code, message)` construction is unchanged, and `status` / `code` /
24
+ `message` are byte-identical. Populated only from the canonical `{ error: { … } }`
25
+ envelope — a flat vendor-masked body (`/v1/search` `502 {"error":"search_provider_error"}`)
26
+ carries no structured detail and grows none. `http_status` and `failed_at` are deliberately
27
+ NOT exposed: `http_status` would duplicate the existing `status` property, and a duplicated
28
+ field that can disagree with its twin is a bug surface. (#2685)
29
+
30
+ - A `tsc --noEmit`-checked parity pin in `src/types.ts` ties the hand-authored search types
31
+ (`SearchOpts`, `SearchResult`, `SearchResultImage`, `SearchResponse` — the SDK's only
32
+ hand-authored request/response types) to the generated `operations['createSearch']`
33
+ request and `200` response. It asserts key-set and optionality parity in both directions
34
+ and assignability in each meaningful direction, so a future spec change that search's
35
+ types do not follow fails the build instead of shipping. Value types are erased before the
36
+ parity comparison, so a deliberate hand-authored refinement (a branded id, a narrowed
37
+ union) cannot trip it. Type-level only — the assertions erase at emit and add nothing to
38
+ `dist/`. (#2678)
39
+
40
+ ### Fixed
41
+
42
+ - **README: the `fetch` sample's `res.cache` type is corrected to include `null`.** The
43
+ documented type read `"hit" | "miss" | "bypass"`, but `FetchResult.cache` is
44
+ `FetchCacheStatus | null` (null when the response carried no recognizable `X-Fetch-Cache`
45
+ header) — the one strict-`tsc` error a real consumption file copied from the README ever
46
+ produced. The line is now a TYPED ASSIGNMENT rather than a comment, so the README compile
47
+ guard (`src/__tests__/readme-compiles.test.ts`) typechecks it: comment-vs-type drift on
48
+ this field now fails the build instead of shipping. Documentation only — no runtime or type
49
+ change. (#2685)
50
+
51
+ - **CHANGELOG: the `[0.1.0]` heading's malformed `- 2026` date is now `- 2026-04-24`**, the
52
+ commit date of `caf17e56b`, which introduced `packages/sdk-typescript/package.json` at
53
+ version `0.1.0`. Every other heading in this file already carried a full ISO date; this one
54
+ broke the Keep a Changelog `## [x.y.z] - YYYY-MM-DD` form. (#2685)
55
+
56
+ - **Test-only: nine SDK test files mocked wire bodies the API can never send.** A
57
+ `monitors.create` 200 (the operation declares 201 as its only success arm), a scrape 200
58
+ carrying `data: { markdown }` (the envelope has no `data` member — content lives under
59
+ `formats`), a `screenshots.getWithResponse` mock asserting `X-Screenshot-*` headers that
60
+ `GET /v1/screenshots/{id}` declares on no arm, error bodies omitting the `Error` schema's
61
+ required `status: 'failed'` / `http_status` / `failed_at`, and twelve `402`/`429` arms on
62
+ operations that declare neither status. Each is rewritten to a spec-valid shape with a
63
+ comment naming the arm it now matches, and every test keeps its original assertion target.
64
+ No shipped code changed. (#2685)
65
+
66
+ - **`SearchResult.snippet` is now optional (`snippet?: string`) — BREAKING AT COMPILE TIME.**
67
+ `type: 'images'` search results omit `snippet` entirely (the spec has always declared a
68
+ result's `required` set as `[position, title, url]`), but the SDK typed it as a required
69
+ `string`. `hit.snippet.length` therefore compiled under strict TypeScript and threw
70
+ `TypeError: Cannot read properties of undefined` on every images hit — the TypeScript twin
71
+ of the Python SDK's #2579 images `KeyError`. **Consumers reading `.snippet` unguarded now
72
+ see `string | undefined`** and must narrow it (`if (typeof hit.snippet === 'string')` or
73
+ `hit.snippet ?? ''`), exactly as they already do for `date`. No runtime behavior and no
74
+ wire bytes change: the field was already absent from those payloads. `web`/`news` results
75
+ are unaffected — they still carry `snippet`, and a narrowed read yields the same string.
76
+ (#2678)
77
+
78
+ - **Path parameters are now percent-encoded.** Every id-bearing method built its request path
79
+ with a raw template literal, so a crafted or interpolated id silently retargeted the
80
+ request: `scrape.get('sc_/../../account')` addressed `GET /v1/account` once WHATWG URL
81
+ parsing collapsed the `../` segments, and `monitors.get('mon_x?admin=1#frag')` turned the id
82
+ into a real query string plus fragment. The branded id types constrain only the PREFIX of an
83
+ id, never its tail, so they were never a defence. An id now travels as one percent-encoded
84
+ path segment — a malformed one 404s server-side instead of reaching a different resource.
85
+ Ports the Python SDK's #2593 fix. (#2677)
86
+
87
+ ### Changed
88
+
89
+ - Request paths are assembled by a single internal `buildPath()` helper in `client.ts` rather
90
+ than by per-resource template literals. It encodes SEGMENTS only — every fixed segment and
91
+ every `/` separator stays literal — and encodes each value exactly once, so an id that
92
+ already contains a `%` is not double-encoded. **A well-formed id (`sc_<hex>`, `mon_<hex>`, …)
93
+ produces a byte-identical URL to before**, on every id-bearing method: encoding is a no-op
94
+ for those characters, so no existing call changes on the wire. (#2677)
95
+
96
+ - Four of the guards shipped above could not fail on the change they appeared to protect, and
97
+ now can. `buildPath()`'s two `TypeError` throws — the fail-loud that replaced the
98
+ compile-time checking a template literal used to give — each survived its own removal with
99
+ the whole suite green, and are now pinned by tests separated by input shape rather than by
100
+ message text. The in-suite path-helper gate read its directory non-recursively while the
101
+ shell grep it reproduces is `-r`, so an offender one directory down was invisible to it; it
102
+ now walks the whole of `src/`, a strict superset of the shell gate's set. The `images`
103
+ search-result literal's key ABSENCE (no `snippet`, no `date`) was held only by a
104
+ one-directional subtype check, which an extra key cannot break; it is now pinned by explicit
105
+ key-absence assertions plus an exact key-set assertion. And the spec's `error.details` —
106
+ read at one site in `client.ts` off a hand-written inline type — is now tied to the
107
+ generated schema by a PRESENCE pin, so dropping the field from `docs/api/openapi.yaml` fails
108
+ the build. Every pin is key-level, never value-level, so a deliberate hand-authored value
109
+ refinement still cannot trip it. Test- and type-level only: no runtime behavior, no exported
110
+ type, and nothing in `dist/` changes. (#2793)
111
+
11
112
  ## [0.5.0] - 2026-07-29
12
113
 
13
114
  ### Added
@@ -152,7 +253,12 @@ today, see [README.md](./README.md), which remains the authoritative usage guide
152
253
  rebuilding the shared `dist`), which is now fixed. No consumer-facing changes
153
254
  beyond 0.2.1.
154
255
 
155
- ## [0.2.1] - 2026-07-16
256
+ ## [0.2.1] - 2026-07-16 [NEVER PUBLISHED]
257
+
258
+ > **Never published to npm.** This version was cut in-tree but no `@hunt-labs/bytekit-sdk@0.2.1`
259
+ > tarball exists — `npm i @hunt-labs/bytekit-sdk@0.2.1` fails. Everything below shipped to users in
260
+ > **0.2.2**, which is the first published release containing it. Reconciled by issue #2680; the
261
+ > `release-drift` CI gate now blocks a new phantom heading from being introduced.
156
262
 
157
263
  ### Changed
158
264
 
@@ -212,7 +318,7 @@ against 0.1.0 — read each one before upgrading.
212
318
  - Publish-ready packaging metadata: types-first `exports`, `engines`, `keywords`,
213
319
  `homepage`, `bugs`, `README.md`, `LICENSE`, and an MIT `license` field.
214
320
 
215
- ## [0.1.0] - 2026
321
+ ## [0.1.0] - 2026-04-24
216
322
 
217
323
  ### Added
218
324
 
@@ -220,6 +326,7 @@ against 0.1.0 — read each one before upgrading.
220
326
  (scrape, screenshot, search, and related endpoints), published under the original
221
327
  `@rapidcrawl/sdk` name.
222
328
 
329
+ [0.6.0]: https://github.com/Hunt-Labs-Inc/ByteKit/tree/main/packages/sdk-typescript
223
330
  [0.5.0]: https://github.com/Hunt-Labs-Inc/ByteKit/tree/main/packages/sdk-typescript
224
331
  [0.4.0]: https://github.com/Hunt-Labs-Inc/ByteKit/tree/main/packages/sdk-typescript
225
332
  [0.3.0]: https://github.com/Hunt-Labs-Inc/ByteKit/tree/main/packages/sdk-typescript
package/README.md CHANGED
@@ -77,16 +77,24 @@ binary content (images, PDFs, etc. — `/v1/fetch` supports raw passthrough)
77
77
  use `bytes` instead, which is byte-for-byte identical to the wire content:
78
78
 
79
79
  ```ts
80
+ import type { FetchCacheStatus } from '@hunt-labs/bytekit-sdk';
81
+
80
82
  const res = await client.fetch.create({ url: 'https://example.com' });
81
83
 
82
84
  console.log(res.body); // raw content (e.g. "<!doctype html>…") — UTF-8 decoded
83
85
  console.log(res.bytes); // Uint8Array — byte-exact, safe for binary content
84
86
  console.log(res.id); // X-Fetch-ID, e.g. "ft_0123abcd…"
85
87
  console.log(res.creditsCharged); // X-Fetch-Credits-Charged, as a number
86
- console.log(res.cache); // "hit" | "miss" | "bypass"
87
88
  console.log(res.custom); // base64 X-Fetch-Custom echo, or null
88
89
  console.log(res.contentType); // response Content-Type, e.g. "text/html"
89
90
 
91
+ // `cache` is NULLABLE — null when the response carried no X-Fetch-Cache header
92
+ // (or an unrecognized value), so narrow it before use. The annotation is
93
+ // deliberate: the README compile guard typechecks this line, so the documented
94
+ // type can no longer drift from the real one.
95
+ const cache: FetchCacheStatus | null = res.cache; // "hit" | "miss" | "bypass" | null
96
+ if (cache !== null) console.log(cache);
97
+
90
98
  // Anything not surfaced as a named field is on res.headers:
91
99
  console.log(res.headers.get('X-Fetch-Waf-Vendor'));
92
100
  ```
@@ -97,6 +105,9 @@ console.log(res.headers.get('X-Fetch-Waf-Vendor'));
97
105
  const results = await client.search.query({ query: 'ai web scraping', limit: 10 });
98
106
  for (const hit of results.results) {
99
107
  console.log(hit.position, hit.title, hit.url);
108
+ // `snippet` and `date` are present for `web`/`news` results and omitted
109
+ // entirely for `type: 'images'` — read them behind a guard.
110
+ if (typeof hit.snippet === 'string') console.log(hit.snippet);
100
111
  }
101
112
  ```
102
113
 
@@ -239,6 +250,42 @@ try {
239
250
  }
240
251
  ```
241
252
 
253
+ ### Reading which field was rejected
254
+
255
+ `err.details` carries the API envelope's structured rejection reason when the
256
+ response supplied one, and is `undefined` otherwise. Its type is
257
+ `ByteKitErrorDetails | undefined`, where `ByteKitErrorDetails` is
258
+ `Record<string, unknown>` — deliberately loose, because the contents vary by
259
+ error code:
260
+
261
+ - a `422 validation_error` carries `{ formErrors, fieldErrors }`, and
262
+ `fieldErrors` names exactly which request field failed;
263
+ - a `400 invalid_url` carries a flat `{ url, field, reason }`.
264
+
265
+ Narrow the member you want before using it:
266
+
267
+ ```ts
268
+ import { ByteKit, ByteKitError } from '@hunt-labs/bytekit-sdk';
269
+
270
+ try {
271
+ await client.scrape.create({ url: 'https://example.com' });
272
+ } catch (err) {
273
+ if (err instanceof ByteKitError && err.status === 422) {
274
+ const fieldErrors = err.details?.['fieldErrors'];
275
+ if (typeof fieldErrors === 'object' && fieldErrors !== null) {
276
+ // e.g. { formats: ["Invalid enum value. Expected 'markdown' | …"] }
277
+ console.error(Object.keys(fieldErrors));
278
+ }
279
+ }
280
+ }
281
+ ```
282
+
283
+ Note that `details` is populated only from the API's canonical
284
+ `{ error: { … } }` envelope. A few endpoints answer with a flat, vendor-masked
285
+ body instead (`/v1/search` returns `502 {"error":"search_provider_error"}`);
286
+ those carry no structured detail, so `err.details` stays `undefined` there —
287
+ `err.code` is still the machine-readable reason.
288
+
242
289
  ## Documentation
243
290
 
244
291
  Full API reference: [bytekit.com/docs](https://bytekit.com/docs).
package/dist/client.d.ts CHANGED
@@ -69,11 +69,57 @@ export interface ApiResponse<T> {
69
69
  }
70
70
  /** Why a request never produced an HTTP response. */
71
71
  export type ConnectionErrorCode = 'timeout' | 'aborted' | 'connection';
72
+ /**
73
+ * Structured, machine-readable detail about WHY a request was rejected — the
74
+ * `error.details` member of the API's canonical error envelope
75
+ * (`docs/api/openapi.yaml` §components.schemas.Error, issue #2685).
76
+ *
77
+ * The spec declares the field with no schema at all ("Optional structured details
78
+ * (e.g. which fields failed validation)"), because its contents vary by error code.
79
+ * Two shapes are live today and both must survive unchanged:
80
+ *
81
+ * - A 422 `validation_error` carries zod's `flatten()` output —
82
+ * `{ formErrors: string[], fieldErrors: Record<string, string[]> }` — which is
83
+ * the shape that names WHICH field failed.
84
+ * - A 400 `invalid_url` carries a flat record — `{ url, field, reason }` — with
85
+ * none of zod's keys.
86
+ *
87
+ * It is therefore typed as a loose record rather than a union of the shapes seen
88
+ * so far: a narrower type would have to drop or mangle the next variant the API
89
+ * adds, and the SDK must never lose information the envelope carried. Read a
90
+ * member and narrow it yourself:
91
+ *
92
+ * ```ts
93
+ * const fieldErrors = err.details?.['fieldErrors'];
94
+ * if (fieldErrors !== undefined && typeof fieldErrors === 'object') { … }
95
+ * ```
96
+ */
97
+ export type ByteKitErrorDetails = Record<string, unknown>;
72
98
  /** Thrown when the API responds with a 4xx or 5xx status code. */
73
99
  export declare class ByteKitError extends Error {
74
100
  readonly status: number;
75
101
  readonly code: string;
76
- constructor(status: number, code: string, message: string);
102
+ /**
103
+ * The envelope's `error.details`, when it carried one — `undefined` otherwise.
104
+ *
105
+ * Populated ONLY from the canonical `{ error: { … } }` envelope. A flat
106
+ * vendor-masked body (`/v1/search` 502 `{"error":"search_provider_error"}`)
107
+ * has no envelope to read a detail from, so it never grows one (#2685 E3).
108
+ *
109
+ * A `details` that is not a JSON object — which the untyped spec field
110
+ * technically permits — is dropped rather than surfaced, because the declared
111
+ * type promises something indexable and a value that contradicts its own type
112
+ * is worse than an absent one.
113
+ *
114
+ * `declare` is load-bearing. `target: ES2022` turns on `useDefineForClassFields`,
115
+ * so a plain `details?: …` declaration would emit a class field and DEFINE the
116
+ * key as `undefined` on every instance — every error the SDK has ever thrown
117
+ * would silently grow a `details` key, changing `'details' in err`, spreads, and
118
+ * `JSON.stringify` output for errors that carry none. `declare` emits no field,
119
+ * so the key exists only when the envelope really supplied one.
120
+ */
121
+ readonly details?: ByteKitErrorDetails;
122
+ constructor(status: number, code: string, message: string, details?: ByteKitErrorDetails);
77
123
  }
78
124
  /**
79
125
  * Thrown when the request never produced an HTTP response at all — DNS, socket, TLS,
@@ -88,6 +134,43 @@ export declare class ByteKitConnectionError extends Error {
88
134
  cause?: unknown;
89
135
  });
90
136
  }
137
+ /**
138
+ * Assemble an API request path from a fixed template and caller-supplied dynamic segments
139
+ * (issue #2677).
140
+ *
141
+ * This is the SINGLE place the SDK turns a caller-supplied id into a URL, mirroring the
142
+ * Python SDK's single-site fix (#2593). Every resource routes through it; no resource
143
+ * interpolates an id into a path template of its own. Before it existed, every path was a
144
+ * raw template literal, so a crafted or interpolated id silently retargeted the request —
145
+ * `scrape.get('sc_/../../account')` addressed `GET /v1/account` once WHATWG URL parsing
146
+ * collapsed the `../` segments, and an id carrying `?` or `#` grew a real query string and
147
+ * fragment. Branded id types (`sc_${string}`) constrain the PREFIX, never the tail, so they
148
+ * were never a defence.
149
+ *
150
+ * `template` is an OpenAPI-style path whose `{name}` placeholders are the only parts that
151
+ * get encoded. Everything else — including every `/` separator and every fixed segment —
152
+ * is emitted verbatim: the helper encodes SEGMENTS, never the assembled path. Lifting the
153
+ * encoding to the whole string would turn `/v1/scrape/{id}` into one giant escaped segment
154
+ * and break every request.
155
+ *
156
+ * Each value is passed through `encodeURIComponent` EXACTLY once, so an id that already
157
+ * contains a `%` becomes `%25…` rather than being double-encoded.
158
+ *
159
+ * `query` is appended verbatim after the path. It is the caller's job to hand over an
160
+ * ALREADY-ENCODED string — `toQueryString()` and `URLSearchParams.toString()` both do that
161
+ * — because a query string is a composed `?a=1&b=2` structure whose separators must survive,
162
+ * exactly as the path's separators do.
163
+ *
164
+ * A `{name}` with no matching value, or a value with no matching `{name}`, throws a
165
+ * `TypeError`: a typo in either half would otherwise ship a literal `{id}` in the URL or
166
+ * silently drop the id.
167
+ */
168
+ export declare function buildPath(template: string, options?: {
169
+ /** Dynamic path segments by placeholder name. Each is percent-encoded exactly once. */
170
+ readonly segments?: Readonly<Record<string, string>>;
171
+ /** An already-encoded query string, including its leading `?`. Appended verbatim. */
172
+ readonly query?: string;
173
+ }): string;
91
174
  export declare class ByteKit {
92
175
  private readonly apiKey;
93
176
  private readonly baseUrl;
@@ -125,7 +208,8 @@ export declare class ByteKit {
125
208
  * - `ByteKitConnectionError` when no HTTP response was produced at all
126
209
  * (DNS/socket/TLS failure, client-side timeout, or caller abort) — including one
127
210
  * that fires while the body is streaming.
128
- * - `ByteKitError` on any 4xx/5xx, carrying the API's `code`/`message`.
211
+ * - `ByteKitError` on any 4xx/5xx, carrying the API's `code`/`message` — plus the
212
+ * envelope's structured `details` when it carried one (#2685).
129
213
  *
130
214
  * The body is read through a continuation each caller supplies — JSON text for
131
215
  * `request()`, raw bytes for `requestRaw()`. This is the single place that owns auth,
package/dist/client.js CHANGED
@@ -21,11 +21,16 @@ const DEFAULT_TIMEOUT_MS = 120_000;
21
21
  export class ByteKitError extends Error {
22
22
  status;
23
23
  code;
24
- constructor(status, code, message) {
24
+ constructor(status, code, message, details) {
25
25
  super(message);
26
26
  this.name = 'ByteKitError';
27
27
  this.status = status;
28
28
  this.code = code;
29
+ // Assigned conditionally under `exactOptionalPropertyTypes`: a three-argument
30
+ // construction leaves the instance without the key at all, exactly as before.
31
+ if (details !== undefined) {
32
+ this.details = details;
33
+ }
29
34
  }
30
35
  }
31
36
  /**
@@ -44,6 +49,59 @@ export class ByteKitConnectionError extends Error {
44
49
  }
45
50
  }
46
51
  const NOOP = () => { };
52
+ /** A `{name}` placeholder in a {@link buildPath} template. */
53
+ const PATH_PLACEHOLDER = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
54
+ /**
55
+ * Assemble an API request path from a fixed template and caller-supplied dynamic segments
56
+ * (issue #2677).
57
+ *
58
+ * This is the SINGLE place the SDK turns a caller-supplied id into a URL, mirroring the
59
+ * Python SDK's single-site fix (#2593). Every resource routes through it; no resource
60
+ * interpolates an id into a path template of its own. Before it existed, every path was a
61
+ * raw template literal, so a crafted or interpolated id silently retargeted the request —
62
+ * `scrape.get('sc_/../../account')` addressed `GET /v1/account` once WHATWG URL parsing
63
+ * collapsed the `../` segments, and an id carrying `?` or `#` grew a real query string and
64
+ * fragment. Branded id types (`sc_${string}`) constrain the PREFIX, never the tail, so they
65
+ * were never a defence.
66
+ *
67
+ * `template` is an OpenAPI-style path whose `{name}` placeholders are the only parts that
68
+ * get encoded. Everything else — including every `/` separator and every fixed segment —
69
+ * is emitted verbatim: the helper encodes SEGMENTS, never the assembled path. Lifting the
70
+ * encoding to the whole string would turn `/v1/scrape/{id}` into one giant escaped segment
71
+ * and break every request.
72
+ *
73
+ * Each value is passed through `encodeURIComponent` EXACTLY once, so an id that already
74
+ * contains a `%` becomes `%25…` rather than being double-encoded.
75
+ *
76
+ * `query` is appended verbatim after the path. It is the caller's job to hand over an
77
+ * ALREADY-ENCODED string — `toQueryString()` and `URLSearchParams.toString()` both do that
78
+ * — because a query string is a composed `?a=1&b=2` structure whose separators must survive,
79
+ * exactly as the path's separators do.
80
+ *
81
+ * A `{name}` with no matching value, or a value with no matching `{name}`, throws a
82
+ * `TypeError`: a typo in either half would otherwise ship a literal `{id}` in the URL or
83
+ * silently drop the id.
84
+ */
85
+ export function buildPath(template, options) {
86
+ const segments = options?.segments;
87
+ const filled = new Set();
88
+ const path = template.replace(PATH_PLACEHOLDER, (_match, name) => {
89
+ const value = segments === undefined ? undefined : segments[name];
90
+ if (value === undefined) {
91
+ throw new TypeError(`ByteKit: no value supplied for path segment "${name}" in "${template}"`);
92
+ }
93
+ filled.add(name);
94
+ return encodeURIComponent(String(value));
95
+ });
96
+ if (segments !== undefined) {
97
+ for (const name of Object.keys(segments)) {
98
+ if (!filled.has(name)) {
99
+ throw new TypeError(`ByteKit: path template "${template}" has no "{${name}}" placeholder`);
100
+ }
101
+ }
102
+ }
103
+ return `${path}${options?.query ?? ''}`;
104
+ }
47
105
  /**
48
106
  * Validate a timeout option. `undefined` passes through; anything that is not a
49
107
  * non-negative finite number throws a `TypeError` naming the option as the caller
@@ -279,7 +337,8 @@ export class ByteKit {
279
337
  * - `ByteKitConnectionError` when no HTTP response was produced at all
280
338
  * (DNS/socket/TLS failure, client-side timeout, or caller abort) — including one
281
339
  * that fires while the body is streaming.
282
- * - `ByteKitError` on any 4xx/5xx, carrying the API's `code`/`message`.
340
+ * - `ByteKitError` on any 4xx/5xx, carrying the API's `code`/`message` — plus the
341
+ * envelope's structured `details` when it carried one (#2685).
283
342
  *
284
343
  * The body is read through a continuation each caller supplies — JSON text for
285
344
  * `request()`, raw bytes for `requestRaw()`. This is the single place that owns auth,
@@ -323,6 +382,7 @@ export class ByteKit {
323
382
  if (!response.ok) {
324
383
  let code = 'internal_error';
325
384
  let message = response.statusText;
385
+ let details;
326
386
  try {
327
387
  const json = (await response.json());
328
388
  // Canonical unified envelope. Tested first, so it always wins: the flat
@@ -332,6 +392,15 @@ export class ByteKit {
332
392
  if (typeof json.error === 'object' && json.error !== null) {
333
393
  code = json.error.code ?? code;
334
394
  message = json.error.message ?? message;
395
+ // Structured rejection reason (issue #2685). This is the ONLY site
396
+ // that reads it: the flat branch below has no envelope to read from
397
+ // and must never grow a phantom `details` (E3). Arrays and scalars
398
+ // are rejected because `ByteKitErrorDetails` promises an indexable
399
+ // record — see the type's doc comment.
400
+ const raw = json.error.details;
401
+ if (typeof raw === 'object' && raw !== null && !Array.isArray(raw)) {
402
+ details = raw;
403
+ }
335
404
  }
336
405
  else if (typeof json.error === 'string' && json.error.length > 0) {
337
406
  // Flat vendor-masked shape, e.g. /v1/search 502
@@ -347,7 +416,7 @@ export class ByteKit {
347
416
  // leave defaults — including when the error body itself stalls and the
348
417
  // budget aborts the read (#2546), so a stuck 500 still surfaces its status.
349
418
  }
350
- throw new ByteKitError(response.status, code, message);
419
+ throw new ByteKitError(response.status, code, message, details);
351
420
  }
352
421
  try {
353
422
  return await readBody(response);
@@ -330,7 +330,19 @@ export interface paths {
330
330
  path?: never;
331
331
  cookie?: never;
332
332
  };
333
- /** Get fetch bulk job status */
333
+ /**
334
+ * Get fetch bulk job status
335
+ * @description Returns the job envelope plus its per-URL items.
336
+ *
337
+ * **Downloading item content.** Each completed item's `content_url` is a
338
+ * **pre-signed** URL, minted fresh on every read of this endpoint and valid
339
+ * for **600 seconds (10 minutes)** — the same signing lifetime as the
340
+ * `image_url` returned by `GET /v1/bulk/{id}/screenshots`. The underlying
341
+ * objects are private, so an unsigned URL returns `401`: download the
342
+ * content inside that window, or re-poll this endpoint for a fresh
343
+ * signature. Items that are not `completed`, and completed items with no
344
+ * stored object, carry `null` or the unsigned stored URL instead.
345
+ */
334
346
  get: operations["getFetchBulk"];
335
347
  put?: never;
336
348
  post?: never;
@@ -407,7 +419,20 @@ export interface paths {
407
419
  path?: never;
408
420
  cookie?: never;
409
421
  };
410
- /** List bulk job items */
422
+ /**
423
+ * List bulk job items
424
+ * @description Returns the bulk job's items, discriminated by `type`.
425
+ *
426
+ * **Downloading item artifacts.** `image_url` (`type: screenshot`) and both
427
+ * `content_url` and `markdown_url` (`type: scrape`) are **pre-signed** URLs,
428
+ * minted fresh on every read of this endpoint and valid for **600 seconds
429
+ * (10 minutes)** —
430
+ * the same signing lifetime as `content_url` on `GET /v1/fetch/bulk/{id}`.
431
+ * The underlying objects are private, so an unsigned URL returns `401`:
432
+ * download inside that window, or re-poll this endpoint for a fresh
433
+ * signature. Items that are not `completed`, and completed items with no
434
+ * stored object, carry `null` or the unsigned stored URL instead.
435
+ */
411
436
  get: operations["listBulkScreenshots"];
412
437
  put?: never;
413
438
  post?: never;
@@ -772,9 +797,9 @@ export interface paths {
772
797
  get?: never;
773
798
  put?: never;
774
799
  /**
775
- * Cancel subscription
776
- * @description Immediately cancels the Stripe subscription with proration.
777
- * Takes a balance snapshot for 6-month grace-period restoration on re-subscribe.
800
+ * Legacy cancellation compatibility terminal
801
+ * @description Returns 410. Customer cancellation is initiated through the hosted
802
+ * Billing Portal and projected through Stripe webhooks.
778
803
  */
779
804
  post: operations["cancelSubscription"];
780
805
  delete?: never;
@@ -3113,13 +3138,41 @@ export interface operations {
3113
3138
  "application/json": components["schemas"]["Error"];
3114
3139
  };
3115
3140
  };
3116
- /** @description Validation error or screenshot failed. */
3141
+ /**
3142
+ * @description Validation error, or a screenshot that failed inside the sync hold.
3143
+ * The two carry DIFFERENT bodies (issue #2679):
3144
+ *
3145
+ * - A request-validation failure returns the unified `Error` envelope
3146
+ * with code `validation_error`.
3147
+ * - A capture that reached a terminal `failed` state inside the sync
3148
+ * hold returns the full `ScreenshotResponse` resource, whose
3149
+ * `error_code`/`error_message` carry the real failure (`blocked`,
3150
+ * `timeout`, …). This is the canonical shape for that case: the
3151
+ * resource body is what carries the `ss_` handle, `poll_url` and
3152
+ * `expires_at` a client needs to redeem the failed capture, none of
3153
+ * which fit the `Error` envelope.
3154
+ *
3155
+ * Discriminate on the shape of the top-level `error` key. The envelope
3156
+ * form carries `error` as an OBJECT (`{ code, message, http_status,
3157
+ * failed_at }`); the resource form has no `error` key at all, and
3158
+ * carries the flat `error_code`/`error_message` pair plus `id` instead.
3159
+ *
3160
+ * Do NOT discriminate on `status`. Both forms carry
3161
+ * `"status": "failed"` on this response — `Error.status` is a required
3162
+ * property with `enum: [failed]` — so it separates nothing here.
3163
+ *
3164
+ * `X-Screenshot-ID` accompanies the resource form only: a
3165
+ * request-validation failure is raised before a screenshot row exists,
3166
+ * so no screenshot headers travel with it.
3167
+ */
3117
3168
  422: {
3118
3169
  headers: {
3170
+ /** @description Stable screenshot identifier (matches `id` in the resource body). Emitted only on the failed-capture variant; absent on a request-validation failure. */
3171
+ "X-Screenshot-ID"?: string;
3119
3172
  [name: string]: unknown;
3120
3173
  };
3121
3174
  content: {
3122
- "application/json": components["schemas"]["Error"];
3175
+ "application/json": components["schemas"]["Error"] | components["schemas"]["ScreenshotResponse"];
3123
3176
  };
3124
3177
  };
3125
3178
  /** @description Rate limit (`rate_limited`) or concurrency limit (`concurrency_limit`) exceeded. Carries a `Retry-After` header — seconds derived from the same clock as `X-RateLimit-Reset` for the rate-limit variant, a fixed small back-off for the concurrency variant. */
@@ -3947,7 +4000,10 @@ export interface operations {
3947
4000
  webhook_url: string;
3948
4001
  /** @description Per-URL fetch items, oldest first, capped at 500 (listBulkFetchItemsWithLatestAttempt). */
3949
4002
  items: {
3950
- /** @example sc_01j9abc123 */
4003
+ /**
4004
+ * @description Scrape-row id, `sc_`-prefixed. Fetch-bulk items are rows of the same resource sync `/v1/scrape` returns, so they share its prefix. Not to be confused with the `ft_`-prefixed `X-Fetch-ID` correlation header on sync `/v1/fetch`, which identifies a request rather than a stored item.
4005
+ * @example sc_01j9abc123
4006
+ */
3951
4007
  id: string;
3952
4008
  /** Format: uri */
3953
4009
  url: string;
@@ -3955,6 +4011,7 @@ export interface operations {
3955
4011
  status: "pending" | "processing" | "completed" | "failed" | "cancelled";
3956
4012
  content_type: string | null;
3957
4013
  content_length: number | null;
4014
+ /** @description Pre-signed download URL for the fetched body, valid for 600 seconds (10 minutes) from the moment this response was generated — matching the signing TTL of `image_url` on `GET /v1/bulk/{id}/screenshots`. A new signature is minted on every poll. Null for items with no stored body; a completed item whose object cannot be signed falls back to the unsigned stored URL, which returns 401 on download. */
3958
4015
  content_url: string | null;
3959
4016
  waf_vendor: string | null;
3960
4017
  cache_hit: boolean;
@@ -4412,7 +4469,7 @@ export interface operations {
4412
4469
  format?: string;
4413
4470
  /** @description `type: recording` only. */
4414
4471
  video_url?: string | null;
4415
- /** @description `type: scrape` only. */
4472
+ /** @description `type: scrape` only. Pre-signed download URL for the scraped body, valid for 600 seconds (10 minutes) from the moment this response was generated — matching the signing TTL of `image_url` above and of `content_url` on `GET /v1/fetch/bulk/{id}`. A new signature is minted on every poll. Null for items with no stored body; a completed item whose object cannot be signed falls back to the unsigned stored URL, which returns 401 on download. */
4416
4473
  content_url?: string | null;
4417
4474
  /** @description `type: scrape` only. */
4418
4475
  content_type?: string | null;
@@ -4420,7 +4477,7 @@ export interface operations {
4420
4477
  content_length?: number | null;
4421
4478
  /** @description `type: scrape` only. Always null in bulk listings — fetch the body via `markdown_url`. */
4422
4479
  markdown?: string | null;
4423
- /** @description `type: scrape` only. */
4480
+ /** @description `type: scrape` only. Pre-signed download URL for the markdown rendering of the page, valid for 600 seconds (10 minutes) from the moment this response was generated — the same signing TTL as `content_url` and `image_url` above. A new signature is minted on every poll. Null for items with no stored markdown; a completed item whose object cannot be signed falls back to the unsigned stored URL, which returns 401 on download. */
4424
4481
  markdown_url?: string | null;
4425
4482
  }[];
4426
4483
  };
@@ -5210,10 +5267,10 @@ export interface operations {
5210
5267
  /** @description Plan base price in cents. */
5211
5268
  plan_price_cents: number;
5212
5269
  /**
5213
- * @description Derived plan category. "payg" for pay-as-you-go plans, "free" for the free tier, "subscription" for all others.
5270
+ * @description Derived plan category. "payg" for pay-as-you-go plans, "subscription" for all recurring paid plans.
5214
5271
  * @enum {string}
5215
5272
  */
5216
- plan_type: "payg" | "free" | "subscription";
5273
+ plan_type: "payg" | "subscription";
5217
5274
  /** @description Total top-up spend in cents for this billing period. Present only in dual/bytes billing mode. */
5218
5275
  topup_spent_cents?: number;
5219
5276
  /** @description Remaining top-up bytes: max(0, topup_bytes − max(0, bytes_used − included_bytes − rollover_bytes)). Present only in dual/bytes billing mode. */
@@ -5418,7 +5475,7 @@ export interface operations {
5418
5475
  error_message?: string | null;
5419
5476
  /** @description Prefixed key id (ak_) that issued the request. */
5420
5477
  api_key_id?: string | null;
5421
- /** @description Stored content/image/video URL, if any. */
5478
+ /** @description Stored content, image or video URL — null when the row has no stored object. For a `completed` row with a stored object this is a **pre-signed** download URL, minted fresh on every read of this endpoint and valid for **600 seconds (10 minutes)** — the same signing lifetime as `content_url` on `GET /v1/fetch/bulk/{id}` and `image_url` on `GET /v1/bulk/{id}/screenshots`. The underlying objects are private, so an unsigned URL returns 401: download inside that window, or re-poll this endpoint for a fresh signature. Rows that are not `completed`, and completed rows whose object cannot be signed, fall back to the unsigned stored URL, which returns 401 on download. */
5422
5479
  artifact_url?: string | null;
5423
5480
  /** @description The original request options. */
5424
5481
  options?: {
@@ -5488,6 +5545,8 @@ export interface operations {
5488
5545
  requestBody: {
5489
5546
  content: {
5490
5547
  "application/json": {
5548
+ /** @enum {string} */
5549
+ purpose: "payment_method_update" | "invoice_history" | "subscription_cancel";
5491
5550
  /** Format: uri */
5492
5551
  return_url: string;
5493
5552
  };
@@ -5515,6 +5574,15 @@ export interface operations {
5515
5574
  "application/json": components["schemas"]["Error"];
5516
5575
  };
5517
5576
  };
5577
+ /** @description Cancellation disclosure revision is absent or stale. */
5578
+ 409: {
5579
+ headers: {
5580
+ [name: string]: unknown;
5581
+ };
5582
+ content: {
5583
+ "application/json": components["schemas"]["Error"];
5584
+ };
5585
+ };
5518
5586
  };
5519
5587
  };
5520
5588
  registerPayg: {
@@ -5714,23 +5782,8 @@ export interface operations {
5714
5782
  };
5715
5783
  };
5716
5784
  responses: {
5717
- /** @description Successful response. */
5718
- 200: {
5719
- headers: {
5720
- [name: string]: unknown;
5721
- };
5722
- content: {
5723
- "application/json": {
5724
- cancelled: boolean;
5725
- /** @enum {string} */
5726
- refund: "prorated";
5727
- /** Format: date-time */
5728
- grace_period_expires_at: string;
5729
- };
5730
- };
5731
- };
5732
- /** @description No subscription or already cancelled. */
5733
- 400: {
5785
+ /** @description Cancellation moved to hosted Billing Portal. */
5786
+ 410: {
5734
5787
  headers: {
5735
5788
  [name: string]: unknown;
5736
5789
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { ByteKit, ByteKitError, ByteKitConnectionError } from './client.js';
2
- export type { ByteKitOptions, RequestOptions, ApiResponse, ConnectionErrorCode } from './client.js';
2
+ export type { ByteKitOptions, RequestOptions, ApiResponse, ConnectionErrorCode, ByteKitErrorDetails, } 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';
@@ -1,3 +1,4 @@
1
+ import { buildPath } from '../client.js';
1
2
  export class AccountResource {
2
3
  client;
3
4
  apiKeys;
@@ -35,11 +36,11 @@ class ApiKeysResource {
35
36
  }
36
37
  /** GET /v1/account/api-keys/{id} — get an API key by ID */
37
38
  get(id, requestOptions) {
38
- return this.client.request('GET', `/v1/account/api-keys/${id}`, undefined, requestOptions);
39
+ return this.client.request('GET', buildPath('/v1/account/api-keys/{id}', { segments: { id } }), undefined, requestOptions);
39
40
  }
40
41
  /** PATCH /v1/account/api-keys/{id} — update an API key */
41
42
  update(id, opts, requestOptions) {
42
- return this.client.request('PATCH', `/v1/account/api-keys/${id}`, opts, requestOptions);
43
+ return this.client.request('PATCH', buildPath('/v1/account/api-keys/{id}', { segments: { id } }), opts, requestOptions);
43
44
  }
44
45
  /**
45
46
  * DELETE /v1/account/api-keys/{id} — revoke an API key (idempotent).
@@ -53,10 +54,10 @@ class ApiKeysResource {
53
54
  * method resolves cleanly on the gateway's empty 204 body.
54
55
  */
55
56
  revoke(id, requestOptions) {
56
- return this.client.request('DELETE', `/v1/account/api-keys/${id}`, undefined, requestOptions);
57
+ return this.client.request('DELETE', buildPath('/v1/account/api-keys/{id}', { segments: { id } }), undefined, requestOptions);
57
58
  }
58
59
  /** POST /v1/account/api-keys/{id}/reveal — reveal the plaintext API key value */
59
60
  reveal(id, requestOptions) {
60
- return this.client.request('POST', `/v1/account/api-keys/${id}/reveal`, undefined, requestOptions);
61
+ return this.client.request('POST', buildPath('/v1/account/api-keys/{id}/reveal', { segments: { id } }), undefined, requestOptions);
61
62
  }
62
63
  }
@@ -1,3 +1,4 @@
1
+ import { buildPath } from '../client.js';
1
2
  export class BulkResource {
2
3
  client;
3
4
  screenshots;
@@ -16,11 +17,11 @@ export class BulkResource {
16
17
  }
17
18
  /** GET /v1/bulk/{id} — get a bulk job status */
18
19
  get(id, requestOptions) {
19
- return this.client.request('GET', `/v1/bulk/${id}`, undefined, requestOptions);
20
+ return this.client.request('GET', buildPath('/v1/bulk/{id}', { segments: { id } }), undefined, requestOptions);
20
21
  }
21
22
  /** DELETE /v1/bulk/{id} — cancel an in-flight bulk job */
22
23
  cancel(id, requestOptions) {
23
- return this.client.request('DELETE', `/v1/bulk/${id}`, undefined, requestOptions);
24
+ return this.client.request('DELETE', buildPath('/v1/bulk/{id}', { segments: { id } }), undefined, requestOptions);
24
25
  }
25
26
  }
26
27
  class BulkScreenshotsResource {
@@ -30,6 +31,6 @@ class BulkScreenshotsResource {
30
31
  }
31
32
  /** GET /v1/bulk/{id}/screenshots — list screenshots in a bulk job */
32
33
  list(id, requestOptions) {
33
- return this.client.request('GET', `/v1/bulk/${id}/screenshots`, undefined, requestOptions);
34
+ return this.client.request('GET', buildPath('/v1/bulk/{id}/screenshots', { segments: { id } }), undefined, requestOptions);
34
35
  }
35
36
  }
@@ -1,3 +1,4 @@
1
+ import { buildPath } from '../client.js';
1
2
  /**
2
3
  * The query params `GET /v1/fetch` accepts, in serialization order.
3
4
  *
@@ -80,7 +81,7 @@ export class FetchResource {
80
81
  if (value !== undefined)
81
82
  params.set(key, String(value));
82
83
  }
83
- return toFetchResult(await this.client.requestRaw('GET', `/v1/fetch?${params.toString()}`, undefined, requestOptions));
84
+ return toFetchResult(await this.client.requestRaw('GET', buildPath('/v1/fetch', { query: `?${params.toString()}` }), undefined, requestOptions));
84
85
  }
85
86
  }
86
87
  class FetchBulkResource {
@@ -94,6 +95,6 @@ class FetchBulkResource {
94
95
  }
95
96
  /** GET /v1/fetch/bulk/{id} — get a bulk fetch job */
96
97
  get(id, requestOptions) {
97
- return this.client.request('GET', `/v1/fetch/bulk/${id}`, undefined, requestOptions);
98
+ return this.client.request('GET', buildPath('/v1/fetch/bulk/{id}', { segments: { id } }), undefined, requestOptions);
98
99
  }
99
100
  }
@@ -1,4 +1,5 @@
1
1
  import { toQueryString } from './query-string.js';
2
+ import { buildPath } from '../client.js';
2
3
  export class MonitorsResource {
3
4
  client;
4
5
  captures;
@@ -20,15 +21,15 @@ export class MonitorsResource {
20
21
  * serializes the bare `/v1/monitors` URL unchanged — existing zero-arg callers are unaffected.
21
22
  */
22
23
  list(params, requestOptions) {
23
- return this.client.request('GET', `/v1/monitors${toQueryString(params)}`, undefined, requestOptions);
24
+ return this.client.request('GET', buildPath('/v1/monitors', { query: toQueryString(params) }), undefined, requestOptions);
24
25
  }
25
26
  /** GET /v1/monitors/{id} — get a monitor by ID */
26
27
  get(id, requestOptions) {
27
- return this.client.request('GET', `/v1/monitors/${id}`, undefined, requestOptions);
28
+ return this.client.request('GET', buildPath('/v1/monitors/{id}', { segments: { id } }), undefined, requestOptions);
28
29
  }
29
30
  /** PATCH /v1/monitors/{id} — update a monitor */
30
31
  update(id, opts, requestOptions) {
31
- return this.client.request('PATCH', `/v1/monitors/${id}`, opts, requestOptions);
32
+ return this.client.request('PATCH', buildPath('/v1/monitors/{id}', { segments: { id } }), opts, requestOptions);
32
33
  }
33
34
  /**
34
35
  * DELETE /v1/monitors/{id} — delete a monitor.
@@ -39,7 +40,7 @@ export class MonitorsResource {
39
40
  * body to `undefined` before it would parse as JSON (#2450).
40
41
  */
41
42
  delete(id, requestOptions) {
42
- return this.client.request('DELETE', `/v1/monitors/${id}`, undefined, requestOptions);
43
+ return this.client.request('DELETE', buildPath('/v1/monitors/{id}', { segments: { id } }), undefined, requestOptions);
43
44
  }
44
45
  }
45
46
  class MonitorCapturesResource {
@@ -55,6 +56,6 @@ class MonitorCapturesResource {
55
56
  * zero-arg callers are unaffected (inverse case).
56
57
  */
57
58
  list(id, params, requestOptions) {
58
- return this.client.request('GET', `/v1/monitors/${id}/captures${toQueryString(params)}`, undefined, requestOptions);
59
+ return this.client.request('GET', buildPath('/v1/monitors/{id}/captures', { segments: { id }, query: toQueryString(params) }), undefined, requestOptions);
59
60
  }
60
61
  }
@@ -1,3 +1,4 @@
1
+ import { buildPath } from '../client.js';
1
2
  export class ScrapeResource {
2
3
  client;
3
4
  bulk;
@@ -35,7 +36,7 @@ export class ScrapeResource {
35
36
  * gateway and is out of this ticket's scope.
36
37
  */
37
38
  get(id, requestOptions) {
38
- return this.client.request('GET', `/v1/scrape/${id}`, undefined, requestOptions);
39
+ return this.client.request('GET', buildPath('/v1/scrape/{id}', { segments: { id } }), undefined, requestOptions);
39
40
  }
40
41
  }
41
42
  class ScrapeBulkResource {
@@ -54,6 +55,6 @@ class ScrapeBulkResource {
54
55
  }
55
56
  /** GET /v1/scrape/bulk/{id} — get a bulk scrape job */
56
57
  get(id, requestOptions) {
57
- return this.client.request('GET', `/v1/scrape/bulk/${id}`, undefined, requestOptions);
58
+ return this.client.request('GET', buildPath('/v1/scrape/bulk/{id}', { segments: { id } }), undefined, requestOptions);
58
59
  }
59
60
  }
@@ -1,3 +1,4 @@
1
+ import { buildPath } from '../client.js';
1
2
  export class ScreenshotsResource {
2
3
  client;
3
4
  constructor(client) {
@@ -17,7 +18,7 @@ export class ScreenshotsResource {
17
18
  */
18
19
  create(opts, params, requestOptions) {
19
20
  const suffix = params?.async === true ? '?async=true' : '';
20
- return this.client.request('POST', `/v1/screenshots${suffix}`, opts, requestOptions);
21
+ return this.client.request('POST', buildPath('/v1/screenshots', { query: suffix }), opts, requestOptions);
21
22
  }
22
23
  /**
23
24
  * GET /v1/screenshots/{id} — poll a screenshot by ID.
@@ -26,7 +27,7 @@ export class ScreenshotsResource {
26
27
  * (issue #2027), so narrow the result before reading `image_url`.
27
28
  */
28
29
  get(id, requestOptions) {
29
- return this.client.request('GET', `/v1/screenshots/${id}`, undefined, requestOptions);
30
+ return this.client.request('GET', buildPath('/v1/screenshots/{id}', { segments: { id } }), undefined, requestOptions);
30
31
  }
31
32
  /**
32
33
  * POST /v1/screenshots, returning the header set alongside the body (#2547).
@@ -42,7 +43,7 @@ export class ScreenshotsResource {
42
43
  */
43
44
  createWithResponse(opts, params, requestOptions) {
44
45
  const suffix = params?.async === true ? '?async=true' : '';
45
- return this.client.requestWithResponse('POST', `/v1/screenshots${suffix}`, opts, requestOptions);
46
+ return this.client.requestWithResponse('POST', buildPath('/v1/screenshots', { query: suffix }), opts, requestOptions);
46
47
  }
47
48
  /**
48
49
  * GET /v1/screenshots/{id}, returning the header set alongside the body (#2547).
@@ -53,6 +54,6 @@ export class ScreenshotsResource {
53
54
  * `X-Raw-Bytes`, `X-Billable-Bytes` and `X-Billing-Multiplier` headers are on `headers`.
54
55
  */
55
56
  getWithResponse(id, requestOptions) {
56
- return this.client.requestWithResponse('GET', `/v1/screenshots/${id}`, undefined, requestOptions);
57
+ return this.client.requestWithResponse('GET', buildPath('/v1/screenshots/{id}', { segments: { id } }), undefined, requestOptions);
57
58
  }
58
59
  }
@@ -1,3 +1,4 @@
1
+ import { buildPath } from '../client.js';
1
2
  export class SitemapResource {
2
3
  client;
3
4
  constructor(client) {
@@ -14,7 +15,7 @@ export class SitemapResource {
14
15
  }
15
16
  /** GET /v1/sitemap/{id} — get a sitemap job by ID */
16
17
  get(id, requestOptions) {
17
- return this.client.request('GET', `/v1/sitemap/${id}`, undefined, requestOptions);
18
+ return this.client.request('GET', buildPath('/v1/sitemap/{id}', { segments: { id } }), undefined, requestOptions);
18
19
  }
19
20
  /**
20
21
  * GET /v1/sitemap/{id}, returning the header set alongside the body (#2547).
@@ -27,6 +28,6 @@ export class SitemapResource {
27
28
  * `X-Billable-Bytes` on this endpoint's 200; every one of them is on `headers`.
28
29
  */
29
30
  getWithResponse(id, requestOptions) {
30
- return this.client.requestWithResponse('GET', `/v1/sitemap/${id}`, undefined, requestOptions);
31
+ return this.client.requestWithResponse('GET', buildPath('/v1/sitemap/{id}', { segments: { id } }), undefined, requestOptions);
31
32
  }
32
33
  }
@@ -1,4 +1,5 @@
1
1
  import { toQueryString } from './query-string.js';
2
+ import { buildPath } from '../client.js';
2
3
  export class UsageResource {
3
4
  client;
4
5
  constructor(client) {
@@ -18,13 +19,13 @@ export class UsageResource {
18
19
  * Default range is the last 30 days; max range is 366 days.
19
20
  */
20
21
  daily(params, requestOptions) {
21
- return this.client.request('GET', `/v1/usage/daily${toQueryString(params)}`, undefined, requestOptions);
22
+ return this.client.request('GET', buildPath('/v1/usage/daily', { query: toQueryString(params) }), undefined, requestOptions);
22
23
  }
23
24
  /**
24
25
  * GET /v1/usage/by-endpoint — get per-endpoint bandwidth breakdown.
25
26
  * Default range is the last 30 days; max range is 366 days.
26
27
  */
27
28
  byEndpoint(params, requestOptions) {
28
- return this.client.request('GET', `/v1/usage/by-endpoint${toQueryString(params)}`, undefined, requestOptions);
29
+ return this.client.request('GET', buildPath('/v1/usage/by-endpoint', { query: toQueryString(params) }), undefined, requestOptions);
29
30
  }
30
31
  }
@@ -1,4 +1,5 @@
1
1
  import { toQueryString } from './query-string.js';
2
+ import { buildPath } from '../client.js';
2
3
  export class WebhooksResource {
3
4
  client;
4
5
  constructor(client) {
@@ -11,10 +12,10 @@ export class WebhooksResource {
11
12
  * optional caller `signal`; both override the constructor's `timeoutMs` default.
12
13
  */
13
14
  list(params, requestOptions) {
14
- return this.client.request('GET', `/v1/webhook-deliveries${toQueryString(params)}`, undefined, requestOptions);
15
+ return this.client.request('GET', buildPath('/v1/webhook-deliveries', { query: toQueryString(params) }), undefined, requestOptions);
15
16
  }
16
17
  /** POST /v1/webhook-deliveries/{id}/retry — requeue a failed or exhausted delivery. */
17
18
  retry(id, requestOptions) {
18
- return this.client.request('POST', `/v1/webhook-deliveries/${id}/retry`, undefined, requestOptions);
19
+ return this.client.request('POST', buildPath('/v1/webhook-deliveries/{id}/retry', { segments: { id } }), undefined, requestOptions);
19
20
  }
20
21
  }
package/dist/types.d.ts CHANGED
@@ -64,8 +64,12 @@ export interface SearchResult {
64
64
  position: number;
65
65
  title: string;
66
66
  url: string;
67
- /** Short text excerpt for the result. Omitted for `images` results. */
68
- snippet: string;
67
+ /**
68
+ * Short text excerpt for the result. Present for `web` and `news` results;
69
+ * omitted entirely (`undefined`) for `images` results — read it behind a
70
+ * guard, exactly like `date` (#2678).
71
+ */
72
+ snippet?: string;
69
73
  /**
70
74
  * Published date when the provider reports one, else `null`. Present for
71
75
  * `web`/`news` results; omitted entirely (`undefined`) for `images` results.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunt-labs/bytekit-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Official TypeScript SDK for the ByteKit API",
5
5
  "license": "MIT",
6
6
  "type": "module",