@hunt-labs/bytekit-sdk 0.1.0 → 0.2.2

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +171 -0
  4. package/dist/allowlist.d.ts +0 -1
  5. package/dist/allowlist.js +0 -1
  6. package/dist/client.d.ts +69 -10
  7. package/dist/client.js +224 -13
  8. package/dist/generated/api.d.ts +6607 -0
  9. package/dist/index.d.ts +8 -2
  10. package/dist/index.js +3 -2
  11. package/dist/resources/account.d.ts +50 -9
  12. package/dist/resources/account.js +17 -3
  13. package/dist/resources/bulk.d.ts +0 -1
  14. package/dist/resources/bulk.js +0 -1
  15. package/dist/resources/fetch.d.ts +0 -1
  16. package/dist/resources/fetch.js +0 -1
  17. package/dist/resources/monitors.d.ts +0 -1
  18. package/dist/resources/monitors.js +0 -1
  19. package/dist/resources/scrape.d.ts +50 -8
  20. package/dist/resources/scrape.js +30 -7
  21. package/dist/resources/screenshots.d.ts +54 -4
  22. package/dist/resources/screenshots.js +6 -2
  23. package/dist/resources/search.d.ts +0 -1
  24. package/dist/resources/search.js +0 -1
  25. package/dist/resources/sitemap.d.ts +0 -1
  26. package/dist/resources/sitemap.js +0 -1
  27. package/dist/types.d.ts +0 -1
  28. package/dist/types.js +0 -1
  29. package/package.json +28 -11
  30. package/dist/allowlist.d.ts.map +0 -1
  31. package/dist/allowlist.js.map +0 -1
  32. package/dist/client.d.ts.map +0 -1
  33. package/dist/client.js.map +0 -1
  34. package/dist/index.d.ts.map +0 -1
  35. package/dist/index.js.map +0 -1
  36. package/dist/resources/account.d.ts.map +0 -1
  37. package/dist/resources/account.js.map +0 -1
  38. package/dist/resources/bulk.d.ts.map +0 -1
  39. package/dist/resources/bulk.js.map +0 -1
  40. package/dist/resources/fetch.d.ts.map +0 -1
  41. package/dist/resources/fetch.js.map +0 -1
  42. package/dist/resources/monitors.d.ts.map +0 -1
  43. package/dist/resources/monitors.js.map +0 -1
  44. package/dist/resources/recordings.d.ts +0 -11
  45. package/dist/resources/recordings.d.ts.map +0 -1
  46. package/dist/resources/recordings.js +0 -15
  47. package/dist/resources/recordings.js.map +0 -1
  48. package/dist/resources/scrape.d.ts.map +0 -1
  49. package/dist/resources/scrape.js.map +0 -1
  50. package/dist/resources/screenshots.d.ts.map +0 -1
  51. package/dist/resources/screenshots.js.map +0 -1
  52. package/dist/resources/search.d.ts.map +0 -1
  53. package/dist/resources/search.js.map +0 -1
  54. package/dist/resources/sitemap.d.ts.map +0 -1
  55. package/dist/resources/sitemap.js.map +0 -1
  56. package/dist/types.d.ts.map +0 -1
  57. package/dist/types.js.map +0 -1
package/dist/client.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { ScrapeResource } from './resources/scrape.js';
2
2
  import { ScreenshotsResource } from './resources/screenshots.js';
3
- import { RecordingsResource } from './resources/recordings.js';
4
3
  import { BulkResource } from './resources/bulk.js';
5
4
  import { FetchResource } from './resources/fetch.js';
6
5
  import { MonitorsResource } from './resources/monitors.js';
@@ -8,23 +7,155 @@ import { SitemapResource } from './resources/sitemap.js';
8
7
  import { SearchResource } from './resources/search.js';
9
8
  import { AccountResource } from './resources/account.js';
10
9
  const DEFAULT_BASE_URL = 'https://api.bytekit.com';
10
+ /**
11
+ * Default client-side request timeout (2 minutes).
12
+ *
13
+ * Above the slowest expected synchronous scrape, and well below undici's ~5-minute
14
+ * headers timeout, so the SDK is always the tighter bound and a hung upstream can
15
+ * never hang the caller indefinitely.
16
+ */
17
+ const DEFAULT_TIMEOUT_MS = 120_000;
11
18
  /** Thrown when the API responds with a 4xx or 5xx status code. */
12
- export class RapidCrawlError extends Error {
19
+ export class ByteKitError extends Error {
13
20
  status;
14
21
  code;
15
22
  constructor(status, code, message) {
16
23
  super(message);
17
- this.name = 'RapidCrawlError';
24
+ this.name = 'ByteKitError';
18
25
  this.status = status;
19
26
  this.code = code;
20
27
  }
21
28
  }
22
- export class RapidCrawl {
29
+ /**
30
+ * Thrown when the request never produced an HTTP response at all — DNS, socket, TLS,
31
+ * a client-side timeout, or a caller-initiated abort. Distinct from `ByteKitError`,
32
+ * which always carries a real HTTP status: catch both to cover every failure.
33
+ *
34
+ * The original failure is preserved on `.cause`.
35
+ */
36
+ export class ByteKitConnectionError extends Error {
37
+ code;
38
+ constructor(code, message, options) {
39
+ super(message, options);
40
+ this.name = 'ByteKitConnectionError';
41
+ this.code = code;
42
+ }
43
+ }
44
+ const NOOP = () => { };
45
+ /**
46
+ * Validate a timeout option. `undefined` passes through; anything that is not a
47
+ * non-negative finite number throws a `TypeError` naming the option as the caller
48
+ * spelled it (`"timeoutMs"` at the constructor, `"requestTimeoutMs"` per call).
49
+ */
50
+ function validateTimeout(value, optionName) {
51
+ if (value === undefined)
52
+ return undefined;
53
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
54
+ throw new TypeError(`ByteKit: ${optionName} must be a non-negative finite number (got ${describe(value)})`);
55
+ }
56
+ return value;
57
+ }
58
+ /** Render a rejected option value for an error message without ever stringifying an object. */
59
+ function describe(value) {
60
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
61
+ return String(value);
62
+ }
63
+ if (typeof value === 'string')
64
+ return JSON.stringify(value);
65
+ if (value === null)
66
+ return 'null';
67
+ return typeof value;
68
+ }
69
+ /** The abort reason used by the `setTimeout` fallback; named so it maps to `code: 'timeout'`. */
70
+ function timeoutReason(ms) {
71
+ const reason = new Error(`ByteKit: request timed out after ${ms}ms`);
72
+ reason.name = 'TimeoutError';
73
+ return reason;
74
+ }
75
+ /** `AbortSignal.timeout(ms)` where available, else an `AbortController` + `setTimeout`. */
76
+ function createTimeoutSignal(ms) {
77
+ if (typeof AbortSignal.timeout === 'function') {
78
+ return { signal: AbortSignal.timeout(ms), cleanup: NOOP };
79
+ }
80
+ const controller = new AbortController();
81
+ const timer = setTimeout(() => controller.abort(timeoutReason(ms)), ms);
82
+ timer.unref?.();
83
+ return {
84
+ signal: controller.signal,
85
+ cleanup: () => clearTimeout(timer),
86
+ };
87
+ }
88
+ /**
89
+ * Abort on whichever source fires first.
90
+ *
91
+ * Prefers `AbortSignal.any`, whose sources hold only WEAK references to the composed
92
+ * signal — so nothing has to be (and nothing must be) retained to keep it collectable.
93
+ * The manual fallback is the path that has to clean up after itself: it attaches a real
94
+ * listener to the CALLER's signal, which typically outlives the request (one long-lived
95
+ * `AbortController` reused across many calls), so `cleanup()` removes it again.
96
+ */
97
+ function composeSignals(caller, timeout) {
98
+ const anyFn = AbortSignal.any?.bind(AbortSignal);
99
+ if (typeof anyFn === 'function') {
100
+ return { signal: anyFn([caller, timeout]), cleanup: NOOP };
101
+ }
102
+ const controller = new AbortController();
103
+ const sources = [caller, timeout];
104
+ const onAbort = (event) => {
105
+ controller.abort(event.target.reason);
106
+ };
107
+ let done = false;
108
+ const cleanup = () => {
109
+ if (done)
110
+ return;
111
+ done = true;
112
+ for (const source of sources)
113
+ source.removeEventListener('abort', onAbort);
114
+ };
115
+ for (const source of sources)
116
+ source.addEventListener('abort', onAbort);
117
+ for (const source of sources) {
118
+ if (source.aborted) {
119
+ controller.abort(source.reason);
120
+ break;
121
+ }
122
+ }
123
+ return { signal: controller.signal, cleanup };
124
+ }
125
+ /** Map a `fetch` rejection onto the connection-error taxonomy. Never leaks the API key. */
126
+ function toConnectionError(err, method, url, timeoutSignal) {
127
+ // undici rejects an aborted fetch with the signal's own reason, so the thrown value's
128
+ // `name` is the primary discriminator (`TimeoutError` / `AbortError`).
129
+ const rawName = typeof err === 'object' && err !== null && 'name' in err ? err.name : undefined;
130
+ const name = typeof rawName === 'string' ? rawName : '';
131
+ let code;
132
+ if (name === 'TimeoutError') {
133
+ code = 'timeout';
134
+ }
135
+ else if (name === 'AbortError') {
136
+ code = 'aborted';
137
+ }
138
+ else if (timeoutSignal?.aborted === true) {
139
+ code = 'timeout';
140
+ }
141
+ else {
142
+ code = 'connection';
143
+ }
144
+ const detail = code === 'timeout'
145
+ ? 'request timed out'
146
+ : code === 'aborted'
147
+ ? 'request aborted by the caller'
148
+ : 'connection failed before a response was received';
149
+ return new ByteKitConnectionError(code, `ByteKit: ${method} ${url} — ${detail}`, {
150
+ cause: err,
151
+ });
152
+ }
153
+ export class ByteKit {
23
154
  apiKey;
24
155
  baseUrl;
156
+ defaultTimeoutMs;
25
157
  scrape;
26
158
  screenshots;
27
- recordings;
28
159
  bulk;
29
160
  fetch;
30
161
  monitors;
@@ -32,11 +163,14 @@ export class RapidCrawl {
32
163
  search;
33
164
  account;
34
165
  constructor(options) {
166
+ if (!options?.apiKey) {
167
+ throw new TypeError('ByteKit: `apiKey` is required — got an empty value. Pass options.apiKey (e.g. from process.env.BYTEKIT_API_KEY).');
168
+ }
35
169
  this.apiKey = options.apiKey;
36
170
  this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
171
+ this.defaultTimeoutMs = validateTimeout(options.timeoutMs, '"timeoutMs"') ?? DEFAULT_TIMEOUT_MS;
37
172
  this.scrape = new ScrapeResource(this);
38
173
  this.screenshots = new ScreenshotsResource(this);
39
- this.recordings = new RecordingsResource(this);
40
174
  this.bulk = new BulkResource(this);
41
175
  this.fetch = new FetchResource(this);
42
176
  this.monitors = new MonitorsResource(this);
@@ -45,12 +179,51 @@ export class RapidCrawl {
45
179
  this.account = new AccountResource(this);
46
180
  }
47
181
  /**
48
- * Make an authenticated JSON request to the RapidCrawl API.
182
+ * Resolve the abort budget for one request from the caller's options and the
183
+ * constructor default. Precedence:
184
+ *
185
+ * | caller `signal` | per-call `requestTimeoutMs` | signal attached to `fetch` |
186
+ * |-----------------|----------------------------|---------------------------------------|
187
+ * | absent | absent | timeout from the constructor default |
188
+ * | absent | present | timeout from the per-call value |
189
+ * | present | absent | the caller's signal, verbatim |
190
+ * | present | present | composition — aborts on either |
191
+ *
192
+ * A timeout of `0` (either level) means "no SDK-side timeout".
193
+ */
194
+ resolveSignal(options) {
195
+ const callerSignal = options?.signal;
196
+ const perCallMs = validateTimeout(options?.requestTimeoutMs, '"requestTimeoutMs"');
197
+ // A caller-supplied signal suppresses the constructor default (it does not
198
+ // suppress an explicit per-call budget, which composes with it instead).
199
+ const effectiveMs = perCallMs ?? (callerSignal !== undefined ? 0 : this.defaultTimeoutMs);
200
+ if (effectiveMs === 0) {
201
+ return { signal: callerSignal, timeoutSignal: undefined, cleanup: NOOP };
202
+ }
203
+ const timeout = createTimeoutSignal(effectiveMs);
204
+ if (callerSignal === undefined) {
205
+ return { signal: timeout.signal, timeoutSignal: timeout.signal, cleanup: timeout.cleanup };
206
+ }
207
+ const composed = composeSignals(callerSignal, timeout.signal);
208
+ return {
209
+ signal: composed.signal,
210
+ timeoutSignal: timeout.signal,
211
+ cleanup: () => {
212
+ composed.cleanup();
213
+ timeout.cleanup();
214
+ },
215
+ };
216
+ }
217
+ /**
218
+ * Make an authenticated JSON request to the ByteKit API.
49
219
  *
50
- * Throws `RapidCrawlError` on 4xx/5xx responses.
220
+ * Throws `ByteKitError` on 4xx/5xx responses, and `ByteKitConnectionError`
221
+ * when no HTTP response was produced at all (DNS/socket/TLS failure, client-side
222
+ * timeout, or caller abort).
51
223
  */
52
- async request(method, path, body) {
224
+ async request(method, path, body, options) {
53
225
  const url = `${this.baseUrl}${path}`;
226
+ const { signal, timeoutSignal, cleanup } = this.resolveSignal(options);
54
227
  const init = {
55
228
  method,
56
229
  headers: {
@@ -61,7 +234,22 @@ export class RapidCrawl {
61
234
  if (body !== undefined) {
62
235
  init.body = JSON.stringify(body);
63
236
  }
64
- const response = await fetch(url, init);
237
+ // `exactOptionalPropertyTypes` — assign only when we actually have a signal.
238
+ if (signal !== undefined) {
239
+ init.signal = signal;
240
+ }
241
+ let response;
242
+ try {
243
+ response = await fetch(url, init);
244
+ }
245
+ catch (err) {
246
+ throw toConnectionError(err, method, url, timeoutSignal);
247
+ }
248
+ finally {
249
+ // Runs on success, on HTTP error, and on abort alike — no dangling timer,
250
+ // no listener left behind on the caller's (long-lived) signal.
251
+ cleanup();
252
+ }
65
253
  if (!response.ok) {
66
254
  let code = 'internal_error';
67
255
  let message = response.statusText;
@@ -73,9 +261,32 @@ export class RapidCrawl {
73
261
  catch {
74
262
  // leave defaults
75
263
  }
76
- throw new RapidCrawlError(response.status, code, message);
264
+ throw new ByteKitError(response.status, code, message);
265
+ }
266
+ // A 204 No Content has an empty body, and a genuinely empty body can arrive on
267
+ // other success codes too (defensive). Calling response.json() on an empty body
268
+ // rejects with a SyntaxError — which is how a successful revoke() (DELETE → 204)
269
+ // used to throw instead of resolving. Read the body once as text and short-circuit
270
+ // to `undefined` when it is empty; a JSON-bearing response (scrape.create,
271
+ // apiKeys.create/get, …) still parses and returns unchanged.
272
+ if (response.status === 204) {
273
+ return undefined;
274
+ }
275
+ const text = await response.text();
276
+ if (text.trim().length === 0) {
277
+ return undefined;
77
278
  }
78
- return response.json();
279
+ return JSON.parse(text);
79
280
  }
80
281
  }
81
- //# sourceMappingURL=client.js.map
282
+ // ---------------------------------------------------------------------------
283
+ // Deprecated RapidCrawl* aliases (#2421).
284
+ //
285
+ // The public surface renamed from RapidCrawl to ByteKit. These aliases point at
286
+ // the SAME implementations (identity preserved: `RapidCrawl === ByteKit`), so an
287
+ // existing consumer's `instanceof` / error-shape contract keeps holding for one
288
+ // minor version. Exported here — not only from index.ts — so the SDK's own
289
+ // resource files can keep importing the legacy type name from './client.js'
290
+ // without churn. Re-exported (with @deprecated JSDoc) from index.ts.
291
+ // ---------------------------------------------------------------------------
292
+ export { ByteKit as RapidCrawl, ByteKitError as RapidCrawlError, ByteKitConnectionError as RapidCrawlConnectionError, };