@crawlora-org/sdk 1.2.0-sdk.13 → 1.2.0-sdk.15

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
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.2.0-sdk.15
4
+
5
+ - Added case-insensitive request header overrides across auth, user-agent, and
6
+ content headers.
7
+ - Added strict response mode validation, response headers on SDK errors, and
8
+ `Retry-After` aware retry delays capped at 30 seconds.
9
+ - Clarified timeout and external abort errors without retrying user-aborted
10
+ requests.
11
+
12
+ ## v1.2.0-sdk.14
13
+
14
+ - Aligned the promoted SDK beta tag with the Python and Go SDKs.
15
+ - Added explicit coverage for request-level header overrides.
16
+
3
17
  ## v1.2.0-sdk.13
4
18
 
5
19
  - Updated the published npmjs package homepage to `https://crawlora.net`.
package/README.md CHANGED
@@ -1,102 +1,154 @@
1
1
  # Crawlora JavaScript SDK
2
2
 
3
- JavaScript and TypeScript SDK for the public Crawlora API. It works in Node.js
4
- 18+ and modern runtimes with `fetch`.
3
+ Official JavaScript and TypeScript client for the public Crawlora API. Use it
4
+ to call Crawlora scraping, search, marketplace, media, maps, finance, and usage
5
+ endpoints with a small `fetch`-based client and generated TypeScript types.
5
6
 
6
- Website: [crawlora.net](https://crawlora.net)
7
+ - Runtime: Node.js 18+ or any modern runtime with `fetch`
8
+ - Auth: `x-api-key`
9
+ - Default API base URL: `https://api.crawlora.net/api/v1`
10
+ - Reference: [operations](docs/operations.md) and [recipes](docs/recipes.md)
7
11
 
8
- ## Get an API Key
12
+ ## Install
9
13
 
10
- Create or sign in to your Crawlora account at [crawlora.net](https://crawlora.net),
11
- then open the dashboard and create an API key. Set it as `CRAWLORA_API_KEY` in
12
- your environment before running the examples or using the SDK.
14
+ Install the latest published npm package:
13
15
 
14
- ## Install
16
+ ```sh
17
+ npm install @crawlora-org/sdk
18
+ ```
19
+
20
+ For reproducible builds, pin a published package version:
15
21
 
16
22
  ```sh
17
- npm install @crawlora-org/sdk@latest
23
+ npm install @crawlora-org/sdk@VERSION
18
24
  ```
19
25
 
20
- For reproducible builds, pin the current package version:
26
+ ## API Key
27
+
28
+ Create or sign in to your Crawlora account at [crawlora.net](https://crawlora.net),
29
+ then create an API key in the dashboard.
21
30
 
22
31
  ```sh
23
- npm install @crawlora-org/sdk@1.2.0-sdk.13
32
+ read -r CRAWLORA_API_KEY
33
+ export CRAWLORA_API_KEY
24
34
  ```
25
35
 
26
- ## Usage
36
+ ## First Request
27
37
 
28
38
  ```js
29
39
  import { CrawloraClient } from "@crawlora-org/sdk";
30
40
 
31
- const crawlora = new CrawloraClient({ apiKey: process.env.CRAWLORA_API_KEY });
32
- const result = await crawlora.bing.search({ q: "coffee shops", count: 10 });
33
- console.log(result);
41
+ const crawlora = new CrawloraClient({
42
+ apiKey: process.env.CRAWLORA_API_KEY
43
+ });
44
+
45
+ const response = await crawlora.bing.search({
46
+ q: "coffee shops",
47
+ count: 10
48
+ });
49
+
50
+ console.log(response.data?.results?.[0]);
34
51
  ```
35
52
 
36
- Generated TypeScript declarations cover operation ids, endpoint groups,
37
- parameter objects, enum values, request options, and response aliases.
38
- Dynamic calls infer parameter and response types from literal operation ids:
53
+ Endpoint groups are generated from the public API contract, so common calls are
54
+ available as methods such as `crawlora.bing.search(...)`,
55
+ `crawlora.youtube.transcript(...)`, and `crawlora.google.mapSearch(...)`.
56
+
57
+ ## Typed Dynamic Calls
58
+
59
+ You can also call by operation id. Literal operation ids infer parameter and
60
+ response types in TypeScript:
39
61
 
40
62
  ```ts
41
- const result = await crawlora.request("bing-search", { q: "coffee shops" });
42
- result.data?.results?.[0]?.title;
63
+ const response = await crawlora.request("bing-search", {
64
+ q: "coffee shops",
65
+ count: 10
66
+ });
67
+
68
+ response.data?.results?.[0]?.title;
43
69
  ```
44
70
 
71
+ Generated declarations include operation ids, endpoint groups, request
72
+ parameters, enum values, response aliases, and request options.
73
+
45
74
  ## Configuration
46
75
 
47
76
  ```js
48
77
  const crawlora = new CrawloraClient({
49
78
  apiKey: process.env.CRAWLORA_API_KEY,
50
79
  baseUrl: "https://api.crawlora.net/api/v1",
80
+ timeout: 30_000,
51
81
  retries: 2,
52
82
  retryDelay: 250,
53
- headers: { "x-client": "example" }
83
+ headers: {
84
+ "x-client": "my-app"
85
+ }
54
86
  });
55
87
  ```
56
88
 
57
- Per-request options support headers, timeout, abort signals, and response mode:
89
+ Per-request options can override headers, timeout, abort signal, and response
90
+ mode:
58
91
 
59
92
  ```js
60
- const text = await crawlora.youtube.transcript(
61
- { id: "VIDEO_ID", format: "text" },
62
- { responseType: "text", timeout: 10000 }
93
+ const controller = new AbortController();
94
+
95
+ const response = await crawlora.bing.search(
96
+ { q: "coffee shops" },
97
+ {
98
+ timeout: 10_000,
99
+ signal: controller.signal,
100
+ headers: { "x-request-id": "search-001" }
101
+ }
63
102
  );
64
103
  ```
65
104
 
66
- Failed API calls throw `CrawloraError` with `status`, optional API `code`,
67
- parsed `body`, and the original `response` when available.
105
+ ## Text Responses
68
106
 
69
- ## Reference
107
+ Most endpoints return JSON. Endpoints that support alternate text output, such
108
+ as YouTube transcripts, can opt into text mode:
70
109
 
71
- - [Operation reference](docs/operations.md)
72
- - [Usage recipes](docs/recipes.md)
110
+ ```js
111
+ const transcript = await crawlora.youtube.transcript(
112
+ { id: "VIDEO_ID", format: "text" },
113
+ { responseType: "text" }
114
+ );
73
115
 
74
- ## Examples
116
+ console.log(transcript);
117
+ ```
75
118
 
76
- Runnable examples live under `examples/`:
119
+ ## Errors
77
120
 
78
- ```sh
79
- CRAWLORA_API_KEY=... npm run example:bing-search
80
- CRAWLORA_API_KEY=... CRAWLORA_YOUTUBE_VIDEO_ID=... npm run example:youtube-transcript
121
+ Failed API calls throw `CrawloraError`:
122
+
123
+ ```js
124
+ import { CrawloraClient, CrawloraError } from "@crawlora-org/sdk";
125
+
126
+ try {
127
+ await crawlora.bing.search({ q: "coffee shops" });
128
+ } catch (error) {
129
+ if (error instanceof CrawloraError) {
130
+ console.error(error.status, error.code, error.body);
131
+ }
132
+ throw error;
133
+ }
81
134
  ```
82
135
 
83
- Each example also accepts `CRAWLORA_BASE_URL` for staging or local API testing.
84
- The examples exit without making a request when the required live environment
85
- variables are not set. `npm run smoke:live` runs all live examples in sequence.
136
+ The error includes `status`, optional API `code`, parsed `body`, the original
137
+ `response` when available, and a parser or transport `cause` when relevant.
138
+
139
+ ## Examples
86
140
 
87
- ## Versioning
141
+ Runnable examples live under `examples/` and skip cleanly when required
142
+ environment variables are missing:
88
143
 
89
- This SDK is published to npmjs and mirrored to GitHub Packages. The package
90
- `latest` dist-tag and the moving Git tag named `latest` track the current
91
- promoted beta. Explicit package versions and Git beta tags such as
92
- `1.2.0-sdk.13` / `v1.2.0-sdk.13` remain available for reproducible builds. Pin
93
- an explicit version in production applications and upgrade intentionally.
144
+ ```sh
145
+ npm run example:bing-search
146
+ npm run example:youtube-transcript
147
+ ```
94
148
 
95
- GitHub Packages installs require a project `.npmrc` that maps the
96
- `@crawlora-org` scope to `https://npm.pkg.github.com` and a GitHub token that
97
- can read packages.
149
+ Set `CRAWLORA_BASE_URL` to point examples at a staging or local API.
98
150
 
99
- ## Package Name
151
+ ## Package Notes
100
152
 
101
153
  The npm package name is `@crawlora-org/sdk`:
102
154
 
@@ -104,11 +156,10 @@ The npm package name is `@crawlora-org/sdk`:
104
156
  import { CrawloraClient } from "@crawlora-org/sdk";
105
157
  ```
106
158
 
107
- The package names `crawlora` and `@crawlora/sdk` are not used by this repository
108
- because they already exist on npm and point to a different package source.
109
-
110
- ## Optional Live Smoke Test
159
+ The package names `crawlora` and `@crawlora/sdk` are not used by this
160
+ repository because they already exist on npm and point to different package
161
+ sources.
111
162
 
112
- Default tests use mock `fetch` implementations. The programs under `examples/`
113
- can be used as optional live smoke tests when `CRAWLORA_API_KEY` is available.
114
- Live calls are not part of default CI.
163
+ This SDK is published to npmjs and may also be mirrored to GitHub Packages. Pin
164
+ an explicit published version for production applications and upgrade
165
+ intentionally.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlora-org/sdk",
3
- "version": "1.2.0-sdk.13",
3
+ "version": "1.2.0-sdk.15",
4
4
  "description": "JavaScript and TypeScript SDK for the public Crawlora API.",
5
5
  "keywords": [
6
6
  "crawlora",
package/src/client.js CHANGED
@@ -1,18 +1,20 @@
1
1
  import { operations, groups } from "./operations.js";
2
2
 
3
3
  const DEFAULT_BASE_URL = "https://api.crawlora.net/api/v1";
4
- export const VERSION = "1.2.0-sdk.13";
4
+ export const VERSION = "1.2.0-sdk.15";
5
5
  const DEFAULT_USER_AGENT = `crawlora-js-sdk/${VERSION}`;
6
6
 
7
7
  export class CrawloraError extends Error {
8
- constructor(message, { status = 0, code, body, response, cause } = {}) {
8
+ constructor(message, { status = 0, code, body, headers = {}, response, cause, retryable } = {}) {
9
9
  super(message);
10
10
  this.name = "CrawloraError";
11
11
  this.status = status;
12
12
  this.code = code;
13
13
  this.body = body;
14
+ this.headers = headers;
14
15
  this.response = response;
15
16
  this.cause = cause;
17
+ this.retryable = retryable;
16
18
  }
17
19
  }
18
20
 
@@ -52,34 +54,39 @@ export class CrawloraClient {
52
54
  }
53
55
  params = params ?? {};
54
56
  options = options ?? {};
57
+ const responseType = validateResponseType(options.responseType || "auto");
55
58
 
56
59
  let attempt = 0;
57
60
  for (;;) {
58
61
  try {
59
- return await this.#send(operation, params, options);
62
+ return await this.#send(operation, params, options, responseType);
60
63
  } catch (error) {
61
- if (!(error instanceof CrawloraError) || attempt >= this.retries || !shouldRetry(error.status)) {
64
+ if (!(error instanceof CrawloraError) || error.retryable === false || attempt >= this.retries || !shouldRetry(error.status)) {
62
65
  throw error;
63
66
  }
64
67
  attempt++;
65
- await sleep(retryDelay(this.retryDelay, attempt), options.signal);
68
+ await sleep(retryDelayForError(error, this.retryDelay, attempt), options.signal);
66
69
  }
67
70
  }
68
71
  }
69
72
 
70
- async #send(operation, params, options) {
73
+ async #send(operation, params, options, responseType) {
71
74
  const { url, body, bodyHeaders } = buildRequest(operation, this.baseUrl, params);
72
- const headers = {
73
- ...this.headers,
74
- ...authHeaders(operation.security, this.apiKey, this.jwtToken),
75
- ...userAgentHeader(this.userAgent),
76
- ...bodyHeaders,
77
- ...(options.headers || {})
78
- };
75
+ const headers = mergeHeaders(
76
+ this.headers,
77
+ authHeaders(operation.security, this.apiKey, this.jwtToken),
78
+ userAgentHeader(this.userAgent),
79
+ bodyHeaders,
80
+ options.headers
81
+ );
79
82
 
80
83
  const controller = new AbortController();
81
84
  const timeoutMs = options.timeout ?? this.timeout;
82
- const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
85
+ let timedOut = false;
86
+ const timeout = timeoutMs > 0 ? setTimeout(() => {
87
+ timedOut = true;
88
+ controller.abort();
89
+ }, timeoutMs) : undefined;
83
90
  const signal = composeSignal(controller, options.signal);
84
91
  let response;
85
92
  try {
@@ -90,12 +97,19 @@ export class CrawloraClient {
90
97
  signal
91
98
  });
92
99
  } catch (error) {
100
+ if (options.signal?.aborted) {
101
+ throw new CrawloraError("Crawlora request aborted", { cause: error, retryable: false });
102
+ }
103
+ if (timedOut) {
104
+ throw new CrawloraError("Crawlora request timed out", { cause: error, retryable: false });
105
+ }
93
106
  throw new CrawloraError("Crawlora transport error", { cause: error });
94
107
  } finally {
95
108
  if (timeout) clearTimeout(timeout);
96
109
  }
97
110
 
98
- const parsed = await parseResponse(response, options.responseType || "auto");
111
+ const headersObject = responseHeaders(response);
112
+ const parsed = await parseResponse(response, responseType, headersObject);
99
113
  if (!response.ok) {
100
114
  const code = parsed && typeof parsed === "object" ? parsed.code : undefined;
101
115
  const message = parsed && typeof parsed === "object" && parsed.msg ? parsed.msg : response.statusText;
@@ -103,6 +117,7 @@ export class CrawloraClient {
103
117
  status: response.status,
104
118
  code,
105
119
  body: parsed,
120
+ headers: headersObject,
106
121
  response
107
122
  });
108
123
  }
@@ -203,6 +218,22 @@ function userAgentHeader(userAgent) {
203
218
  return { "user-agent": userAgent };
204
219
  }
205
220
 
221
+ function mergeHeaders(...sources) {
222
+ const headers = {};
223
+ const names = new Map();
224
+ for (const source of sources) {
225
+ for (const [name, value] of Object.entries(source || {})) {
226
+ if (value === undefined || value === null) continue;
227
+ const lower = name.toLowerCase();
228
+ const existing = names.get(lower);
229
+ if (existing && existing !== name) delete headers[existing];
230
+ headers[name] = String(value);
231
+ names.set(lower, name);
232
+ }
233
+ }
234
+ return headers;
235
+ }
236
+
206
237
  function composeSignal(controller, signal) {
207
238
  if (!signal) return controller.signal;
208
239
  if (signal.aborted) controller.abort();
@@ -210,7 +241,14 @@ function composeSignal(controller, signal) {
210
241
  return controller.signal;
211
242
  }
212
243
 
213
- async function parseResponse(response, responseType) {
244
+ function validateResponseType(responseType) {
245
+ if (responseType === "auto" || responseType === "json" || responseType === "text") {
246
+ return responseType;
247
+ }
248
+ throw new TypeError("Invalid responseType: expected one of auto, json, text");
249
+ }
250
+
251
+ async function parseResponse(response, responseType, headers) {
214
252
  if (responseType === "text") return response.text();
215
253
  const contentType = response.headers.get("content-type") || "";
216
254
  if (responseType === "json" || contentType.toLowerCase().includes("application/json")) {
@@ -221,6 +259,7 @@ async function parseResponse(response, responseType) {
221
259
  throw new CrawloraError("Crawlora JSON parse error", {
222
260
  status: response.status,
223
261
  body: text,
262
+ headers,
224
263
  response,
225
264
  cause: error
226
265
  });
@@ -240,6 +279,36 @@ function retryDelay(baseDelay, attempt) {
240
279
  return delay + jitter;
241
280
  }
242
281
 
282
+ function retryDelayForError(error, baseDelay, attempt) {
283
+ return parseRetryAfter(error.headers) ?? retryDelay(baseDelay, attempt);
284
+ }
285
+
286
+ function parseRetryAfter(headers) {
287
+ const value = headerValue(headers, "retry-after");
288
+ if (!value) return undefined;
289
+ const seconds = Number(value);
290
+ if (Number.isFinite(seconds) && seconds > 0) {
291
+ return Math.min(seconds * 1000, 30000);
292
+ }
293
+ const date = Date.parse(value);
294
+ if (Number.isFinite(date)) {
295
+ const delay = date - Date.now();
296
+ if (delay > 0) return Math.min(delay, 30000);
297
+ }
298
+ return undefined;
299
+ }
300
+
301
+ function headerValue(headers, name) {
302
+ for (const [key, value] of Object.entries(headers || {})) {
303
+ if (key.toLowerCase() === name.toLowerCase()) return value;
304
+ }
305
+ return "";
306
+ }
307
+
308
+ function responseHeaders(response) {
309
+ return Object.fromEntries(response.headers.entries());
310
+ }
311
+
243
312
  function normalizeNonNegativeInteger(value) {
244
313
  const number = Number(value);
245
314
  if (!Number.isFinite(number) || number <= 0) return 0;
@@ -256,13 +325,13 @@ function sleep(ms, signal) {
256
325
  if (!ms || ms <= 0) return Promise.resolve();
257
326
  return new Promise((resolve, reject) => {
258
327
  if (signal?.aborted) {
259
- reject(new CrawloraError("Crawlora request aborted", { cause: signal.reason }));
328
+ reject(new CrawloraError("Crawlora request aborted", { cause: signal.reason, retryable: false }));
260
329
  return;
261
330
  }
262
331
  const timer = setTimeout(resolve, ms);
263
332
  signal?.addEventListener("abort", () => {
264
333
  clearTimeout(timer);
265
- reject(new CrawloraError("Crawlora request aborted", { cause: signal.reason }));
334
+ reject(new CrawloraError("Crawlora request aborted", { cause: signal.reason, retryable: false }));
266
335
  }, { once: true });
267
336
  });
268
337
  }
package/src/index.d.ts CHANGED
@@ -49,8 +49,10 @@ export class CrawloraError extends Error {
49
49
  status: number;
50
50
  code?: number;
51
51
  body: unknown;
52
+ headers: Record<string, string>;
52
53
  response?: Response;
53
54
  cause?: unknown;
55
+ retryable?: boolean;
54
56
  }
55
57
 
56
58
  export class CrawloraClient {