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/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -30,6 +40,7 @@ __export(index_exports, {
30
40
  CostcoNamespace: () => CostcoNamespace,
31
41
  DEFAULT_MAX_REQUESTS_PER_SECOND: () => DEFAULT_MAX_REQUESTS_PER_SECOND,
32
42
  EbayNamespace: () => EbayNamespace,
43
+ ExtractNamespace: () => ExtractNamespace,
33
44
  G2Namespace: () => G2Namespace,
34
45
  GlassdoorNamespace: () => GlassdoorNamespace,
35
46
  GoogleAdsNamespace: () => GoogleAdsNamespace,
@@ -57,6 +68,7 @@ __export(index_exports, {
57
68
  ScavioAPIError: () => ScavioAPIError,
58
69
  ScavioConnectionError: () => ScavioConnectionError,
59
70
  ScavioError: () => ScavioError,
71
+ ScavioJobTimeoutError: () => ScavioJobTimeoutError,
60
72
  ScavioTimeoutError: () => ScavioTimeoutError,
61
73
  TargetNamespace: () => TargetNamespace,
62
74
  ThreadsNamespace: () => ThreadsNamespace,
@@ -68,7 +80,8 @@ __export(index_exports, {
68
80
  XNamespace: () => XNamespace,
69
81
  YelpNamespace: () => YelpNamespace,
70
82
  YouTubeNamespace: () => YouTubeNamespace,
71
- ZillowNamespace: () => ZillowNamespace
83
+ ZillowNamespace: () => ZillowNamespace,
84
+ verifyWebhook: () => verifyWebhook
72
85
  });
73
86
  module.exports = __toCommonJS(index_exports);
74
87
 
@@ -155,6 +168,22 @@ var ScavioAPIError = class extends ScavioError {
155
168
  this.responseBody = responseBody;
156
169
  }
157
170
  };
171
+ var ScavioJobTimeoutError = class extends ScavioError {
172
+ constructor(jobType, jobId, statusUrl, lastStatus) {
173
+ super(
174
+ `${jobType} ${jobId} was still ${lastStatus} when the wait timed out. It keeps running: poll extract.${jobType}Status("${jobId}") or cancel it.`
175
+ );
176
+ this.jobType = jobType;
177
+ this.jobId = jobId;
178
+ this.statusUrl = statusUrl;
179
+ this.lastStatus = lastStatus;
180
+ this.name = "ScavioJobTimeoutError";
181
+ }
182
+ jobType;
183
+ jobId;
184
+ statusUrl;
185
+ lastStatus;
186
+ };
158
187
 
159
188
  // src/retry.ts
160
189
  var DEFAULT_RETRY_STATUSES = /* @__PURE__ */ new Set([
@@ -203,7 +232,7 @@ function parseRetryAfter(header) {
203
232
  var BASE_URL = "https://api.scavio.dev";
204
233
  var DEFAULT_TIMEOUT = 3e4;
205
234
  var DEFAULT_MAX_RETRIES = 2;
206
- var SDK_VERSION = true ? "0.21.1" : "0.0.0";
235
+ var SDK_VERSION = true ? "0.23.0" : "0.0.0";
207
236
  var USER_AGENT = `scavio-js/${SDK_VERSION}`;
208
237
  function isServerRuntime() {
209
238
  const g = globalThis;
@@ -261,9 +290,18 @@ function getHeader(response, name) {
261
290
  }
262
291
  return null;
263
292
  }
293
+ function buildQuery(query) {
294
+ if (!query) return "";
295
+ const params = new URLSearchParams();
296
+ for (const [key, value] of Object.entries(query)) {
297
+ if (value !== void 0) params.append(key, String(value));
298
+ }
299
+ const qs = params.toString();
300
+ return qs ? `?${qs}` : "";
301
+ }
264
302
  async function request(options) {
265
- const url = `${options.baseUrl}${options.path}`;
266
- const headers = buildHeaders(options.apiKey);
303
+ const url = `${options.baseUrl}${options.path}${buildQuery(options.query)}`;
304
+ const headers = { ...buildHeaders(options.apiKey), ...options.headers };
267
305
  const retry = makeRetryConfig(
268
306
  options.maxRetries ?? DEFAULT_MAX_RETRIES
269
307
  );
@@ -493,6 +531,21 @@ var RedditNamespace = class {
493
531
  async trending() {
494
532
  return this.client._post("/api/v1/reddit/trending", {});
495
533
  }
534
+ /**
535
+ * Search Reddit comments by keyword. Each result carries the comment text,
536
+ * author, score and date, plus the post and subreddit it was left on.
537
+ * Costs 1 credit.
538
+ */
539
+ async searchComments(options) {
540
+ return this.client._post("/api/v1/reddit/search/comments", options);
541
+ }
542
+ /**
543
+ * Find subreddits by keyword, with subscriber counts, descriptions and NSFW
544
+ * flags. Costs 1 credit.
545
+ */
546
+ async searchSubreddits(options) {
547
+ return this.client._post("/api/v1/reddit/search/subreddits", options);
548
+ }
496
549
  };
497
550
 
498
551
  // src/namespaces/tiktok.ts
@@ -510,6 +563,14 @@ var TikTokNamespace = class {
510
563
  async video(options) {
511
564
  return this.client._post("/api/v1/tiktok/video", options);
512
565
  }
566
+ /**
567
+ * Spoken-word transcript of a TikTok video, as timestamped segments and
568
+ * full text. Pass `video_id` or `url`. Videos with no spoken captions
569
+ * (music, dance, muted clips) return `available: false`. 1 credit.
570
+ */
571
+ async videoTranscript(options) {
572
+ return this.client._post("/api/v1/tiktok/video/transcript", options);
573
+ }
513
574
  async videoComments(options) {
514
575
  return this.client._post("/api/v1/tiktok/video/comments", options);
515
576
  }
@@ -534,6 +595,107 @@ var TikTokNamespace = class {
534
595
  async userFollowings(options) {
535
596
  return this.client._post("/api/v1/tiktok/user/followings", options);
536
597
  }
598
+ /**
599
+ * Search terms trending on TikTok now (about 90 per call, US-weighted, no
600
+ * region filter). Costs 1 credit.
601
+ */
602
+ async trendingSearches() {
603
+ return this.client._post("/api/v1/tiktok/trending/searches", {});
604
+ }
605
+ /**
606
+ * A sample of videos trending on TikTok now, with author, stats, sound and
607
+ * hashtags. Each call returns about 6 videos whatever `count` is set to;
608
+ * call again for a fresh sample. No region filter. Costs 1 credit.
609
+ * Returns TikTok's raw payload: large numeric ids can lose precision as JSON
610
+ * numbers, so use the string forms (`id_str`, `sec_uid`).
611
+ */
612
+ async trendingVideos(options = {}) {
613
+ return this.client._post("/api/v1/tiktok/trending/videos", options);
614
+ }
615
+ /**
616
+ * TikTok search autocomplete suggestions for a keyword. Long-tail keywords
617
+ * can return none, which is still a billed answer. Costs 1 credit.
618
+ */
619
+ async searchSuggestions(options) {
620
+ return this.client._post("/api/v1/tiktok/search/suggestions", options);
621
+ }
622
+ /**
623
+ * A TikTok sound by id: title, artist, album, duration, how many videos use
624
+ * it, original vs commercial, play URL, cover, and chart rank when charting.
625
+ * Costs 1 credit.
626
+ * Returns TikTok's raw payload: large numeric ids can lose precision as JSON
627
+ * numbers, so use the string forms (`id_str`, `sec_uid`).
628
+ */
629
+ async music(options) {
630
+ return this.client._post("/api/v1/tiktok/music", options);
631
+ }
632
+ /**
633
+ * Videos that use a TikTok sound, about 14 per page (`count` is ignored
634
+ * upstream). Costs 1 credit.
635
+ * Returns TikTok's raw payload: large numeric ids can lose precision as JSON
636
+ * numbers, so use the string forms (`id_str`, `sec_uid`).
637
+ */
638
+ async musicVideos(options) {
639
+ return this.client._post("/api/v1/tiktok/music/videos", options);
640
+ }
641
+ /**
642
+ * Search TikTok sounds by keyword: id, title, artist, duration, usage count
643
+ * and play URL. Costs 1 credit.
644
+ * Returns TikTok's raw payload: large numeric ids can lose precision as JSON
645
+ * numbers, so use the string forms (`id_str`, `sec_uid`).
646
+ */
647
+ async searchMusic(options) {
648
+ return this.client._post("/api/v1/tiktok/search/music", options);
649
+ }
650
+ /**
651
+ * TikTok's Top 50 or Viral 50 sound chart, with each sound's details and
652
+ * trend. One global chart, no region filter. Costs 1 credit.
653
+ * Returns TikTok's raw payload: large numeric ids can lose precision as JSON
654
+ * numbers, so use the string forms (`id_str`, `sec_uid`).
655
+ */
656
+ async musicChart(options = {}) {
657
+ return this.client._post("/api/v1/tiktok/music/chart", options);
658
+ }
659
+ /**
660
+ * Creators similar to a TikTok user (about 30 per page), with follower,
661
+ * following, video and like counts, bio and verification. Costs 1 credit.
662
+ * Returns TikTok's raw payload: large numeric ids can lose precision as JSON
663
+ * numbers, so use the string forms (`id_str`, `sec_uid`).
664
+ */
665
+ async userSimilar(options) {
666
+ return this.client._post("/api/v1/tiktok/user/similar", options);
667
+ }
668
+ /**
669
+ * A TikTok user's playlists (series): id, name, video count and cover.
670
+ * Accounts without playlists return an empty list. Costs 1 credit.
671
+ */
672
+ async userPlaylists(options) {
673
+ return this.client._post("/api/v1/tiktok/user/playlists", options);
674
+ }
675
+ /**
676
+ * Videos in a TikTok playlist, with pagination. Costs 1 credit.
677
+ * Returns TikTok's raw payload: large numeric ids can lose precision as JSON
678
+ * numbers, so use the string forms (`id_str`, `sec_uid`).
679
+ */
680
+ async playlistVideos(options) {
681
+ return this.client._post("/api/v1/tiktok/playlist/videos", options);
682
+ }
683
+ /**
684
+ * Whether a TikTok user is live now: the user, live room status (2 = live,
685
+ * 4 = not live), title, start time and viewer counts. An unknown username is
686
+ * a billed 404. Costs 1 credit.
687
+ */
688
+ async userLive(options) {
689
+ return this.client._post("/api/v1/tiktok/user/live", options);
690
+ }
691
+ /**
692
+ * The region a TikTok account is registered in (ISO country code, not the
693
+ * creator's nationality), plus its user id and sec_user_id. An unknown
694
+ * username is a 404 and is not billed. Costs 1 credit.
695
+ */
696
+ async userRegion(options) {
697
+ return this.client._post("/api/v1/tiktok/user/region", options);
698
+ }
537
699
  };
538
700
 
539
701
  // src/namespaces/tiktok-shop.ts
@@ -826,6 +988,7 @@ var YouTubeNamespace = class {
826
988
  async commentReplies(options) {
827
989
  return this.client._post("/api/v1/youtube/comments/replies", options);
828
990
  }
991
+ /** Transcript / captions for a YouTube video. Costs 5 credits. */
829
992
  async transcript(options) {
830
993
  return this.client._post("/api/v1/youtube/transcript", options);
831
994
  }
@@ -857,6 +1020,21 @@ var YouTubeNamespace = class {
857
1020
  async streams(options) {
858
1021
  return this.client._post("/api/v1/youtube/streams", options);
859
1022
  }
1023
+ /**
1024
+ * A single YouTube community post: text, author, likes, comment count,
1025
+ * attached images, and a comments_cursor for communityPostComments.
1026
+ * Costs 1 credit.
1027
+ */
1028
+ async communityPost(options) {
1029
+ return this.client._post("/api/v1/youtube/community/post", options);
1030
+ }
1031
+ /**
1032
+ * Comments on a YouTube community post, with likes, reply counts and
1033
+ * pagination. Costs 1 credit.
1034
+ */
1035
+ async communityPostComments(options) {
1036
+ return this.client._post("/api/v1/youtube/community/post/comments", options);
1037
+ }
860
1038
  };
861
1039
 
862
1040
  // src/namespaces/x.ts
@@ -898,6 +1076,27 @@ var XNamespace = class {
898
1076
  async trending(options = {}) {
899
1077
  return this.client._post("/api/v1/x/trending", options);
900
1078
  }
1079
+ /**
1080
+ * Find X Communities by keyword, with member counts, topic and NSFW flag.
1081
+ * Costs 1 credit.
1082
+ */
1083
+ async searchCommunities(options) {
1084
+ return this.client._post("/api/v1/x/search/communities", options);
1085
+ }
1086
+ /**
1087
+ * An X Community: name, description, creation date, member count, topic and
1088
+ * rules. Costs 1 credit.
1089
+ */
1090
+ async community(options) {
1091
+ return this.client._post("/api/v1/x/community", options);
1092
+ }
1093
+ /**
1094
+ * Posts in an X Community, by relevance or recency, with pagination.
1095
+ * Costs 1 credit.
1096
+ */
1097
+ async communityTweets(options) {
1098
+ return this.client._post("/api/v1/x/community/tweets", options);
1099
+ }
901
1100
  };
902
1101
 
903
1102
  // src/namespaces/linkedin.ts
@@ -2792,10 +2991,128 @@ var MercadoLibreNamespace = class {
2792
2991
  }
2793
2992
  };
2794
2993
 
2994
+ // src/namespaces/extract.ts
2995
+ var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
2996
+ var DEFAULT_POLL_INTERVAL = 2e3;
2997
+ var DEFAULT_WAIT_TIMEOUT = 6e5;
2998
+ var COLLECT_PAGE_SIZE = 100;
2999
+ function sleep2(ms) {
3000
+ return new Promise((resolve) => setTimeout(resolve, ms));
3001
+ }
3002
+ var ExtractNamespace = class {
3003
+ constructor(client) {
3004
+ this.client = client;
3005
+ }
3006
+ client;
3007
+ /**
3008
+ * List a site's page URLs, gathered from its sitemaps and its start page,
3009
+ * deduplicated, filtered by path and limited. Pages disallowed by the
3010
+ * site's robots.txt are left out. 1 credit on the normal tier.
3011
+ */
3012
+ async map(options) {
3013
+ return await this.client._post("/api/v1/extract/map", options);
3014
+ }
3015
+ async crawl(options, request2 = {}) {
3016
+ return this.start("crawl", options, request2);
3017
+ }
3018
+ /** A crawl's progress and its page results, paged by cursor. Free. */
3019
+ async crawlStatus(id, query) {
3020
+ return this.status("crawl", id, query);
3021
+ }
3022
+ /**
3023
+ * Stop a queued or running crawl. No further pages are fetched or billed;
3024
+ * results so far stay available. A finished crawl answers 409. Free.
3025
+ */
3026
+ async cancelCrawl(id) {
3027
+ return this.cancel("crawl", id);
3028
+ }
3029
+ async batch(options, request2 = {}) {
3030
+ return this.start("batch", options, request2);
3031
+ }
3032
+ /** A batch's progress and its page results in input order, paged by cursor. Free. */
3033
+ async batchStatus(id, query) {
3034
+ return this.status("batch", id, query);
3035
+ }
3036
+ /** Stop a queued or running batch. A finished batch answers 409. Free. */
3037
+ async cancelBatch(id) {
3038
+ return this.cancel("batch", id);
3039
+ }
3040
+ async start(type, body, request2) {
3041
+ const headers = request2.idempotencyKey ? { "Idempotency-Key": request2.idempotencyKey } : void 0;
3042
+ const created = await this.client._post(
3043
+ `/api/v1/extract/${type}`,
3044
+ body,
3045
+ headers
3046
+ );
3047
+ if (!request2.wait) return created;
3048
+ return this.waitFor(type, created.data.id, created.data.status_url, request2);
3049
+ }
3050
+ async status(type, id, query = {}) {
3051
+ return await this.client._get(`/api/v1/extract/${type}/${encodeURIComponent(id)}`, {
3052
+ cursor: query.cursor,
3053
+ limit: query.limit,
3054
+ status: query.status,
3055
+ include_content: query.include_content
3056
+ });
3057
+ }
3058
+ async cancel(type, id) {
3059
+ return await this.client._delete(
3060
+ `/api/v1/extract/${type}/${encodeURIComponent(id)}`
3061
+ );
3062
+ }
3063
+ async waitFor(type, id, statusUrl, request2) {
3064
+ const interval = request2.pollInterval ?? DEFAULT_POLL_INTERVAL;
3065
+ const deadline = Date.now() + (request2.waitTimeout ?? DEFAULT_WAIT_TIMEOUT);
3066
+ for (; ; ) {
3067
+ const probe = await this.status(type, id, { limit: 1, include_content: false });
3068
+ if (TERMINAL.has(probe.data.status)) break;
3069
+ if (Date.now() + interval > deadline) {
3070
+ throw new ScavioJobTimeoutError(type, id, statusUrl, probe.data.status);
3071
+ }
3072
+ await sleep2(interval);
3073
+ }
3074
+ const results = [];
3075
+ let cursor;
3076
+ let last;
3077
+ do {
3078
+ last = await this.status(type, id, { cursor, limit: COLLECT_PAGE_SIZE });
3079
+ results.push(...last.data.results);
3080
+ cursor = last.data.next_cursor ?? void 0;
3081
+ } while (cursor);
3082
+ return { data: { ...last.data, results, next_cursor: null } };
3083
+ }
3084
+ };
3085
+
2795
3086
  // src/client.ts
2796
3087
  var DEFAULT_MAX_REQUESTS_PER_SECOND = 1;
2797
3088
  var MAX_REQUESTS_PER_SECOND = 50;
2798
- var Scavio = class {
3089
+ var ScavioCore = class {
3090
+ /**
3091
+ * Read ANY web page and get it back as readability Markdown (the default),
3092
+ * plain text, or raw HTML. Returns `{ url, format, mode, content,
3093
+ * content_length }`.
3094
+ *
3095
+ * Extract is a core endpoint, not a platform: `scavio.extract({ url })`
3096
+ * reads one page, and `scavio.extract.map()` / `.crawl()` / `.batch()` work
3097
+ * on whole sites and URL lists.
3098
+ *
3099
+ * Credits are a function of `mode`, not a flat per-call constant:
3100
+ * "normal" costs 1, "advanced" costs 1, "ultra" costs 2. Billing happens
3101
+ * only on a successful extraction - a dead link, bot wall or timeout costs
3102
+ * nothing.
3103
+ *
3104
+ * Start on "normal". Move to "advanced" when the page builds its content in
3105
+ * the browser, and to "ultra" only when a bot wall blocks the other two.
3106
+ *
3107
+ * @example
3108
+ * const page = await scavio.extract({ url: "https://example.com/pricing" });
3109
+ * console.log(page.content);
3110
+ */
3111
+ async extract(options) {
3112
+ return this._post("/api/v1/extract", options);
3113
+ }
3114
+ };
3115
+ var Scavio = class extends ScavioCore {
2799
3116
  google;
2800
3117
  amazon;
2801
3118
  walmart;
@@ -2838,6 +3155,7 @@ var Scavio = class {
2838
3155
  maxRetries;
2839
3156
  rateLimiter;
2840
3157
  constructor(config) {
3158
+ super();
2841
3159
  this.apiKey = config?.apiKey ?? process.env.SCAVIO_API_KEY ?? "";
2842
3160
  if (!this.apiKey) {
2843
3161
  throw new MissingAPIKeyError();
@@ -2888,9 +3206,19 @@ var Scavio = class {
2888
3206
  this.realtor = new RealtorNamespace(this);
2889
3207
  this.rightmove = new RightmoveNamespace(this);
2890
3208
  this.mercadolibre = new MercadoLibreNamespace(this);
3209
+ const extractNs = new ExtractNamespace(this);
3210
+ this.extract = Object.assign(ScavioCore.prototype.extract.bind(this), {
3211
+ map: extractNs.map.bind(extractNs),
3212
+ crawl: extractNs.crawl.bind(extractNs),
3213
+ crawlStatus: extractNs.crawlStatus.bind(extractNs),
3214
+ cancelCrawl: extractNs.cancelCrawl.bind(extractNs),
3215
+ batch: extractNs.batch.bind(extractNs),
3216
+ batchStatus: extractNs.batchStatus.bind(extractNs),
3217
+ cancelBatch: extractNs.cancelBatch.bind(extractNs)
3218
+ });
2891
3219
  }
2892
3220
  /** @internal */
2893
- async _post(path, body) {
3221
+ async _post(path, body, headers) {
2894
3222
  return request({
2895
3223
  method: "POST",
2896
3224
  path,
@@ -2899,11 +3227,12 @@ var Scavio = class {
2899
3227
  timeout: this.timeout,
2900
3228
  maxRetries: this.maxRetries,
2901
3229
  rateLimiter: this.rateLimiter,
2902
- body
3230
+ body,
3231
+ headers
2903
3232
  });
2904
3233
  }
2905
3234
  /** @internal */
2906
- async _get(path) {
3235
+ async _get(path, query) {
2907
3236
  return request({
2908
3237
  method: "GET",
2909
3238
  path,
@@ -2911,39 +3240,73 @@ var Scavio = class {
2911
3240
  baseUrl: this.baseUrl,
2912
3241
  timeout: this.timeout,
2913
3242
  maxRetries: this.maxRetries,
3243
+ rateLimiter: this.rateLimiter,
3244
+ query
3245
+ });
3246
+ }
3247
+ /** @internal */
3248
+ async _delete(path) {
3249
+ return request({
3250
+ method: "DELETE",
3251
+ path,
3252
+ apiKey: this.apiKey,
3253
+ baseUrl: this.baseUrl,
3254
+ timeout: this.timeout,
3255
+ maxRetries: this.maxRetries,
2914
3256
  rateLimiter: this.rateLimiter
2915
3257
  });
2916
3258
  }
2917
3259
  async search(options) {
2918
3260
  return this.google.search(options);
2919
3261
  }
2920
- /**
2921
- * Read ANY web page and get it back as readability Markdown (the default),
2922
- * plain text, or raw HTML. Returns `{ url, format, mode, content,
2923
- * content_length }`.
2924
- *
2925
- * This is a core endpoint, not a platform, so it lives on the client itself:
2926
- * `scavio.extract({ url })`, never `scavio.extract.extract()`.
2927
- *
2928
- * Credits are a function of `mode`, not a flat per-call constant:
2929
- * "normal" costs 1, "advanced" costs 1, "ultra" costs 2. Billing happens
2930
- * only on a successful extraction - a dead link, bot wall or timeout costs
2931
- * nothing.
2932
- *
2933
- * Start on "normal". Move to "advanced" when the page builds its content in
2934
- * the browser, and to "ultra" only when a bot wall blocks the other two.
2935
- *
2936
- * @example
2937
- * const page = await scavio.extract({ url: "https://example.com/pricing" });
2938
- * console.log(page.content);
2939
- */
2940
- async extract(options) {
2941
- return this._post("/api/v1/extract", options);
2942
- }
2943
3262
  async getUsage() {
2944
3263
  return this._get("/api/v1/usage");
2945
3264
  }
2946
3265
  };
3266
+
3267
+ // src/webhook.ts
3268
+ async function verifyWebhook(secret, header, body, toleranceSec = 300) {
3269
+ if (!secret || !header) return false;
3270
+ const parts = {};
3271
+ for (const piece of header.split(",")) {
3272
+ const i = piece.indexOf("=");
3273
+ if (i > 0) parts[piece.slice(0, i).trim()] = piece.slice(i + 1).trim();
3274
+ }
3275
+ const t = Number(parts.t);
3276
+ const v1 = parts.v1;
3277
+ if (!parts.t || !Number.isFinite(t) || !v1 || !/^[0-9a-f]+$/i.test(v1)) return false;
3278
+ if (Math.abs(Math.floor(Date.now() / 1e3) - t) > toleranceSec) return false;
3279
+ const subtle = await getSubtle();
3280
+ const enc = new TextEncoder();
3281
+ const key = await subtle.importKey(
3282
+ "raw",
3283
+ enc.encode(secret),
3284
+ { name: "HMAC", hash: "SHA-256" },
3285
+ false,
3286
+ ["sign"]
3287
+ );
3288
+ const bodyBytes = typeof body === "string" ? enc.encode(body) : body;
3289
+ const prefix = enc.encode(`${t}.`);
3290
+ const message = new Uint8Array(prefix.length + bodyBytes.length);
3291
+ message.set(prefix, 0);
3292
+ message.set(bodyBytes, prefix.length);
3293
+ const mac = new Uint8Array(await subtle.sign("HMAC", key, message));
3294
+ let expected = "";
3295
+ for (const b of mac) expected += b.toString(16).padStart(2, "0");
3296
+ return timingSafeEqualHex(expected, v1.toLowerCase());
3297
+ }
3298
+ function timingSafeEqualHex(a, b) {
3299
+ if (a.length !== b.length) return false;
3300
+ let diff = 0;
3301
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
3302
+ return diff === 0;
3303
+ }
3304
+ async function getSubtle() {
3305
+ const g = globalThis;
3306
+ if (g.crypto?.subtle) return g.crypto.subtle;
3307
+ const nodeCrypto = await import("crypto");
3308
+ return nodeCrypto.webcrypto.subtle;
3309
+ }
2947
3310
  // Annotate the CommonJS export names for ESM import in node:
2948
3311
  0 && (module.exports = {
2949
3312
  AirbnbNamespace,
@@ -2956,6 +3319,7 @@ var Scavio = class {
2956
3319
  CostcoNamespace,
2957
3320
  DEFAULT_MAX_REQUESTS_PER_SECOND,
2958
3321
  EbayNamespace,
3322
+ ExtractNamespace,
2959
3323
  G2Namespace,
2960
3324
  GlassdoorNamespace,
2961
3325
  GoogleAdsNamespace,
@@ -2983,6 +3347,7 @@ var Scavio = class {
2983
3347
  ScavioAPIError,
2984
3348
  ScavioConnectionError,
2985
3349
  ScavioError,
3350
+ ScavioJobTimeoutError,
2986
3351
  ScavioTimeoutError,
2987
3352
  TargetNamespace,
2988
3353
  ThreadsNamespace,
@@ -2994,6 +3359,7 @@ var Scavio = class {
2994
3359
  XNamespace,
2995
3360
  YelpNamespace,
2996
3361
  YouTubeNamespace,
2997
- ZillowNamespace
3362
+ ZillowNamespace,
3363
+ verifyWebhook
2998
3364
  });
2999
3365
  //# sourceMappingURL=index.cjs.map