scavio 0.22.0 → 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.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.22.0" : "0.0.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
  );
@@ -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,128 @@ 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
+ 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
+
2874
2999
  // src/client.ts
2875
3000
  var DEFAULT_MAX_REQUESTS_PER_SECOND = 1;
2876
3001
  var MAX_REQUESTS_PER_SECOND = 50;
2877
- var Scavio = class {
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 {
2878
3029
  google;
2879
3030
  amazon;
2880
3031
  walmart;
@@ -2917,6 +3068,7 @@ var Scavio = class {
2917
3068
  maxRetries;
2918
3069
  rateLimiter;
2919
3070
  constructor(config) {
3071
+ super();
2920
3072
  this.apiKey = config?.apiKey ?? process.env.SCAVIO_API_KEY ?? "";
2921
3073
  if (!this.apiKey) {
2922
3074
  throw new MissingAPIKeyError();
@@ -2967,9 +3119,19 @@ var Scavio = class {
2967
3119
  this.realtor = new RealtorNamespace(this);
2968
3120
  this.rightmove = new RightmoveNamespace(this);
2969
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
+ });
2970
3132
  }
2971
3133
  /** @internal */
2972
- async _post(path, body) {
3134
+ async _post(path, body, headers) {
2973
3135
  return request({
2974
3136
  method: "POST",
2975
3137
  path,
@@ -2978,11 +3140,12 @@ var Scavio = class {
2978
3140
  timeout: this.timeout,
2979
3141
  maxRetries: this.maxRetries,
2980
3142
  rateLimiter: this.rateLimiter,
2981
- body
3143
+ body,
3144
+ headers
2982
3145
  });
2983
3146
  }
2984
3147
  /** @internal */
2985
- async _get(path) {
3148
+ async _get(path, query) {
2986
3149
  return request({
2987
3150
  method: "GET",
2988
3151
  path,
@@ -2990,39 +3153,73 @@ var Scavio = class {
2990
3153
  baseUrl: this.baseUrl,
2991
3154
  timeout: this.timeout,
2992
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,
2993
3169
  rateLimiter: this.rateLimiter
2994
3170
  });
2995
3171
  }
2996
3172
  async search(options) {
2997
3173
  return this.google.search(options);
2998
3174
  }
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
3175
  async getUsage() {
3023
3176
  return this._get("/api/v1/usage");
3024
3177
  }
3025
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
+ }
3026
3223
  export {
3027
3224
  AirbnbNamespace,
3028
3225
  AmazonNamespace,
@@ -3034,6 +3231,7 @@ export {
3034
3231
  CostcoNamespace,
3035
3232
  DEFAULT_MAX_REQUESTS_PER_SECOND,
3036
3233
  EbayNamespace,
3234
+ ExtractNamespace,
3037
3235
  G2Namespace,
3038
3236
  GlassdoorNamespace,
3039
3237
  GoogleAdsNamespace,
@@ -3061,6 +3259,7 @@ export {
3061
3259
  ScavioAPIError,
3062
3260
  ScavioConnectionError,
3063
3261
  ScavioError,
3262
+ ScavioJobTimeoutError,
3064
3263
  ScavioTimeoutError,
3065
3264
  TargetNamespace,
3066
3265
  ThreadsNamespace,
@@ -3072,6 +3271,7 @@ export {
3072
3271
  XNamespace,
3073
3272
  YelpNamespace,
3074
3273
  YouTubeNamespace,
3075
- ZillowNamespace
3274
+ ZillowNamespace,
3275
+ verifyWebhook
3076
3276
  };
3077
3277
  //# sourceMappingURL=index.js.map