scavio 0.22.0 → 0.23.1
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/README.md +137 -2
- package/dist/index.cjs +252 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +374 -28
- package/dist/index.d.ts +374 -28
- package/dist/index.js +238 -31
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -81,6 +81,22 @@ var ScavioAPIError = class extends ScavioError {
|
|
|
81
81
|
this.responseBody = responseBody;
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
|
+
var ScavioJobTimeoutError = class extends ScavioError {
|
|
85
|
+
constructor(jobType, jobId, statusUrl, lastStatus) {
|
|
86
|
+
super(
|
|
87
|
+
`${jobType} ${jobId} was still ${lastStatus} when the wait timed out. It keeps running: poll extract.${jobType}Status("${jobId}") or cancel it.`
|
|
88
|
+
);
|
|
89
|
+
this.jobType = jobType;
|
|
90
|
+
this.jobId = jobId;
|
|
91
|
+
this.statusUrl = statusUrl;
|
|
92
|
+
this.lastStatus = lastStatus;
|
|
93
|
+
this.name = "ScavioJobTimeoutError";
|
|
94
|
+
}
|
|
95
|
+
jobType;
|
|
96
|
+
jobId;
|
|
97
|
+
statusUrl;
|
|
98
|
+
lastStatus;
|
|
99
|
+
};
|
|
84
100
|
|
|
85
101
|
// src/retry.ts
|
|
86
102
|
var DEFAULT_RETRY_STATUSES = /* @__PURE__ */ new Set([
|
|
@@ -129,7 +145,7 @@ function parseRetryAfter(header) {
|
|
|
129
145
|
var BASE_URL = "https://api.scavio.dev";
|
|
130
146
|
var DEFAULT_TIMEOUT = 3e4;
|
|
131
147
|
var DEFAULT_MAX_RETRIES = 2;
|
|
132
|
-
var SDK_VERSION = true ? "0.
|
|
148
|
+
var SDK_VERSION = true ? "0.23.1" : "0.0.0";
|
|
133
149
|
var USER_AGENT = `scavio-js/${SDK_VERSION}`;
|
|
134
150
|
function isServerRuntime() {
|
|
135
151
|
const g = globalThis;
|
|
@@ -187,9 +203,18 @@ function getHeader(response, name) {
|
|
|
187
203
|
}
|
|
188
204
|
return null;
|
|
189
205
|
}
|
|
206
|
+
function buildQuery(query) {
|
|
207
|
+
if (!query) return "";
|
|
208
|
+
const params = new URLSearchParams();
|
|
209
|
+
for (const [key, value] of Object.entries(query)) {
|
|
210
|
+
if (value !== void 0) params.append(key, String(value));
|
|
211
|
+
}
|
|
212
|
+
const qs = params.toString();
|
|
213
|
+
return qs ? `?${qs}` : "";
|
|
214
|
+
}
|
|
190
215
|
async function request(options) {
|
|
191
|
-
const url = `${options.baseUrl}${options.path}`;
|
|
192
|
-
const headers = buildHeaders(options.apiKey);
|
|
216
|
+
const url = `${options.baseUrl}${options.path}${buildQuery(options.query)}`;
|
|
217
|
+
const headers = { ...buildHeaders(options.apiKey), ...options.headers };
|
|
193
218
|
const retry = makeRetryConfig(
|
|
194
219
|
options.maxRetries ?? DEFAULT_MAX_RETRIES
|
|
195
220
|
);
|
|
@@ -451,6 +476,14 @@ var TikTokNamespace = class {
|
|
|
451
476
|
async video(options) {
|
|
452
477
|
return this.client._post("/api/v1/tiktok/video", options);
|
|
453
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* Spoken-word transcript of a TikTok video, as timestamped segments and
|
|
481
|
+
* full text. Pass `video_id` or `url`. Videos with no spoken captions
|
|
482
|
+
* (music, dance, muted clips) return `available: false`. 1 credit.
|
|
483
|
+
*/
|
|
484
|
+
async videoTranscript(options) {
|
|
485
|
+
return this.client._post("/api/v1/tiktok/video/transcript", options);
|
|
486
|
+
}
|
|
454
487
|
async videoComments(options) {
|
|
455
488
|
return this.client._post("/api/v1/tiktok/video/comments", options);
|
|
456
489
|
}
|
|
@@ -2871,10 +2904,135 @@ var MercadoLibreNamespace = class {
|
|
|
2871
2904
|
}
|
|
2872
2905
|
};
|
|
2873
2906
|
|
|
2907
|
+
// src/namespaces/extract.ts
|
|
2908
|
+
var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
2909
|
+
var DEFAULT_POLL_INTERVAL = 2e3;
|
|
2910
|
+
var DEFAULT_WAIT_TIMEOUT = 6e5;
|
|
2911
|
+
var COLLECT_PAGE_SIZE = 100;
|
|
2912
|
+
function sleep2(ms) {
|
|
2913
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2914
|
+
}
|
|
2915
|
+
async function randomUUID() {
|
|
2916
|
+
const g = globalThis;
|
|
2917
|
+
if (typeof g.crypto?.randomUUID === "function") return g.crypto.randomUUID();
|
|
2918
|
+
const nodeCrypto = await import("crypto");
|
|
2919
|
+
return nodeCrypto.randomUUID();
|
|
2920
|
+
}
|
|
2921
|
+
var ExtractNamespace = class {
|
|
2922
|
+
constructor(client) {
|
|
2923
|
+
this.client = client;
|
|
2924
|
+
}
|
|
2925
|
+
client;
|
|
2926
|
+
/**
|
|
2927
|
+
* List a site's page URLs, gathered from its sitemaps and its start page,
|
|
2928
|
+
* deduplicated, filtered by path and limited. Pages disallowed by the
|
|
2929
|
+
* site's robots.txt are left out. 1 credit on the normal tier.
|
|
2930
|
+
*/
|
|
2931
|
+
async map(options) {
|
|
2932
|
+
return await this.client._post("/api/v1/extract/map", options);
|
|
2933
|
+
}
|
|
2934
|
+
async crawl(options, request2 = {}) {
|
|
2935
|
+
return this.start("crawl", options, request2);
|
|
2936
|
+
}
|
|
2937
|
+
/** A crawl's progress and its page results, paged by cursor. Free. */
|
|
2938
|
+
async crawlStatus(id, query) {
|
|
2939
|
+
return this.status("crawl", id, query);
|
|
2940
|
+
}
|
|
2941
|
+
/**
|
|
2942
|
+
* Stop a queued or running crawl. No further pages are fetched or billed;
|
|
2943
|
+
* results so far stay available. A finished crawl answers 409. Free.
|
|
2944
|
+
*/
|
|
2945
|
+
async cancelCrawl(id) {
|
|
2946
|
+
return this.cancel("crawl", id);
|
|
2947
|
+
}
|
|
2948
|
+
async batch(options, request2 = {}) {
|
|
2949
|
+
return this.start("batch", options, request2);
|
|
2950
|
+
}
|
|
2951
|
+
/** A batch's progress and its page results in input order, paged by cursor. Free. */
|
|
2952
|
+
async batchStatus(id, query) {
|
|
2953
|
+
return this.status("batch", id, query);
|
|
2954
|
+
}
|
|
2955
|
+
/** Stop a queued or running batch. A finished batch answers 409. Free. */
|
|
2956
|
+
async cancelBatch(id) {
|
|
2957
|
+
return this.cancel("batch", id);
|
|
2958
|
+
}
|
|
2959
|
+
async start(type, body, request2) {
|
|
2960
|
+
const headers = { "Idempotency-Key": request2.idempotencyKey ?? await randomUUID() };
|
|
2961
|
+
const created = await this.client._post(
|
|
2962
|
+
`/api/v1/extract/${type}`,
|
|
2963
|
+
body,
|
|
2964
|
+
headers
|
|
2965
|
+
);
|
|
2966
|
+
if (!request2.wait) return created;
|
|
2967
|
+
const done = await this.waitFor(type, created.data.id, created.data.status_url, request2);
|
|
2968
|
+
return { ...done, created: created.data };
|
|
2969
|
+
}
|
|
2970
|
+
async status(type, id, query = {}) {
|
|
2971
|
+
return await this.client._get(`/api/v1/extract/${type}/${encodeURIComponent(id)}`, {
|
|
2972
|
+
cursor: query.cursor,
|
|
2973
|
+
limit: query.limit,
|
|
2974
|
+
status: query.status,
|
|
2975
|
+
include_content: query.include_content
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
async cancel(type, id) {
|
|
2979
|
+
return await this.client._delete(
|
|
2980
|
+
`/api/v1/extract/${type}/${encodeURIComponent(id)}`
|
|
2981
|
+
);
|
|
2982
|
+
}
|
|
2983
|
+
async waitFor(type, id, statusUrl, request2) {
|
|
2984
|
+
const interval = request2.pollInterval ?? DEFAULT_POLL_INTERVAL;
|
|
2985
|
+
const deadline = Date.now() + (request2.waitTimeout ?? DEFAULT_WAIT_TIMEOUT);
|
|
2986
|
+
for (; ; ) {
|
|
2987
|
+
const probe = await this.status(type, id, { limit: 1, include_content: false });
|
|
2988
|
+
if (TERMINAL.has(probe.data.status)) break;
|
|
2989
|
+
if (Date.now() + interval > deadline) {
|
|
2990
|
+
throw new ScavioJobTimeoutError(type, id, statusUrl, probe.data.status);
|
|
2991
|
+
}
|
|
2992
|
+
await sleep2(interval);
|
|
2993
|
+
}
|
|
2994
|
+
const results = [];
|
|
2995
|
+
let cursor;
|
|
2996
|
+
let last;
|
|
2997
|
+
do {
|
|
2998
|
+
last = await this.status(type, id, { cursor, limit: COLLECT_PAGE_SIZE });
|
|
2999
|
+
results.push(...last.data.results);
|
|
3000
|
+
cursor = last.data.next_cursor ?? void 0;
|
|
3001
|
+
} while (cursor);
|
|
3002
|
+
return { data: { ...last.data, results, next_cursor: null } };
|
|
3003
|
+
}
|
|
3004
|
+
};
|
|
3005
|
+
|
|
2874
3006
|
// src/client.ts
|
|
2875
3007
|
var DEFAULT_MAX_REQUESTS_PER_SECOND = 1;
|
|
2876
3008
|
var MAX_REQUESTS_PER_SECOND = 50;
|
|
2877
|
-
var
|
|
3009
|
+
var ScavioCore = class {
|
|
3010
|
+
/**
|
|
3011
|
+
* Read ANY web page and get it back as readability Markdown (the default),
|
|
3012
|
+
* plain text, or raw HTML. Returns `{ url, format, mode, content,
|
|
3013
|
+
* content_length }`.
|
|
3014
|
+
*
|
|
3015
|
+
* Extract is a core endpoint, not a platform: `scavio.extract({ url })`
|
|
3016
|
+
* reads one page, and `scavio.extract.map()` / `.crawl()` / `.batch()` work
|
|
3017
|
+
* on whole sites and URL lists.
|
|
3018
|
+
*
|
|
3019
|
+
* Credits are a function of `mode`, not a flat per-call constant:
|
|
3020
|
+
* "normal" costs 1, "advanced" costs 1, "ultra" costs 2. Billing happens
|
|
3021
|
+
* only on a successful extraction - a dead link, bot wall or timeout costs
|
|
3022
|
+
* nothing.
|
|
3023
|
+
*
|
|
3024
|
+
* Start on "normal". Move to "advanced" when the page builds its content in
|
|
3025
|
+
* the browser, and to "ultra" only when a bot wall blocks the other two.
|
|
3026
|
+
*
|
|
3027
|
+
* @example
|
|
3028
|
+
* const page = await scavio.extract({ url: "https://example.com/pricing" });
|
|
3029
|
+
* console.log(page.content);
|
|
3030
|
+
*/
|
|
3031
|
+
async extract(options) {
|
|
3032
|
+
return this._post("/api/v1/extract", options);
|
|
3033
|
+
}
|
|
3034
|
+
};
|
|
3035
|
+
var Scavio = class extends ScavioCore {
|
|
2878
3036
|
google;
|
|
2879
3037
|
amazon;
|
|
2880
3038
|
walmart;
|
|
@@ -2917,6 +3075,7 @@ var Scavio = class {
|
|
|
2917
3075
|
maxRetries;
|
|
2918
3076
|
rateLimiter;
|
|
2919
3077
|
constructor(config) {
|
|
3078
|
+
super();
|
|
2920
3079
|
this.apiKey = config?.apiKey ?? process.env.SCAVIO_API_KEY ?? "";
|
|
2921
3080
|
if (!this.apiKey) {
|
|
2922
3081
|
throw new MissingAPIKeyError();
|
|
@@ -2967,9 +3126,19 @@ var Scavio = class {
|
|
|
2967
3126
|
this.realtor = new RealtorNamespace(this);
|
|
2968
3127
|
this.rightmove = new RightmoveNamespace(this);
|
|
2969
3128
|
this.mercadolibre = new MercadoLibreNamespace(this);
|
|
3129
|
+
const extractNs = new ExtractNamespace(this);
|
|
3130
|
+
this.extract = Object.assign(ScavioCore.prototype.extract.bind(this), {
|
|
3131
|
+
map: extractNs.map.bind(extractNs),
|
|
3132
|
+
crawl: extractNs.crawl.bind(extractNs),
|
|
3133
|
+
crawlStatus: extractNs.crawlStatus.bind(extractNs),
|
|
3134
|
+
cancelCrawl: extractNs.cancelCrawl.bind(extractNs),
|
|
3135
|
+
batch: extractNs.batch.bind(extractNs),
|
|
3136
|
+
batchStatus: extractNs.batchStatus.bind(extractNs),
|
|
3137
|
+
cancelBatch: extractNs.cancelBatch.bind(extractNs)
|
|
3138
|
+
});
|
|
2970
3139
|
}
|
|
2971
3140
|
/** @internal */
|
|
2972
|
-
async _post(path, body) {
|
|
3141
|
+
async _post(path, body, headers) {
|
|
2973
3142
|
return request({
|
|
2974
3143
|
method: "POST",
|
|
2975
3144
|
path,
|
|
@@ -2978,11 +3147,12 @@ var Scavio = class {
|
|
|
2978
3147
|
timeout: this.timeout,
|
|
2979
3148
|
maxRetries: this.maxRetries,
|
|
2980
3149
|
rateLimiter: this.rateLimiter,
|
|
2981
|
-
body
|
|
3150
|
+
body,
|
|
3151
|
+
headers
|
|
2982
3152
|
});
|
|
2983
3153
|
}
|
|
2984
3154
|
/** @internal */
|
|
2985
|
-
async _get(path) {
|
|
3155
|
+
async _get(path, query) {
|
|
2986
3156
|
return request({
|
|
2987
3157
|
method: "GET",
|
|
2988
3158
|
path,
|
|
@@ -2990,39 +3160,73 @@ var Scavio = class {
|
|
|
2990
3160
|
baseUrl: this.baseUrl,
|
|
2991
3161
|
timeout: this.timeout,
|
|
2992
3162
|
maxRetries: this.maxRetries,
|
|
3163
|
+
rateLimiter: this.rateLimiter,
|
|
3164
|
+
query
|
|
3165
|
+
});
|
|
3166
|
+
}
|
|
3167
|
+
/** @internal */
|
|
3168
|
+
async _delete(path) {
|
|
3169
|
+
return request({
|
|
3170
|
+
method: "DELETE",
|
|
3171
|
+
path,
|
|
3172
|
+
apiKey: this.apiKey,
|
|
3173
|
+
baseUrl: this.baseUrl,
|
|
3174
|
+
timeout: this.timeout,
|
|
3175
|
+
maxRetries: this.maxRetries,
|
|
2993
3176
|
rateLimiter: this.rateLimiter
|
|
2994
3177
|
});
|
|
2995
3178
|
}
|
|
2996
3179
|
async search(options) {
|
|
2997
3180
|
return this.google.search(options);
|
|
2998
3181
|
}
|
|
2999
|
-
/**
|
|
3000
|
-
* Read ANY web page and get it back as readability Markdown (the default),
|
|
3001
|
-
* plain text, or raw HTML. Returns `{ url, format, mode, content,
|
|
3002
|
-
* content_length }`.
|
|
3003
|
-
*
|
|
3004
|
-
* This is a core endpoint, not a platform, so it lives on the client itself:
|
|
3005
|
-
* `scavio.extract({ url })`, never `scavio.extract.extract()`.
|
|
3006
|
-
*
|
|
3007
|
-
* Credits are a function of `mode`, not a flat per-call constant:
|
|
3008
|
-
* "normal" costs 1, "advanced" costs 1, "ultra" costs 2. Billing happens
|
|
3009
|
-
* only on a successful extraction - a dead link, bot wall or timeout costs
|
|
3010
|
-
* nothing.
|
|
3011
|
-
*
|
|
3012
|
-
* Start on "normal". Move to "advanced" when the page builds its content in
|
|
3013
|
-
* the browser, and to "ultra" only when a bot wall blocks the other two.
|
|
3014
|
-
*
|
|
3015
|
-
* @example
|
|
3016
|
-
* const page = await scavio.extract({ url: "https://example.com/pricing" });
|
|
3017
|
-
* console.log(page.content);
|
|
3018
|
-
*/
|
|
3019
|
-
async extract(options) {
|
|
3020
|
-
return this._post("/api/v1/extract", options);
|
|
3021
|
-
}
|
|
3022
3182
|
async getUsage() {
|
|
3023
3183
|
return this._get("/api/v1/usage");
|
|
3024
3184
|
}
|
|
3025
3185
|
};
|
|
3186
|
+
|
|
3187
|
+
// src/webhook.ts
|
|
3188
|
+
async function verifyWebhook(secret, header, body, toleranceSec = 300) {
|
|
3189
|
+
if (!secret || !header) return false;
|
|
3190
|
+
const parts = {};
|
|
3191
|
+
for (const piece of header.split(",")) {
|
|
3192
|
+
const i = piece.indexOf("=");
|
|
3193
|
+
if (i > 0) parts[piece.slice(0, i).trim()] = piece.slice(i + 1).trim();
|
|
3194
|
+
}
|
|
3195
|
+
const t = Number(parts.t);
|
|
3196
|
+
const v1 = parts.v1;
|
|
3197
|
+
if (!parts.t || !Number.isFinite(t) || !v1 || !/^[0-9a-f]+$/i.test(v1)) return false;
|
|
3198
|
+
if (Math.abs(Math.floor(Date.now() / 1e3) - t) > toleranceSec) return false;
|
|
3199
|
+
const subtle = await getSubtle();
|
|
3200
|
+
const enc = new TextEncoder();
|
|
3201
|
+
const key = await subtle.importKey(
|
|
3202
|
+
"raw",
|
|
3203
|
+
enc.encode(secret),
|
|
3204
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
3205
|
+
false,
|
|
3206
|
+
["sign"]
|
|
3207
|
+
);
|
|
3208
|
+
const bodyBytes = typeof body === "string" ? enc.encode(body) : body;
|
|
3209
|
+
const prefix = enc.encode(`${t}.`);
|
|
3210
|
+
const message = new Uint8Array(prefix.length + bodyBytes.length);
|
|
3211
|
+
message.set(prefix, 0);
|
|
3212
|
+
message.set(bodyBytes, prefix.length);
|
|
3213
|
+
const mac = new Uint8Array(await subtle.sign("HMAC", key, message));
|
|
3214
|
+
let expected = "";
|
|
3215
|
+
for (const b of mac) expected += b.toString(16).padStart(2, "0");
|
|
3216
|
+
return timingSafeEqualHex(expected, v1.toLowerCase());
|
|
3217
|
+
}
|
|
3218
|
+
function timingSafeEqualHex(a, b) {
|
|
3219
|
+
if (a.length !== b.length) return false;
|
|
3220
|
+
let diff = 0;
|
|
3221
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
3222
|
+
return diff === 0;
|
|
3223
|
+
}
|
|
3224
|
+
async function getSubtle() {
|
|
3225
|
+
const g = globalThis;
|
|
3226
|
+
if (g.crypto?.subtle) return g.crypto.subtle;
|
|
3227
|
+
const nodeCrypto = await import("crypto");
|
|
3228
|
+
return nodeCrypto.webcrypto.subtle;
|
|
3229
|
+
}
|
|
3026
3230
|
export {
|
|
3027
3231
|
AirbnbNamespace,
|
|
3028
3232
|
AmazonNamespace,
|
|
@@ -3034,6 +3238,7 @@ export {
|
|
|
3034
3238
|
CostcoNamespace,
|
|
3035
3239
|
DEFAULT_MAX_REQUESTS_PER_SECOND,
|
|
3036
3240
|
EbayNamespace,
|
|
3241
|
+
ExtractNamespace,
|
|
3037
3242
|
G2Namespace,
|
|
3038
3243
|
GlassdoorNamespace,
|
|
3039
3244
|
GoogleAdsNamespace,
|
|
@@ -3061,6 +3266,7 @@ export {
|
|
|
3061
3266
|
ScavioAPIError,
|
|
3062
3267
|
ScavioConnectionError,
|
|
3063
3268
|
ScavioError,
|
|
3269
|
+
ScavioJobTimeoutError,
|
|
3064
3270
|
ScavioTimeoutError,
|
|
3065
3271
|
TargetNamespace,
|
|
3066
3272
|
ThreadsNamespace,
|
|
@@ -3072,6 +3278,7 @@ export {
|
|
|
3072
3278
|
XNamespace,
|
|
3073
3279
|
YelpNamespace,
|
|
3074
3280
|
YouTubeNamespace,
|
|
3075
|
-
ZillowNamespace
|
|
3281
|
+
ZillowNamespace,
|
|
3282
|
+
verifyWebhook
|
|
3076
3283
|
};
|
|
3077
3284
|
//# sourceMappingURL=index.js.map
|