scavio 0.21.1 → 0.23.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/README.md +181 -4
- package/dist/index.cjs +398 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +601 -28
- package/dist/index.d.ts +601 -28
- package/dist/index.js +384 -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.0" : "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
|
);
|
|
@@ -419,6 +444,21 @@ var RedditNamespace = class {
|
|
|
419
444
|
async trending() {
|
|
420
445
|
return this.client._post("/api/v1/reddit/trending", {});
|
|
421
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* Search Reddit comments by keyword. Each result carries the comment text,
|
|
449
|
+
* author, score and date, plus the post and subreddit it was left on.
|
|
450
|
+
* Costs 1 credit.
|
|
451
|
+
*/
|
|
452
|
+
async searchComments(options) {
|
|
453
|
+
return this.client._post("/api/v1/reddit/search/comments", options);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Find subreddits by keyword, with subscriber counts, descriptions and NSFW
|
|
457
|
+
* flags. Costs 1 credit.
|
|
458
|
+
*/
|
|
459
|
+
async searchSubreddits(options) {
|
|
460
|
+
return this.client._post("/api/v1/reddit/search/subreddits", options);
|
|
461
|
+
}
|
|
422
462
|
};
|
|
423
463
|
|
|
424
464
|
// src/namespaces/tiktok.ts
|
|
@@ -436,6 +476,14 @@ var TikTokNamespace = class {
|
|
|
436
476
|
async video(options) {
|
|
437
477
|
return this.client._post("/api/v1/tiktok/video", options);
|
|
438
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
|
+
}
|
|
439
487
|
async videoComments(options) {
|
|
440
488
|
return this.client._post("/api/v1/tiktok/video/comments", options);
|
|
441
489
|
}
|
|
@@ -460,6 +508,107 @@ var TikTokNamespace = class {
|
|
|
460
508
|
async userFollowings(options) {
|
|
461
509
|
return this.client._post("/api/v1/tiktok/user/followings", options);
|
|
462
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Search terms trending on TikTok now (about 90 per call, US-weighted, no
|
|
513
|
+
* region filter). Costs 1 credit.
|
|
514
|
+
*/
|
|
515
|
+
async trendingSearches() {
|
|
516
|
+
return this.client._post("/api/v1/tiktok/trending/searches", {});
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* A sample of videos trending on TikTok now, with author, stats, sound and
|
|
520
|
+
* hashtags. Each call returns about 6 videos whatever `count` is set to;
|
|
521
|
+
* call again for a fresh sample. No region filter. Costs 1 credit.
|
|
522
|
+
* Returns TikTok's raw payload: large numeric ids can lose precision as JSON
|
|
523
|
+
* numbers, so use the string forms (`id_str`, `sec_uid`).
|
|
524
|
+
*/
|
|
525
|
+
async trendingVideos(options = {}) {
|
|
526
|
+
return this.client._post("/api/v1/tiktok/trending/videos", options);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* TikTok search autocomplete suggestions for a keyword. Long-tail keywords
|
|
530
|
+
* can return none, which is still a billed answer. Costs 1 credit.
|
|
531
|
+
*/
|
|
532
|
+
async searchSuggestions(options) {
|
|
533
|
+
return this.client._post("/api/v1/tiktok/search/suggestions", options);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* A TikTok sound by id: title, artist, album, duration, how many videos use
|
|
537
|
+
* it, original vs commercial, play URL, cover, and chart rank when charting.
|
|
538
|
+
* Costs 1 credit.
|
|
539
|
+
* Returns TikTok's raw payload: large numeric ids can lose precision as JSON
|
|
540
|
+
* numbers, so use the string forms (`id_str`, `sec_uid`).
|
|
541
|
+
*/
|
|
542
|
+
async music(options) {
|
|
543
|
+
return this.client._post("/api/v1/tiktok/music", options);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Videos that use a TikTok sound, about 14 per page (`count` is ignored
|
|
547
|
+
* upstream). Costs 1 credit.
|
|
548
|
+
* Returns TikTok's raw payload: large numeric ids can lose precision as JSON
|
|
549
|
+
* numbers, so use the string forms (`id_str`, `sec_uid`).
|
|
550
|
+
*/
|
|
551
|
+
async musicVideos(options) {
|
|
552
|
+
return this.client._post("/api/v1/tiktok/music/videos", options);
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Search TikTok sounds by keyword: id, title, artist, duration, usage count
|
|
556
|
+
* and play URL. Costs 1 credit.
|
|
557
|
+
* Returns TikTok's raw payload: large numeric ids can lose precision as JSON
|
|
558
|
+
* numbers, so use the string forms (`id_str`, `sec_uid`).
|
|
559
|
+
*/
|
|
560
|
+
async searchMusic(options) {
|
|
561
|
+
return this.client._post("/api/v1/tiktok/search/music", options);
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* TikTok's Top 50 or Viral 50 sound chart, with each sound's details and
|
|
565
|
+
* trend. One global chart, no region filter. Costs 1 credit.
|
|
566
|
+
* Returns TikTok's raw payload: large numeric ids can lose precision as JSON
|
|
567
|
+
* numbers, so use the string forms (`id_str`, `sec_uid`).
|
|
568
|
+
*/
|
|
569
|
+
async musicChart(options = {}) {
|
|
570
|
+
return this.client._post("/api/v1/tiktok/music/chart", options);
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Creators similar to a TikTok user (about 30 per page), with follower,
|
|
574
|
+
* following, video and like counts, bio and verification. Costs 1 credit.
|
|
575
|
+
* Returns TikTok's raw payload: large numeric ids can lose precision as JSON
|
|
576
|
+
* numbers, so use the string forms (`id_str`, `sec_uid`).
|
|
577
|
+
*/
|
|
578
|
+
async userSimilar(options) {
|
|
579
|
+
return this.client._post("/api/v1/tiktok/user/similar", options);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* A TikTok user's playlists (series): id, name, video count and cover.
|
|
583
|
+
* Accounts without playlists return an empty list. Costs 1 credit.
|
|
584
|
+
*/
|
|
585
|
+
async userPlaylists(options) {
|
|
586
|
+
return this.client._post("/api/v1/tiktok/user/playlists", options);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Videos in a TikTok playlist, with pagination. Costs 1 credit.
|
|
590
|
+
* Returns TikTok's raw payload: large numeric ids can lose precision as JSON
|
|
591
|
+
* numbers, so use the string forms (`id_str`, `sec_uid`).
|
|
592
|
+
*/
|
|
593
|
+
async playlistVideos(options) {
|
|
594
|
+
return this.client._post("/api/v1/tiktok/playlist/videos", options);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Whether a TikTok user is live now: the user, live room status (2 = live,
|
|
598
|
+
* 4 = not live), title, start time and viewer counts. An unknown username is
|
|
599
|
+
* a billed 404. Costs 1 credit.
|
|
600
|
+
*/
|
|
601
|
+
async userLive(options) {
|
|
602
|
+
return this.client._post("/api/v1/tiktok/user/live", options);
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* The region a TikTok account is registered in (ISO country code, not the
|
|
606
|
+
* creator's nationality), plus its user id and sec_user_id. An unknown
|
|
607
|
+
* username is a 404 and is not billed. Costs 1 credit.
|
|
608
|
+
*/
|
|
609
|
+
async userRegion(options) {
|
|
610
|
+
return this.client._post("/api/v1/tiktok/user/region", options);
|
|
611
|
+
}
|
|
463
612
|
};
|
|
464
613
|
|
|
465
614
|
// src/namespaces/tiktok-shop.ts
|
|
@@ -752,6 +901,7 @@ var YouTubeNamespace = class {
|
|
|
752
901
|
async commentReplies(options) {
|
|
753
902
|
return this.client._post("/api/v1/youtube/comments/replies", options);
|
|
754
903
|
}
|
|
904
|
+
/** Transcript / captions for a YouTube video. Costs 5 credits. */
|
|
755
905
|
async transcript(options) {
|
|
756
906
|
return this.client._post("/api/v1/youtube/transcript", options);
|
|
757
907
|
}
|
|
@@ -783,6 +933,21 @@ var YouTubeNamespace = class {
|
|
|
783
933
|
async streams(options) {
|
|
784
934
|
return this.client._post("/api/v1/youtube/streams", options);
|
|
785
935
|
}
|
|
936
|
+
/**
|
|
937
|
+
* A single YouTube community post: text, author, likes, comment count,
|
|
938
|
+
* attached images, and a comments_cursor for communityPostComments.
|
|
939
|
+
* Costs 1 credit.
|
|
940
|
+
*/
|
|
941
|
+
async communityPost(options) {
|
|
942
|
+
return this.client._post("/api/v1/youtube/community/post", options);
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Comments on a YouTube community post, with likes, reply counts and
|
|
946
|
+
* pagination. Costs 1 credit.
|
|
947
|
+
*/
|
|
948
|
+
async communityPostComments(options) {
|
|
949
|
+
return this.client._post("/api/v1/youtube/community/post/comments", options);
|
|
950
|
+
}
|
|
786
951
|
};
|
|
787
952
|
|
|
788
953
|
// src/namespaces/x.ts
|
|
@@ -824,6 +989,27 @@ var XNamespace = class {
|
|
|
824
989
|
async trending(options = {}) {
|
|
825
990
|
return this.client._post("/api/v1/x/trending", options);
|
|
826
991
|
}
|
|
992
|
+
/**
|
|
993
|
+
* Find X Communities by keyword, with member counts, topic and NSFW flag.
|
|
994
|
+
* Costs 1 credit.
|
|
995
|
+
*/
|
|
996
|
+
async searchCommunities(options) {
|
|
997
|
+
return this.client._post("/api/v1/x/search/communities", options);
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* An X Community: name, description, creation date, member count, topic and
|
|
1001
|
+
* rules. Costs 1 credit.
|
|
1002
|
+
*/
|
|
1003
|
+
async community(options) {
|
|
1004
|
+
return this.client._post("/api/v1/x/community", options);
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Posts in an X Community, by relevance or recency, with pagination.
|
|
1008
|
+
* Costs 1 credit.
|
|
1009
|
+
*/
|
|
1010
|
+
async communityTweets(options) {
|
|
1011
|
+
return this.client._post("/api/v1/x/community/tweets", options);
|
|
1012
|
+
}
|
|
827
1013
|
};
|
|
828
1014
|
|
|
829
1015
|
// src/namespaces/linkedin.ts
|
|
@@ -2718,10 +2904,128 @@ var MercadoLibreNamespace = class {
|
|
|
2718
2904
|
}
|
|
2719
2905
|
};
|
|
2720
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
|
+
var ExtractNamespace = class {
|
|
2916
|
+
constructor(client) {
|
|
2917
|
+
this.client = client;
|
|
2918
|
+
}
|
|
2919
|
+
client;
|
|
2920
|
+
/**
|
|
2921
|
+
* List a site's page URLs, gathered from its sitemaps and its start page,
|
|
2922
|
+
* deduplicated, filtered by path and limited. Pages disallowed by the
|
|
2923
|
+
* site's robots.txt are left out. 1 credit on the normal tier.
|
|
2924
|
+
*/
|
|
2925
|
+
async map(options) {
|
|
2926
|
+
return await this.client._post("/api/v1/extract/map", options);
|
|
2927
|
+
}
|
|
2928
|
+
async crawl(options, request2 = {}) {
|
|
2929
|
+
return this.start("crawl", options, request2);
|
|
2930
|
+
}
|
|
2931
|
+
/** A crawl's progress and its page results, paged by cursor. Free. */
|
|
2932
|
+
async crawlStatus(id, query) {
|
|
2933
|
+
return this.status("crawl", id, query);
|
|
2934
|
+
}
|
|
2935
|
+
/**
|
|
2936
|
+
* Stop a queued or running crawl. No further pages are fetched or billed;
|
|
2937
|
+
* results so far stay available. A finished crawl answers 409. Free.
|
|
2938
|
+
*/
|
|
2939
|
+
async cancelCrawl(id) {
|
|
2940
|
+
return this.cancel("crawl", id);
|
|
2941
|
+
}
|
|
2942
|
+
async batch(options, request2 = {}) {
|
|
2943
|
+
return this.start("batch", options, request2);
|
|
2944
|
+
}
|
|
2945
|
+
/** A batch's progress and its page results in input order, paged by cursor. Free. */
|
|
2946
|
+
async batchStatus(id, query) {
|
|
2947
|
+
return this.status("batch", id, query);
|
|
2948
|
+
}
|
|
2949
|
+
/** Stop a queued or running batch. A finished batch answers 409. Free. */
|
|
2950
|
+
async cancelBatch(id) {
|
|
2951
|
+
return this.cancel("batch", id);
|
|
2952
|
+
}
|
|
2953
|
+
async start(type, body, request2) {
|
|
2954
|
+
const headers = request2.idempotencyKey ? { "Idempotency-Key": request2.idempotencyKey } : void 0;
|
|
2955
|
+
const created = await this.client._post(
|
|
2956
|
+
`/api/v1/extract/${type}`,
|
|
2957
|
+
body,
|
|
2958
|
+
headers
|
|
2959
|
+
);
|
|
2960
|
+
if (!request2.wait) return created;
|
|
2961
|
+
return this.waitFor(type, created.data.id, created.data.status_url, request2);
|
|
2962
|
+
}
|
|
2963
|
+
async status(type, id, query = {}) {
|
|
2964
|
+
return await this.client._get(`/api/v1/extract/${type}/${encodeURIComponent(id)}`, {
|
|
2965
|
+
cursor: query.cursor,
|
|
2966
|
+
limit: query.limit,
|
|
2967
|
+
status: query.status,
|
|
2968
|
+
include_content: query.include_content
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
async cancel(type, id) {
|
|
2972
|
+
return await this.client._delete(
|
|
2973
|
+
`/api/v1/extract/${type}/${encodeURIComponent(id)}`
|
|
2974
|
+
);
|
|
2975
|
+
}
|
|
2976
|
+
async waitFor(type, id, statusUrl, request2) {
|
|
2977
|
+
const interval = request2.pollInterval ?? DEFAULT_POLL_INTERVAL;
|
|
2978
|
+
const deadline = Date.now() + (request2.waitTimeout ?? DEFAULT_WAIT_TIMEOUT);
|
|
2979
|
+
for (; ; ) {
|
|
2980
|
+
const probe = await this.status(type, id, { limit: 1, include_content: false });
|
|
2981
|
+
if (TERMINAL.has(probe.data.status)) break;
|
|
2982
|
+
if (Date.now() + interval > deadline) {
|
|
2983
|
+
throw new ScavioJobTimeoutError(type, id, statusUrl, probe.data.status);
|
|
2984
|
+
}
|
|
2985
|
+
await sleep2(interval);
|
|
2986
|
+
}
|
|
2987
|
+
const results = [];
|
|
2988
|
+
let cursor;
|
|
2989
|
+
let last;
|
|
2990
|
+
do {
|
|
2991
|
+
last = await this.status(type, id, { cursor, limit: COLLECT_PAGE_SIZE });
|
|
2992
|
+
results.push(...last.data.results);
|
|
2993
|
+
cursor = last.data.next_cursor ?? void 0;
|
|
2994
|
+
} while (cursor);
|
|
2995
|
+
return { data: { ...last.data, results, next_cursor: null } };
|
|
2996
|
+
}
|
|
2997
|
+
};
|
|
2998
|
+
|
|
2721
2999
|
// src/client.ts
|
|
2722
3000
|
var DEFAULT_MAX_REQUESTS_PER_SECOND = 1;
|
|
2723
3001
|
var MAX_REQUESTS_PER_SECOND = 50;
|
|
2724
|
-
var
|
|
3002
|
+
var ScavioCore = class {
|
|
3003
|
+
/**
|
|
3004
|
+
* Read ANY web page and get it back as readability Markdown (the default),
|
|
3005
|
+
* plain text, or raw HTML. Returns `{ url, format, mode, content,
|
|
3006
|
+
* content_length }`.
|
|
3007
|
+
*
|
|
3008
|
+
* Extract is a core endpoint, not a platform: `scavio.extract({ url })`
|
|
3009
|
+
* reads one page, and `scavio.extract.map()` / `.crawl()` / `.batch()` work
|
|
3010
|
+
* on whole sites and URL lists.
|
|
3011
|
+
*
|
|
3012
|
+
* Credits are a function of `mode`, not a flat per-call constant:
|
|
3013
|
+
* "normal" costs 1, "advanced" costs 1, "ultra" costs 2. Billing happens
|
|
3014
|
+
* only on a successful extraction - a dead link, bot wall or timeout costs
|
|
3015
|
+
* nothing.
|
|
3016
|
+
*
|
|
3017
|
+
* Start on "normal". Move to "advanced" when the page builds its content in
|
|
3018
|
+
* the browser, and to "ultra" only when a bot wall blocks the other two.
|
|
3019
|
+
*
|
|
3020
|
+
* @example
|
|
3021
|
+
* const page = await scavio.extract({ url: "https://example.com/pricing" });
|
|
3022
|
+
* console.log(page.content);
|
|
3023
|
+
*/
|
|
3024
|
+
async extract(options) {
|
|
3025
|
+
return this._post("/api/v1/extract", options);
|
|
3026
|
+
}
|
|
3027
|
+
};
|
|
3028
|
+
var Scavio = class extends ScavioCore {
|
|
2725
3029
|
google;
|
|
2726
3030
|
amazon;
|
|
2727
3031
|
walmart;
|
|
@@ -2764,6 +3068,7 @@ var Scavio = class {
|
|
|
2764
3068
|
maxRetries;
|
|
2765
3069
|
rateLimiter;
|
|
2766
3070
|
constructor(config) {
|
|
3071
|
+
super();
|
|
2767
3072
|
this.apiKey = config?.apiKey ?? process.env.SCAVIO_API_KEY ?? "";
|
|
2768
3073
|
if (!this.apiKey) {
|
|
2769
3074
|
throw new MissingAPIKeyError();
|
|
@@ -2814,9 +3119,19 @@ var Scavio = class {
|
|
|
2814
3119
|
this.realtor = new RealtorNamespace(this);
|
|
2815
3120
|
this.rightmove = new RightmoveNamespace(this);
|
|
2816
3121
|
this.mercadolibre = new MercadoLibreNamespace(this);
|
|
3122
|
+
const extractNs = new ExtractNamespace(this);
|
|
3123
|
+
this.extract = Object.assign(ScavioCore.prototype.extract.bind(this), {
|
|
3124
|
+
map: extractNs.map.bind(extractNs),
|
|
3125
|
+
crawl: extractNs.crawl.bind(extractNs),
|
|
3126
|
+
crawlStatus: extractNs.crawlStatus.bind(extractNs),
|
|
3127
|
+
cancelCrawl: extractNs.cancelCrawl.bind(extractNs),
|
|
3128
|
+
batch: extractNs.batch.bind(extractNs),
|
|
3129
|
+
batchStatus: extractNs.batchStatus.bind(extractNs),
|
|
3130
|
+
cancelBatch: extractNs.cancelBatch.bind(extractNs)
|
|
3131
|
+
});
|
|
2817
3132
|
}
|
|
2818
3133
|
/** @internal */
|
|
2819
|
-
async _post(path, body) {
|
|
3134
|
+
async _post(path, body, headers) {
|
|
2820
3135
|
return request({
|
|
2821
3136
|
method: "POST",
|
|
2822
3137
|
path,
|
|
@@ -2825,11 +3140,12 @@ var Scavio = class {
|
|
|
2825
3140
|
timeout: this.timeout,
|
|
2826
3141
|
maxRetries: this.maxRetries,
|
|
2827
3142
|
rateLimiter: this.rateLimiter,
|
|
2828
|
-
body
|
|
3143
|
+
body,
|
|
3144
|
+
headers
|
|
2829
3145
|
});
|
|
2830
3146
|
}
|
|
2831
3147
|
/** @internal */
|
|
2832
|
-
async _get(path) {
|
|
3148
|
+
async _get(path, query) {
|
|
2833
3149
|
return request({
|
|
2834
3150
|
method: "GET",
|
|
2835
3151
|
path,
|
|
@@ -2837,39 +3153,73 @@ var Scavio = class {
|
|
|
2837
3153
|
baseUrl: this.baseUrl,
|
|
2838
3154
|
timeout: this.timeout,
|
|
2839
3155
|
maxRetries: this.maxRetries,
|
|
3156
|
+
rateLimiter: this.rateLimiter,
|
|
3157
|
+
query
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
/** @internal */
|
|
3161
|
+
async _delete(path) {
|
|
3162
|
+
return request({
|
|
3163
|
+
method: "DELETE",
|
|
3164
|
+
path,
|
|
3165
|
+
apiKey: this.apiKey,
|
|
3166
|
+
baseUrl: this.baseUrl,
|
|
3167
|
+
timeout: this.timeout,
|
|
3168
|
+
maxRetries: this.maxRetries,
|
|
2840
3169
|
rateLimiter: this.rateLimiter
|
|
2841
3170
|
});
|
|
2842
3171
|
}
|
|
2843
3172
|
async search(options) {
|
|
2844
3173
|
return this.google.search(options);
|
|
2845
3174
|
}
|
|
2846
|
-
/**
|
|
2847
|
-
* Read ANY web page and get it back as readability Markdown (the default),
|
|
2848
|
-
* plain text, or raw HTML. Returns `{ url, format, mode, content,
|
|
2849
|
-
* content_length }`.
|
|
2850
|
-
*
|
|
2851
|
-
* This is a core endpoint, not a platform, so it lives on the client itself:
|
|
2852
|
-
* `scavio.extract({ url })`, never `scavio.extract.extract()`.
|
|
2853
|
-
*
|
|
2854
|
-
* Credits are a function of `mode`, not a flat per-call constant:
|
|
2855
|
-
* "normal" costs 1, "advanced" costs 1, "ultra" costs 2. Billing happens
|
|
2856
|
-
* only on a successful extraction - a dead link, bot wall or timeout costs
|
|
2857
|
-
* nothing.
|
|
2858
|
-
*
|
|
2859
|
-
* Start on "normal". Move to "advanced" when the page builds its content in
|
|
2860
|
-
* the browser, and to "ultra" only when a bot wall blocks the other two.
|
|
2861
|
-
*
|
|
2862
|
-
* @example
|
|
2863
|
-
* const page = await scavio.extract({ url: "https://example.com/pricing" });
|
|
2864
|
-
* console.log(page.content);
|
|
2865
|
-
*/
|
|
2866
|
-
async extract(options) {
|
|
2867
|
-
return this._post("/api/v1/extract", options);
|
|
2868
|
-
}
|
|
2869
3175
|
async getUsage() {
|
|
2870
3176
|
return this._get("/api/v1/usage");
|
|
2871
3177
|
}
|
|
2872
3178
|
};
|
|
3179
|
+
|
|
3180
|
+
// src/webhook.ts
|
|
3181
|
+
async function verifyWebhook(secret, header, body, toleranceSec = 300) {
|
|
3182
|
+
if (!secret || !header) return false;
|
|
3183
|
+
const parts = {};
|
|
3184
|
+
for (const piece of header.split(",")) {
|
|
3185
|
+
const i = piece.indexOf("=");
|
|
3186
|
+
if (i > 0) parts[piece.slice(0, i).trim()] = piece.slice(i + 1).trim();
|
|
3187
|
+
}
|
|
3188
|
+
const t = Number(parts.t);
|
|
3189
|
+
const v1 = parts.v1;
|
|
3190
|
+
if (!parts.t || !Number.isFinite(t) || !v1 || !/^[0-9a-f]+$/i.test(v1)) return false;
|
|
3191
|
+
if (Math.abs(Math.floor(Date.now() / 1e3) - t) > toleranceSec) return false;
|
|
3192
|
+
const subtle = await getSubtle();
|
|
3193
|
+
const enc = new TextEncoder();
|
|
3194
|
+
const key = await subtle.importKey(
|
|
3195
|
+
"raw",
|
|
3196
|
+
enc.encode(secret),
|
|
3197
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
3198
|
+
false,
|
|
3199
|
+
["sign"]
|
|
3200
|
+
);
|
|
3201
|
+
const bodyBytes = typeof body === "string" ? enc.encode(body) : body;
|
|
3202
|
+
const prefix = enc.encode(`${t}.`);
|
|
3203
|
+
const message = new Uint8Array(prefix.length + bodyBytes.length);
|
|
3204
|
+
message.set(prefix, 0);
|
|
3205
|
+
message.set(bodyBytes, prefix.length);
|
|
3206
|
+
const mac = new Uint8Array(await subtle.sign("HMAC", key, message));
|
|
3207
|
+
let expected = "";
|
|
3208
|
+
for (const b of mac) expected += b.toString(16).padStart(2, "0");
|
|
3209
|
+
return timingSafeEqualHex(expected, v1.toLowerCase());
|
|
3210
|
+
}
|
|
3211
|
+
function timingSafeEqualHex(a, b) {
|
|
3212
|
+
if (a.length !== b.length) return false;
|
|
3213
|
+
let diff = 0;
|
|
3214
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
3215
|
+
return diff === 0;
|
|
3216
|
+
}
|
|
3217
|
+
async function getSubtle() {
|
|
3218
|
+
const g = globalThis;
|
|
3219
|
+
if (g.crypto?.subtle) return g.crypto.subtle;
|
|
3220
|
+
const nodeCrypto = await import("crypto");
|
|
3221
|
+
return nodeCrypto.webcrypto.subtle;
|
|
3222
|
+
}
|
|
2873
3223
|
export {
|
|
2874
3224
|
AirbnbNamespace,
|
|
2875
3225
|
AmazonNamespace,
|
|
@@ -2881,6 +3231,7 @@ export {
|
|
|
2881
3231
|
CostcoNamespace,
|
|
2882
3232
|
DEFAULT_MAX_REQUESTS_PER_SECOND,
|
|
2883
3233
|
EbayNamespace,
|
|
3234
|
+
ExtractNamespace,
|
|
2884
3235
|
G2Namespace,
|
|
2885
3236
|
GlassdoorNamespace,
|
|
2886
3237
|
GoogleAdsNamespace,
|
|
@@ -2908,6 +3259,7 @@ export {
|
|
|
2908
3259
|
ScavioAPIError,
|
|
2909
3260
|
ScavioConnectionError,
|
|
2910
3261
|
ScavioError,
|
|
3262
|
+
ScavioJobTimeoutError,
|
|
2911
3263
|
ScavioTimeoutError,
|
|
2912
3264
|
TargetNamespace,
|
|
2913
3265
|
ThreadsNamespace,
|
|
@@ -2919,6 +3271,7 @@ export {
|
|
|
2919
3271
|
XNamespace,
|
|
2920
3272
|
YelpNamespace,
|
|
2921
3273
|
YouTubeNamespace,
|
|
2922
|
-
ZillowNamespace
|
|
3274
|
+
ZillowNamespace,
|
|
3275
|
+
verifyWebhook
|
|
2923
3276
|
};
|
|
2924
3277
|
//# sourceMappingURL=index.js.map
|