@hunt-labs/bytekit-sdk 0.4.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 +130 -2
- package/README.md +63 -9
- package/dist/allowlist.d.ts +22 -0
- package/dist/allowlist.js +64 -0
- package/dist/client.d.ts +91 -5
- package/dist/client.js +77 -6
- package/dist/generated/api.d.ts +394 -43
- package/dist/index.d.ts +2 -2
- package/dist/resources/account.js +5 -4
- package/dist/resources/bulk.js +4 -3
- package/dist/resources/fetch.js +7 -3
- package/dist/resources/monitors.d.ts +21 -2
- package/dist/resources/monitors.js +14 -7
- package/dist/resources/scrape.js +3 -2
- package/dist/resources/screenshots.js +5 -4
- package/dist/resources/sitemap.js +3 -2
- package/dist/resources/usage.js +3 -2
- package/dist/resources/webhooks.js +3 -2
- package/dist/types.d.ts +6 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,126 @@ 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
|
+
|
|
112
|
+
## [0.5.0] - 2026-07-29
|
|
113
|
+
|
|
114
|
+
### Added
|
|
115
|
+
|
|
116
|
+
- `src/generated/api.d.ts` now models the `400` responses that the gateway's new
|
|
117
|
+
request-body-size guards can return. `operations['stripeWebhook']['responses']` gains a
|
|
118
|
+
`400` with this route's bespoke `{ error?: string }` body, and
|
|
119
|
+
`operations['retryWebhookDelivery']['responses']` gains a `400` carrying the unified
|
|
120
|
+
`components['schemas']['Error']` envelope. Both are purely additive — no existing member
|
|
121
|
+
changed shape — so every current call site still compiles. (#2640)
|
|
122
|
+
|
|
123
|
+
### Changed
|
|
124
|
+
|
|
125
|
+
- Regenerated `src/generated/api.d.ts` against the merged spec. Beyond the two added `400`
|
|
126
|
+
members above, the delta is JSDoc only: every guarded endpoint's description now carries a
|
|
127
|
+
`**Limits:**` block stating its byte ceiling and the `request_too_large` error code
|
|
128
|
+
(#2640), and `POST /v1/bulk`'s description documents `defaults.type` inheritance and the
|
|
129
|
+
422 on unrecognized keys (#2643). No runtime code changed. (#2640, #2643)
|
|
130
|
+
|
|
11
131
|
## [0.4.0] - 2026-07-24
|
|
12
132
|
|
|
13
133
|
### Added
|
|
@@ -133,7 +253,12 @@ today, see [README.md](./README.md), which remains the authoritative usage guide
|
|
|
133
253
|
rebuilding the shared `dist`), which is now fixed. No consumer-facing changes
|
|
134
254
|
beyond 0.2.1.
|
|
135
255
|
|
|
136
|
-
## [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.
|
|
137
262
|
|
|
138
263
|
### Changed
|
|
139
264
|
|
|
@@ -193,7 +318,7 @@ against 0.1.0 — read each one before upgrading.
|
|
|
193
318
|
- Publish-ready packaging metadata: types-first `exports`, `engines`, `keywords`,
|
|
194
319
|
`homepage`, `bugs`, `README.md`, `LICENSE`, and an MIT `license` field.
|
|
195
320
|
|
|
196
|
-
## [0.1.0] - 2026
|
|
321
|
+
## [0.1.0] - 2026-04-24
|
|
197
322
|
|
|
198
323
|
### Added
|
|
199
324
|
|
|
@@ -201,6 +326,9 @@ against 0.1.0 — read each one before upgrading.
|
|
|
201
326
|
(scrape, screenshot, search, and related endpoints), published under the original
|
|
202
327
|
`@rapidcrawl/sdk` name.
|
|
203
328
|
|
|
329
|
+
[0.6.0]: https://github.com/Hunt-Labs-Inc/ByteKit/tree/main/packages/sdk-typescript
|
|
330
|
+
[0.5.0]: https://github.com/Hunt-Labs-Inc/ByteKit/tree/main/packages/sdk-typescript
|
|
331
|
+
[0.4.0]: https://github.com/Hunt-Labs-Inc/ByteKit/tree/main/packages/sdk-typescript
|
|
204
332
|
[0.3.0]: https://github.com/Hunt-Labs-Inc/ByteKit/tree/main/packages/sdk-typescript
|
|
205
333
|
[0.2.2]: https://github.com/Hunt-Labs-Inc/ByteKit/tree/main/packages/sdk-typescript
|
|
206
334
|
[0.2.1]: 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
|
|
|
@@ -185,12 +196,15 @@ const deliveries = await client.webhooks.list();
|
|
|
185
196
|
await client.webhooks.retry('wh_123abc');
|
|
186
197
|
```
|
|
187
198
|
|
|
188
|
-
> **Note
|
|
189
|
-
>
|
|
190
|
-
>
|
|
191
|
-
>
|
|
192
|
-
>
|
|
193
|
-
>
|
|
199
|
+
> **Note (the real auth split):** `account.get()` authenticates with the same Bearer
|
|
200
|
+
> API key as every other v0.1 method above — it succeeds normally. `client.account.apiKeys.*`
|
|
201
|
+
> is different: it targets `/v1/account/api-keys*`, which authenticates with a Clerk
|
|
202
|
+
> dashboard session, **not** a Bearer API key — calling any `apiKeys.*` method with an
|
|
203
|
+
> API-key client fails auth. `apiKeys.*` remains on the SDK surface for dashboard/session
|
|
204
|
+
> consumers (deliberate retention — see `DOCUMENTED_SURFACE_EXCEPTIONS` in
|
|
205
|
+
> `src/allowlist.ts`) and is marked `x-sdk-scope: internal` in the spec, so it is
|
|
206
|
+
> intentionally excluded from the v0.1 Bearer-key parity guarantee. `account.get()` is not:
|
|
207
|
+
> it is a real `V01_METHODS` entry.
|
|
194
208
|
|
|
195
209
|
### Response headers
|
|
196
210
|
|
|
@@ -207,8 +221,12 @@ console.log(headers.get('X-Raw-Bytes')); // e.g. "12345"
|
|
|
207
221
|
console.log(status); // 200
|
|
208
222
|
```
|
|
209
223
|
|
|
210
|
-
`client.screenshots.createWithResponse()` and
|
|
211
|
-
|
|
224
|
+
`client.screenshots.createWithResponse()` works the same way, and is where the
|
|
225
|
+
`X-Screenshot-*`/`X-Raw-Bytes`/`X-Billable-Bytes`/`X-Billing-Multiplier` headers
|
|
226
|
+
actually live — they're declared on `POST /v1/screenshots` only. Its sibling
|
|
227
|
+
`client.screenshots.getWithResponse()` is still additive and safe to call, but
|
|
228
|
+
`GET /v1/screenshots/{id}` emits none of those headers, so `headers` will be
|
|
229
|
+
empty for it. Any other endpoint's headers are reachable through the generic
|
|
212
230
|
transport, `client.requestWithResponse('GET', '/v1/scrape/sc_123abc')`.
|
|
213
231
|
|
|
214
232
|
These siblings are purely additive: they issue the identical request and accept the
|
|
@@ -232,6 +250,42 @@ try {
|
|
|
232
250
|
}
|
|
233
251
|
```
|
|
234
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
|
+
|
|
235
289
|
## Documentation
|
|
236
290
|
|
|
237
291
|
Full API reference: [bytekit.com/docs](https://bytekit.com/docs).
|
package/dist/allowlist.d.ts
CHANGED
|
@@ -10,3 +10,25 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export declare const V01_METHODS: readonly ["scrape.create", "scrape.get", "screenshots.create", "screenshots.get", "bulk.create", "bulk.get", "bulk.cancel", "bulk.screenshots.list", "scrape.bulk.create", "scrape.bulk.get", "fetch.create", "fetch.get", "fetch.bulk.create", "fetch.bulk.get", "monitors.create", "monitors.list", "monitors.get", "monitors.update", "monitors.delete", "monitors.captures.list", "sitemap.create", "sitemap.get", "search.query", "account.get", "usage.get", "usage.daily", "usage.byEndpoint", "webhooks.list", "webhooks.retry"];
|
|
12
12
|
export type V01Method = (typeof V01_METHODS)[number];
|
|
13
|
+
/**
|
|
14
|
+
* Methods that exist on the real, instantiated `ByteKit` client surface but are
|
|
15
|
+
* deliberately EXCLUDED from `V01_METHODS` — each with the reason it stays off the
|
|
16
|
+
* v0.1 parity guard.
|
|
17
|
+
*
|
|
18
|
+
* `parity.test.ts` only compares the OpenAPI spec's `x-sdk-scope: v0.1` operations
|
|
19
|
+
* against `V01_METHODS` — it says nothing about what the actual client *instance*
|
|
20
|
+
* exposes. That gap is how the six `account.apiKeys.*` methods shipped unguarded
|
|
21
|
+
* (issue #2595): they are real, callable methods on `client.account.apiKeys`, are
|
|
22
|
+
* NOT in `V01_METHODS`, and no test walked the instantiated surface to notice.
|
|
23
|
+
*
|
|
24
|
+
* `surface-guard.test.ts` walks the real client and asserts every callable is
|
|
25
|
+
* either in `V01_METHODS` or listed here — removing a method (from either list) or
|
|
26
|
+
* adding an undocumented one fails that test. To add a new client-surface method
|
|
27
|
+
* that is deliberately NOT part of the v0.1 Bearer-key parity guarantee, add an
|
|
28
|
+
* entry here with the reason; to add one that IS part of it, add it to
|
|
29
|
+
* `V01_METHODS` instead (and annotate the spec operation with `x-sdk-method`).
|
|
30
|
+
*/
|
|
31
|
+
export declare const DOCUMENTED_SURFACE_EXCEPTIONS: ReadonlyArray<{
|
|
32
|
+
readonly method: string;
|
|
33
|
+
readonly reason: string;
|
|
34
|
+
}>;
|
package/dist/allowlist.js
CHANGED
|
@@ -42,3 +42,67 @@ export const V01_METHODS = [
|
|
|
42
42
|
'webhooks.list',
|
|
43
43
|
'webhooks.retry',
|
|
44
44
|
];
|
|
45
|
+
/**
|
|
46
|
+
* Methods that exist on the real, instantiated `ByteKit` client surface but are
|
|
47
|
+
* deliberately EXCLUDED from `V01_METHODS` — each with the reason it stays off the
|
|
48
|
+
* v0.1 parity guard.
|
|
49
|
+
*
|
|
50
|
+
* `parity.test.ts` only compares the OpenAPI spec's `x-sdk-scope: v0.1` operations
|
|
51
|
+
* against `V01_METHODS` — it says nothing about what the actual client *instance*
|
|
52
|
+
* exposes. That gap is how the six `account.apiKeys.*` methods shipped unguarded
|
|
53
|
+
* (issue #2595): they are real, callable methods on `client.account.apiKeys`, are
|
|
54
|
+
* NOT in `V01_METHODS`, and no test walked the instantiated surface to notice.
|
|
55
|
+
*
|
|
56
|
+
* `surface-guard.test.ts` walks the real client and asserts every callable is
|
|
57
|
+
* either in `V01_METHODS` or listed here — removing a method (from either list) or
|
|
58
|
+
* adding an undocumented one fails that test. To add a new client-surface method
|
|
59
|
+
* that is deliberately NOT part of the v0.1 Bearer-key parity guarantee, add an
|
|
60
|
+
* entry here with the reason; to add one that IS part of it, add it to
|
|
61
|
+
* `V01_METHODS` instead (and annotate the spec operation with `x-sdk-method`).
|
|
62
|
+
*/
|
|
63
|
+
export const DOCUMENTED_SURFACE_EXCEPTIONS = [
|
|
64
|
+
// account.apiKeys.* — x-sdk-scope: internal in the spec (Clerk-session-only auth;
|
|
65
|
+
// a Bearer API key 401s on every one of these). Deliberate retention: the resource
|
|
66
|
+
// stays on the SDK surface for dashboard/session consumers rather than being
|
|
67
|
+
// removed, matching the split documented in README.md's "Account, usage, and API
|
|
68
|
+
// keys" section (issue #2595).
|
|
69
|
+
{
|
|
70
|
+
method: 'account.apiKeys.create',
|
|
71
|
+
reason: 'Clerk-session-only (x-sdk-scope: internal) — deliberate retention for dashboard/session consumers, not part of the Bearer-key v0.1 surface.',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
method: 'account.apiKeys.list',
|
|
75
|
+
reason: 'Clerk-session-only (x-sdk-scope: internal) — deliberate retention for dashboard/session consumers, not part of the Bearer-key v0.1 surface.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
method: 'account.apiKeys.get',
|
|
79
|
+
reason: 'Clerk-session-only (x-sdk-scope: internal) — deliberate retention for dashboard/session consumers, not part of the Bearer-key v0.1 surface.',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
method: 'account.apiKeys.update',
|
|
83
|
+
reason: 'Clerk-session-only (x-sdk-scope: internal) — deliberate retention for dashboard/session consumers, not part of the Bearer-key v0.1 surface.',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
method: 'account.apiKeys.revoke',
|
|
87
|
+
reason: 'Clerk-session-only (x-sdk-scope: internal) — deliberate retention for dashboard/session consumers, not part of the Bearer-key v0.1 surface.',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
method: 'account.apiKeys.reveal',
|
|
91
|
+
reason: 'Clerk-session-only (x-sdk-scope: internal) — deliberate retention for dashboard/session consumers, not part of the Bearer-key v0.1 surface.',
|
|
92
|
+
},
|
|
93
|
+
// *WithResponse siblings (#2547) — additive `ApiResponse<T>` shorthands over an
|
|
94
|
+
// already-allowlisted base method (same HTTP method, path, and body). Not a
|
|
95
|
+
// distinct v0.1 API operation, so not a `V01_METHODS` entry in its own right.
|
|
96
|
+
{
|
|
97
|
+
method: 'screenshots.createWithResponse',
|
|
98
|
+
reason: 'Additive #2547 ApiResponse<T> shorthand over screenshots.create (same request) — not a distinct v0.1 API operation.',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
method: 'screenshots.getWithResponse',
|
|
102
|
+
reason: 'Additive #2547 ApiResponse<T> shorthand over screenshots.get (same request) — not a distinct v0.1 API operation.',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
method: 'sitemap.getWithResponse',
|
|
106
|
+
reason: 'Additive #2547 ApiResponse<T> shorthand over sitemap.get (same request) — not a distinct v0.1 API operation.',
|
|
107
|
+
},
|
|
108
|
+
];
|
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
|
-
|
|
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,
|
|
@@ -185,9 +269,11 @@ export declare class ByteKit {
|
|
|
185
269
|
*
|
|
186
270
|
* Use it to read the documented `X-*` metadata the API emits alongside a JSON envelope —
|
|
187
271
|
* `X-Raw-Bytes`, `X-Billable-Bytes`, `X-Billing-Multiplier`, the `X-Screenshot-*` family
|
|
188
|
-
* — for any endpoint. `sitemap.getWithResponse
|
|
189
|
-
*
|
|
190
|
-
*
|
|
272
|
+
* — for any endpoint. `sitemap.getWithResponse` and `screenshots.createWithResponse` are
|
|
273
|
+
* typed shorthands over this for the endpoints whose headers are documented in the API
|
|
274
|
+
* reference. `screenshots.getWithResponse` is the same shorthand shape applied to
|
|
275
|
+
* `GET /v1/screenshots/{id}`, which the spec declares with NO `X-*` response headers — call
|
|
276
|
+
* it for the typed body, not for header metadata.
|
|
191
277
|
*/
|
|
192
278
|
requestWithResponse<T>(method: string, path: string, body?: unknown, options?: RequestOptions): Promise<ApiResponse<T>>;
|
|
193
279
|
}
|
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);
|
|
@@ -426,9 +495,11 @@ export class ByteKit {
|
|
|
426
495
|
*
|
|
427
496
|
* Use it to read the documented `X-*` metadata the API emits alongside a JSON envelope —
|
|
428
497
|
* `X-Raw-Bytes`, `X-Billable-Bytes`, `X-Billing-Multiplier`, the `X-Screenshot-*` family
|
|
429
|
-
* — for any endpoint. `sitemap.getWithResponse
|
|
430
|
-
*
|
|
431
|
-
*
|
|
498
|
+
* — for any endpoint. `sitemap.getWithResponse` and `screenshots.createWithResponse` are
|
|
499
|
+
* typed shorthands over this for the endpoints whose headers are documented in the API
|
|
500
|
+
* reference. `screenshots.getWithResponse` is the same shorthand shape applied to
|
|
501
|
+
* `GET /v1/screenshots/{id}`, which the spec declares with NO `X-*` response headers — call
|
|
502
|
+
* it for the typed body, not for header metadata.
|
|
432
503
|
*/
|
|
433
504
|
async requestWithResponse(method, path, body, options) {
|
|
434
505
|
// Same continuation shape as request()/requestRaw() (#2546): the readBody continuation
|