@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 +14 -0
- package/README.md +106 -55
- package/package.json +1 -1
- package/src/client.js +87 -18
- package/src/index.d.ts +2 -0
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
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
12
|
+
## Install
|
|
9
13
|
|
|
10
|
-
|
|
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
|
-
|
|
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@
|
|
23
|
+
npm install @crawlora-org/sdk@VERSION
|
|
18
24
|
```
|
|
19
25
|
|
|
20
|
-
|
|
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
|
-
|
|
32
|
+
read -r CRAWLORA_API_KEY
|
|
33
|
+
export CRAWLORA_API_KEY
|
|
24
34
|
```
|
|
25
35
|
|
|
26
|
-
##
|
|
36
|
+
## First Request
|
|
27
37
|
|
|
28
38
|
```js
|
|
29
39
|
import { CrawloraClient } from "@crawlora-org/sdk";
|
|
30
40
|
|
|
31
|
-
const crawlora = new CrawloraClient({
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
42
|
-
|
|
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: {
|
|
83
|
+
headers: {
|
|
84
|
+
"x-client": "my-app"
|
|
85
|
+
}
|
|
54
86
|
});
|
|
55
87
|
```
|
|
56
88
|
|
|
57
|
-
Per-request options
|
|
89
|
+
Per-request options can override headers, timeout, abort signal, and response
|
|
90
|
+
mode:
|
|
58
91
|
|
|
59
92
|
```js
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
parsed `body`, and the original `response` when available.
|
|
105
|
+
## Text Responses
|
|
68
106
|
|
|
69
|
-
|
|
107
|
+
Most endpoints return JSON. Endpoints that support alternate text output, such
|
|
108
|
+
as YouTube transcripts, can opt into text mode:
|
|
70
109
|
|
|
71
|
-
|
|
72
|
-
|
|
110
|
+
```js
|
|
111
|
+
const transcript = await crawlora.youtube.transcript(
|
|
112
|
+
{ id: "VIDEO_ID", format: "text" },
|
|
113
|
+
{ responseType: "text" }
|
|
114
|
+
);
|
|
73
115
|
|
|
74
|
-
|
|
116
|
+
console.log(transcript);
|
|
117
|
+
```
|
|
75
118
|
|
|
76
|
-
|
|
119
|
+
## Errors
|
|
77
120
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
141
|
+
Runnable examples live under `examples/` and skip cleanly when required
|
|
142
|
+
environment variables are missing:
|
|
88
143
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
108
|
-
because they already exist on npm and point to
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
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.
|
|
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(
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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