serpkite 0.3.0 → 0.5.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 +138 -7
- package/dist/index.cjs +214 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1986 -135
- package/dist/index.d.ts +1986 -135
- package/dist/index.js +214 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,6 +87,7 @@ vertical-specific extras and `meta` (`request_id`, `credits_used`, `cached`, `la
|
|
|
87
87
|
| `news(params)` | `POST /v1/news` | `SearchParams` | `NewsResponse` |
|
|
88
88
|
| `maps(params)` | `POST /v1/maps` | `SearchParams` (+ `ll`: `"@lat,lng,14z"`) | `PlacesResponse` |
|
|
89
89
|
| `places(params)` | `POST /v1/places` | `SearchParams` | `PlacesResponse` |
|
|
90
|
+
| `place(params)` | `POST /v1/place` | `PlaceParams` (`place_id` \| `cid` \| `fid`, `country`, `language`) | `PlacesResponse` (one place: `weekly_hours`, `photos`, `timezone`, …; empty when the id is unknown) |
|
|
90
91
|
| `reviews(params)` | `POST /v1/reviews` | `ReviewsParams` (`place_id` \| `cid` \| `fid`, `sort`, `page_token`, `num` ≤ 50) | `ReviewsResponse` (+ `next_page_token`) |
|
|
91
92
|
| `shopping(params)` | `POST /v1/shopping` | `SearchParams` | `ShoppingResponse` |
|
|
92
93
|
| `scholar(params)` | `POST /v1/scholar` | `SearchParams` | `ScholarResponse` |
|
|
@@ -94,6 +95,13 @@ vertical-specific extras and `meta` (`request_id`, `credits_used`, `cached`, `la
|
|
|
94
95
|
| `autocomplete(params)` | `POST /v1/autocomplete` | `SearchParams` | `AutocompleteResponse` (`results[].value`) |
|
|
95
96
|
| `webpage(params)` | `POST /v1/webpage` | `WebpageParams` (`url`, `include_html`) | `WebpageResponse` (`markdown`, `text`, `metadata`) |
|
|
96
97
|
| `rank(params)` | `POST /v1/rank` | `RankParams` (`q`, `domain`, `num`: 10\|20\|30\|50\|100) | `RankResponse` (`position` or `null`, `matches`, `checked`) |
|
|
98
|
+
| `extract(params)` | `POST /v1/extract` | `ExtractParams` (`urls` ≤ 20, `format`, `query`, `highlights`, …) | `ExtractResponse` (`results`, `failed`) |
|
|
99
|
+
| `map(params)` | `POST /v1/map` | `MapParams` (`url`, `search`, `limit`, `sitemap`, path filters) | `MapResponse` (`results[].url`) |
|
|
100
|
+
| `crawl(params)` | `POST /v1/crawl` | `CrawlParams` (`url`, `limit` ≤ 1000, `max_depth` ≤ 10, …) | `TaskCreated` (`202`) |
|
|
101
|
+
| `getCrawl(id)` / `cancelCrawl(id)` | `GET` / `DELETE /v1/crawl/{id}` | task id | `CrawlTask` / `TaskCancelResponse` |
|
|
102
|
+
| `waitForCrawl(idOrTask, options?)` | polls `GET /v1/crawl/{id}` | task id or task | `CrawlTask` (`completed`, `failed` or `canceled`) |
|
|
103
|
+
| `monitors.create/list/get/update/delete/run` | `/v1/monitors…` | `MonitorCreateParams`, `MonitorUpdateParams` | `Monitor`, `MonitorList` |
|
|
104
|
+
| `monitors.runs(id, params?)` / `monitors.iterRuns(id)` | `GET /v1/monitors/{id}/runs` | `limit`, `before` | `MonitorRunList` / `AsyncIterable<MonitorRun>` |
|
|
97
105
|
| `account()` | `GET /v1/account` | none | `Account` (`balance`, `plan`, `rate_limit_rps`, `month`, …) |
|
|
98
106
|
| `batches.create(params)` | `POST /v1/batches` | `BatchCreateParams` | `BatchCreateResponse` |
|
|
99
107
|
| `batches.get(id)` | `GET /v1/batches/{id}` | batch id | `Batch` |
|
|
@@ -121,8 +129,12 @@ const lean = await sk.search({ q: "espresso", fields: "results.title,results.lin
|
|
|
121
129
|
// Token-lean JSON for agents
|
|
122
130
|
const compact = await sk.search({ q: "espresso", format: "compact" });
|
|
123
131
|
|
|
124
|
-
//
|
|
125
|
-
|
|
132
|
+
// One place in full: structured hours, phones, time zone, photos
|
|
133
|
+
const { results } = await sk.place({ place_id: "ChIJN1t_tDeuEmsRUsoyG83frY4" });
|
|
134
|
+
console.log(results[0]?.timezone, results[0]?.weekly_hours?.monday); // [{ open: "09:00", close: "17:00" }]
|
|
135
|
+
|
|
136
|
+
// Reviews, paged (the first 10 of a place)
|
|
137
|
+
let page = await sk.reviews({ place_id: "ChIJN1t_tDeuEmsRUsoyG83frY4", num: 5 });
|
|
126
138
|
while (page.next_page_token) {
|
|
127
139
|
page = await sk.reviews({ place_id: "ChIJN1t_tDeuEmsRUsoyG83frY4", page_token: page.next_page_token });
|
|
128
140
|
}
|
|
@@ -146,6 +158,24 @@ await sk.search({ q: "espresso", max_age: 3600 });
|
|
|
146
158
|
"compact"` resolves to a `CompactResponse`. The return type follows the literal you pass. With
|
|
147
159
|
`fields` the response contains only the requested keys, so treat the typed fields as optional.
|
|
148
160
|
|
|
161
|
+
## Search controls
|
|
162
|
+
|
|
163
|
+
Domain filters and date ranges work on search, news, images and videos; `boost_domains` on search
|
|
164
|
+
and news; `highlights` on search with `include_content`. None of them costs extra credits.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
const res = await sk.search({
|
|
168
|
+
q: "connection pooling",
|
|
169
|
+
include_domains: ["postgresql.org", "github.com/pgbouncer", ".edu"], // host, path prefix or TLD (≤ 20)
|
|
170
|
+
exclude_domains: ["pinterest.com"],
|
|
171
|
+
boost_domains: ["postgresql.org"], // to the top, keeping the rest
|
|
172
|
+
start_date: "2026-01-01", // YYYY-MM-DD, end_date too
|
|
173
|
+
include_content: 3,
|
|
174
|
+
highlights: true, // 3 query-ranked passages per page instead of the whole page
|
|
175
|
+
});
|
|
176
|
+
for (const r of res.results) console.log(r.position, r.published_at, r.highlights?.[0]?.text);
|
|
177
|
+
```
|
|
178
|
+
|
|
149
179
|
## Search engines & fallback
|
|
150
180
|
|
|
151
181
|
By default every request is answered by Google only (`engine: "google"`); SerpKite already
|
|
@@ -173,6 +203,104 @@ await sk.news({ q: "espresso", engine: ["google", "brave"] });
|
|
|
173
203
|
and its `outcome` (absent on cache hits). `request.engine` echoes what you asked for.
|
|
174
204
|
- Credits (`meta.credits_used`, `X-Credits-Used`) follow the answering provider's price.
|
|
175
205
|
|
|
206
|
+
## Map and extract
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
// The URLs of a site (1 credit): sitemaps + start page, canonicalised and deduplicated
|
|
210
|
+
const site = await sk.map({ url: "https://docs.example.com/", search: "install", include_paths: ["^/guides/"] });
|
|
211
|
+
for (const u of site.results) console.log(u.url);
|
|
212
|
+
|
|
213
|
+
// Up to 20 URLs (HTML or PDF) as Markdown in one call: 1 credit per URL that came back
|
|
214
|
+
const pages = await sk.extract({ urls: site.results.slice(0, 5).map((u) => u.url), query: "install", highlights: 3 });
|
|
215
|
+
for (const f of pages.failed) console.log("failed", f.url, f.error.code); // not charged
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Crawl
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
const task = await sk.crawl({
|
|
222
|
+
url: "https://docs.example.com/",
|
|
223
|
+
limit: 200, // up to 1000 pages; max_depth up to 10
|
|
224
|
+
include_paths: ["^/guides/"],
|
|
225
|
+
sitemap: "include", // include (default) | only | skip
|
|
226
|
+
query: "authentication", // read the most relevant pages first
|
|
227
|
+
});
|
|
228
|
+
const done = await sk.waitForCrawl(task); // polls until completed/failed/canceled
|
|
229
|
+
for (const p of done.result?.pages ?? []) console.log(p.url, p.markdown?.length);
|
|
230
|
+
console.log(done.result?.stats.stopped); // done | limit | time_limit | size_limit | too_many_failures | canceled
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
- `crawl` reserves `limit` credits and charges 1 per page read (0.5 from cache); the rest is
|
|
234
|
+
refunded. A crawl that reads nothing fails (`no_pages`) and costs nothing.
|
|
235
|
+
- It returns `202` with a task; `waitForCrawl` takes `{ timeoutMs, pollIntervalMs, maxPollIntervalMs,
|
|
236
|
+
signal }` like `batches.wait` (default timeout 35 minutes: a crawl runs for up to 30) and
|
|
237
|
+
returns failed or canceled tasks instead of throwing.
|
|
238
|
+
- `cancelCrawl(id)` refunds a queued crawl; a running one stops at its next checkpoint
|
|
239
|
+
(`status: "canceling"`) and is charged for the pages read.
|
|
240
|
+
- Robots.txt (per host) and Crawl-delay are honoured; `result.stats` reports `discovered`, `queued`,
|
|
241
|
+
`duplicates`, `sitemap_urls`, `robots`, `robots_blocked` and why it `stopped`.
|
|
242
|
+
- Pass `webhook_url` to get a signed `crawl.completed` delivery instead of polling. A result over
|
|
243
|
+
4 MB arrives as `result: null` with `result_omitted: true`; fetch it from `poll_url`.
|
|
244
|
+
|
|
245
|
+
## Monitors
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
const mon = await sk.monitors.create({
|
|
249
|
+
q: "ai agents",
|
|
250
|
+
endpoint: "news",
|
|
251
|
+
interval: "hourly", // hourly | daily | weekly, or interval_seconds (3600-2592000)
|
|
252
|
+
webhook_url: "https://example.com/hooks/serpkite",
|
|
253
|
+
});
|
|
254
|
+
await sk.monitors.run(mon.id); // due within ~30 s
|
|
255
|
+
await sk.monitors.update(mon.id, { active: false });
|
|
256
|
+
await sk.monitors.update(mon.id, { q: "ai agents frameworks", num: 20 }); // change the saved search
|
|
257
|
+
const { results } = await sk.monitors.list();
|
|
258
|
+
|
|
259
|
+
// Run history, newest first (new results kept 24 h): one page, or every run
|
|
260
|
+
const page = await sk.monitors.runs(mon.id, { limit: 20 }); // page.next_before → { before }
|
|
261
|
+
for await (const run of sk.monitors.iterRuns(mon.id)) console.log(run.status, run.new_results);
|
|
262
|
+
await sk.monitors.delete(mon.id);
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
A `webpage` monitor watches one page instead of a search: it checks `url` each interval (1 credit
|
|
266
|
+
per check) and reports it as a `MonitorPageChange` (`url`, `title`, `change: "new" | "changed"`,
|
|
267
|
+
`content_hash`, `markdown`) when its content changes. `metadata` (your own JSON object, ≤ 2 KB) is
|
|
268
|
+
stored on any monitor and echoed in its `monitor.results` webhooks; on `update`, an object replaces it
|
|
269
|
+
and `null` clears it.
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
const watch = await sk.monitors.create({
|
|
273
|
+
endpoint: "webpage",
|
|
274
|
+
url: "https://example.com/pricing",
|
|
275
|
+
interval: "daily",
|
|
276
|
+
metadata: { customer: "acme" },
|
|
277
|
+
});
|
|
278
|
+
await sk.monitors.update(watch.id, { metadata: null });
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Each search run costs what its search costs (1 credit per 10 results, `num` 100 is 7; empty and failed runs are free) and POSTs only results it
|
|
282
|
+
hasn't seen before (among the top `num`) as a `monitor.results` webhook. `webhook_url` is optional:
|
|
283
|
+
without one, read new results from `monitors.runs`. `active: false` creates a monitor paused; after 10
|
|
284
|
+
failed runs in a row it pauses itself (`last_status: "paused"`) and `update(id, { active: true })`
|
|
285
|
+
resumes it. `webhook_url: ""` removes the webhook; a changed search reports every result as new once.
|
|
286
|
+
|
|
287
|
+
## Webhooks
|
|
288
|
+
|
|
289
|
+
Every delivery (`batch.completed`, `crawl.completed`, `monitor.results`) is signed with
|
|
290
|
+
`X-SerpKite-Signature`. `parseWebhook` verifies it and returns the event typed by
|
|
291
|
+
`X-SerpKite-Event`, with `deliveryId` from `X-SerpKite-Delivery` (for `monitor.results`, the
|
|
292
|
+
`run_id`) to deduplicate retries:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { parseWebhook } from "serpkite";
|
|
296
|
+
|
|
297
|
+
const ev = await parseWebhook(process.env.SERPKITE_WEBHOOK_SECRET!, rawBody, req.headers); // throws invalid_signature
|
|
298
|
+
if (ev.type === "monitor.results") console.log(ev.data.metadata, ev.data.url, ev.data.new_results.length);
|
|
299
|
+
if (ev.type === "crawl.completed") console.log(ev.data.status, ev.data.poll_url);
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`verifyWebhook(secret, rawBody, headers)` only checks the signature and returns a boolean.
|
|
303
|
+
|
|
176
304
|
## Batches
|
|
177
305
|
|
|
178
306
|
Batch jobs cost half price. Submit 1-100 requests for one endpoint; each request becomes a job.
|
|
@@ -246,18 +374,21 @@ Failed, empty and blocked searches are refunded, so they never cost credits.
|
|
|
246
374
|
Requests are retried up to `maxRetries` times (default 2) on `429`, `5xx`, network errors and
|
|
247
375
|
timeouts, with exponential backoff (`retryDelayMs · 2^attempt`, capped at 8 s) and jitter. A
|
|
248
376
|
`Retry-After` header (seconds or HTTP date, capped at 60 s) takes precedence. Other `4xx` errors
|
|
249
|
-
are never retried. `batches.create
|
|
250
|
-
(and bill) the same
|
|
377
|
+
are never retried. `batches.create`, `crawl`, `monitors.create` and `monitors.run` retry only on
|
|
378
|
+
`429`, so a lost response can never queue (and bill) the same work twice.
|
|
251
379
|
|
|
252
380
|
## Types
|
|
253
381
|
|
|
254
382
|
Types are generated from the OpenAPI contract (`backend/api/serp-api.yaml`) with
|
|
255
383
|
`openapi-typescript`. Friendly aliases are exported: `SearchParams`, `SearchResponse`,
|
|
256
384
|
`OrganicResult`, `AnswerBox`, `NewsResponse`, `NewsResult`, `ImagesResponse`, `PlacesResponse`,
|
|
257
|
-
`
|
|
258
|
-
`
|
|
385
|
+
`PlaceParams`, `PlaceResult`, `WeeklyHours`, `HoursInterval`, `PlacePhoto`, `ReviewsResponse`,
|
|
386
|
+
`ShoppingResponse`, `ScholarResponse`, `PatentsResponse`, `AutocompleteResponse`, `WebpageResponse`,
|
|
387
|
+
`RankResponse`,
|
|
259
388
|
`Account`, `Batch`, `BatchCreateParams`, `Meta`, `Engine`, `Provider`, `RouteStep`,
|
|
260
|
-
`CompactResponse`
|
|
389
|
+
`CompactResponse`, `MapResponse`, `ExtractResponse`, `CrawlParams`, `CrawlTask`, `CrawlResult`,
|
|
390
|
+
`TaskCreated`, `Monitor`, `MonitorRun`, `MonitorPageChange`, `TaskCompletedEvent`,
|
|
391
|
+
`MonitorResultsEvent`, `WebhookEvent` and more. The raw
|
|
261
392
|
`paths`, `components` and `operations` types are exported as well.
|
|
262
393
|
|
|
263
394
|
## Development
|
package/dist/index.cjs
CHANGED
|
@@ -50,8 +50,9 @@ function errorFromResponse(status, headers, body) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// src/client.ts
|
|
53
|
-
var VERSION = "0.
|
|
53
|
+
var VERSION = "0.5.0";
|
|
54
54
|
var DEFAULT_BASE_URL = "https://api.serpkite.com";
|
|
55
|
+
var TASK_DONE = /* @__PURE__ */ new Set(["completed", "failed", "canceled"]);
|
|
55
56
|
var SerpKite = class {
|
|
56
57
|
baseUrl;
|
|
57
58
|
timeoutMs;
|
|
@@ -101,6 +102,12 @@ var SerpKite = class {
|
|
|
101
102
|
maps = this.#vertical("/v1/maps");
|
|
102
103
|
/** Google local results. */
|
|
103
104
|
places = this.#vertical("/v1/places");
|
|
105
|
+
/**
|
|
106
|
+
* Details of one place by `place_id`, `cid` or `fid`: address, country, time zone, hours
|
|
107
|
+
* (`opening_hours` text and structured `weekly_hours`), phones, description and photos.
|
|
108
|
+
* `results` holds the place, or is empty (not charged) when no place has that id. 1 credit.
|
|
109
|
+
*/
|
|
110
|
+
place = this.#vertical("/v1/place");
|
|
104
111
|
/** Reviews of a place by `place_id`, `cid` or `fid`. Page with `page_token` = `next_page_token`. */
|
|
105
112
|
reviews = this.#vertical("/v1/reviews");
|
|
106
113
|
/** Google Shopping. */
|
|
@@ -117,10 +124,150 @@ var SerpKite = class {
|
|
|
117
124
|
rank(params, options) {
|
|
118
125
|
return this.request({ method: "POST", path: "/v1/rank", body: params, options });
|
|
119
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Read up to 20 URLs (HTML or PDF) as Markdown, text or HTML in one call, with optional
|
|
129
|
+
* query-ranked `highlights` (BM25), `include_links` and `include_images`. 1 credit per URL
|
|
130
|
+
* that comes back (0.5 from cache); URLs that fail are listed in `failed` and cost nothing.
|
|
131
|
+
*/
|
|
132
|
+
extract(params, options) {
|
|
133
|
+
const wait = ((params.timeout ?? 50) + 15) * 1e3;
|
|
134
|
+
return this.request({
|
|
135
|
+
method: "POST",
|
|
136
|
+
path: "/v1/extract",
|
|
137
|
+
body: params,
|
|
138
|
+
options: {
|
|
139
|
+
...options,
|
|
140
|
+
timeoutMs: options?.timeoutMs ?? (this.timeoutMs > 0 ? Math.max(this.timeoutMs, wait) : 0)
|
|
141
|
+
},
|
|
142
|
+
retry: "rate_limit"
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The URLs of a site from its sitemaps (XML, RSS/Atom, gzip, plain text) and start page,
|
|
147
|
+
* canonicalised and deduplicated, optionally ranked by `search`. `include_paths` /
|
|
148
|
+
* `exclude_paths` (regular expressions on the URL path) and `ignore_query_parameters`
|
|
149
|
+
* narrow the list. 1 credit; free when nothing is found.
|
|
150
|
+
*/
|
|
151
|
+
map(params, options) {
|
|
152
|
+
return this.request({ method: "POST", path: "/v1/map", body: params, options });
|
|
153
|
+
}
|
|
120
154
|
/** Balance, limits and this month's usage for the key's account. */
|
|
121
155
|
account(options) {
|
|
122
156
|
return this.request({ method: "GET", path: "/v1/account", options });
|
|
123
157
|
}
|
|
158
|
+
// ── Async tasks: crawl ────────────────────────────────────────────────
|
|
159
|
+
/**
|
|
160
|
+
* Starts an async crawl of one site and returns the queued task (`202`). 1 credit per page
|
|
161
|
+
* read (0.5 from cache); `limit` credits (up to 1000 pages, `max_depth` up to 10) are
|
|
162
|
+
* reserved up front and the rest refunded. `sitemap` (`include` default, `only`, `skip`)
|
|
163
|
+
* seeds the crawl with sitemap URLs; `query` reads the most relevant pages first. robots.txt
|
|
164
|
+
* and Crawl-delay are honoured; `result.stats.stopped` says why the crawl ended (`done`,
|
|
165
|
+
* `limit`, `time_limit`, `size_limit`, `too_many_failures`, `canceled`).
|
|
166
|
+
* Only retried on 429, so a lost response never starts (and reserves) a second crawl.
|
|
167
|
+
*/
|
|
168
|
+
crawl(params, options) {
|
|
169
|
+
return this.request({
|
|
170
|
+
method: "POST",
|
|
171
|
+
path: "/v1/crawl",
|
|
172
|
+
body: params,
|
|
173
|
+
options,
|
|
174
|
+
retry: "rate_limit"
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/** Polls a crawl (results are kept for 24 hours). */
|
|
178
|
+
getCrawl(id, options) {
|
|
179
|
+
return this.request({ method: "GET", path: `/v1/crawl/${encodeURIComponent(id)}`, options });
|
|
180
|
+
}
|
|
181
|
+
/** Cancels a crawl: refunded if still queued, else stops at the next checkpoint. */
|
|
182
|
+
cancelCrawl(id, options) {
|
|
183
|
+
return this.request({
|
|
184
|
+
method: "DELETE",
|
|
185
|
+
path: `/v1/crawl/${encodeURIComponent(id)}`,
|
|
186
|
+
options
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Polls until the crawl is `completed`, `failed` or `canceled` and returns it (a failed
|
|
191
|
+
* task is returned, not thrown; check `status` and `error`). Accepts an id or the task
|
|
192
|
+
* returned by `crawl`.
|
|
193
|
+
*/
|
|
194
|
+
async waitForCrawl(idOrTask, options = {}) {
|
|
195
|
+
return this.#waitTask(taskId(idOrTask), (id) => this.getCrawl(id, { signal: options.signal }), {
|
|
196
|
+
...options,
|
|
197
|
+
timeoutMs: options.timeoutMs ?? 21e5
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// ── Monitors ──────────────────────────────────────────────────────────
|
|
201
|
+
monitors = {
|
|
202
|
+
/**
|
|
203
|
+
* Saves a `search` or `news` request (`q`) that runs on a schedule (hourly to monthly) and
|
|
204
|
+
* POSTs results it hasn't seen before to `webhook_url` (`monitor.results`). With
|
|
205
|
+
* `endpoint: "webpage"` and a `url` it checks that page each interval (1 credit per check)
|
|
206
|
+
* and reports it (`MonitorPageChange`) when its content changes. `metadata` (your JSON
|
|
207
|
+
* object, at most 2 KB) is echoed on the monitor and in its webhooks. Without a
|
|
208
|
+
* `webhook_url`, read new results from `monitors.runs`. `active: false`
|
|
209
|
+
* creates it paused. A monitor pauses itself after 10 failed runs in a row
|
|
210
|
+
* (`last_status: "paused"`); `update(id, { active: true })` resumes it. Each run costs
|
|
211
|
+
* what its search costs. Only retried on 429.
|
|
212
|
+
*/
|
|
213
|
+
create: (params, options) => this.request({
|
|
214
|
+
method: "POST",
|
|
215
|
+
path: "/v1/monitors",
|
|
216
|
+
body: params,
|
|
217
|
+
options,
|
|
218
|
+
retry: "rate_limit"
|
|
219
|
+
}),
|
|
220
|
+
/** The account's monitors. */
|
|
221
|
+
list: (options) => this.request({ method: "GET", path: "/v1/monitors", options }),
|
|
222
|
+
get: (id, options) => this.request({ method: "GET", path: `/v1/monitors/${encodeURIComponent(id)}`, options }),
|
|
223
|
+
/**
|
|
224
|
+
* Changes `name`, `interval` / `interval_seconds`, `webhook_url` (`""` removes it),
|
|
225
|
+
* `active` (`true` resumes a paused monitor), `metadata` (an object replaces it, `null`
|
|
226
|
+
* clears it) or the saved request (`endpoint`, `q`, `url`, `country`, `num`, domains,
|
|
227
|
+
* `engine`…). Switching `endpoint` between a search and `webpage` drops the other kind's
|
|
228
|
+
* fields. A changed search reports every result as new on the next run.
|
|
229
|
+
*/
|
|
230
|
+
update: (id, params, options) => this.request({
|
|
231
|
+
method: "PATCH",
|
|
232
|
+
path: `/v1/monitors/${encodeURIComponent(id)}`,
|
|
233
|
+
body: params,
|
|
234
|
+
options
|
|
235
|
+
}),
|
|
236
|
+
delete: async (id, options) => {
|
|
237
|
+
await this.request({
|
|
238
|
+
method: "DELETE",
|
|
239
|
+
path: `/v1/monitors/${encodeURIComponent(id)}`,
|
|
240
|
+
options
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
/** Runs an active monitor at the next scheduler poll (within about 30 seconds).
|
|
244
|
+
* Paused monitors return 409; update(id, { active: true }) resumes and schedules them. */
|
|
245
|
+
run: (id, options) => this.request({
|
|
246
|
+
method: "POST",
|
|
247
|
+
path: `/v1/monitors/${encodeURIComponent(id)}/run`,
|
|
248
|
+
options,
|
|
249
|
+
retry: "rate_limit"
|
|
250
|
+
}),
|
|
251
|
+
/**
|
|
252
|
+
* One page of the monitor's run history, newest first (`limit` 1-100, default 20). Each
|
|
253
|
+
* run carries its new results for 24 hours. Pass `next_before` as `before` for the next
|
|
254
|
+
* page; it is null on the last one.
|
|
255
|
+
*/
|
|
256
|
+
runs: (id, params = {}, options) => this.request({
|
|
257
|
+
method: "GET",
|
|
258
|
+
path: `/v1/monitors/${encodeURIComponent(id)}/runs${query(params)}`,
|
|
259
|
+
options
|
|
260
|
+
}),
|
|
261
|
+
/**
|
|
262
|
+
* Iterates over every run of the monitor, newest first, fetching pages of `limit` as
|
|
263
|
+
* needed (starting at `before` when given).
|
|
264
|
+
*
|
|
265
|
+
* ```ts
|
|
266
|
+
* for await (const run of sk.monitors.iterRuns("m1")) console.log(run.new_results);
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
iterRuns: (id, params = {}, options) => this.#iterRuns(id, params, options)
|
|
270
|
+
};
|
|
124
271
|
// ── Batches ───────────────────────────────────────────────────────────
|
|
125
272
|
batches = {
|
|
126
273
|
/**
|
|
@@ -150,6 +297,35 @@ var SerpKite = class {
|
|
|
150
297
|
wait: (idOrEntry, options = {}) => this.#wait(idOrEntry, options)
|
|
151
298
|
};
|
|
152
299
|
// ── Plumbing ──────────────────────────────────────────────────────────
|
|
300
|
+
async *#iterRuns(id, params, options) {
|
|
301
|
+
let before = params.before;
|
|
302
|
+
for (; ; ) {
|
|
303
|
+
const page = await this.monitors.runs(id, { ...params, before }, options);
|
|
304
|
+
yield* page.results;
|
|
305
|
+
if (!page.next_before || page.results.length === 0) return;
|
|
306
|
+
before = page.next_before;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async #waitTask(id, get, o) {
|
|
310
|
+
const timeoutMs = o.timeoutMs ?? 6e5;
|
|
311
|
+
const maxInterval = o.maxPollIntervalMs ?? 1e4;
|
|
312
|
+
let interval = o.pollIntervalMs ?? 1e3;
|
|
313
|
+
const deadline = Date.now() + timeoutMs;
|
|
314
|
+
for (; ; ) {
|
|
315
|
+
const task = await get(id);
|
|
316
|
+
if (TASK_DONE.has(task.status)) return task;
|
|
317
|
+
const left = deadline - Date.now();
|
|
318
|
+
if (left <= 0) {
|
|
319
|
+
throw new SerpKiteError(
|
|
320
|
+
0,
|
|
321
|
+
"timeout",
|
|
322
|
+
`task ${id} still ${task.status} after ${timeoutMs} ms`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
await sleep(Math.min(interval, left), o.signal);
|
|
326
|
+
interval = Math.min(interval * 1.5, maxInterval);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
153
329
|
#vertical(path) {
|
|
154
330
|
return ((params, options) => this.request({ method: "POST", path, body: params, options }));
|
|
155
331
|
}
|
|
@@ -284,6 +460,19 @@ var SerpKite = class {
|
|
|
284
460
|
return after === void 0 ? this.#backoff(attempt) : Math.min(after, 6e4);
|
|
285
461
|
}
|
|
286
462
|
};
|
|
463
|
+
function query(params) {
|
|
464
|
+
const q = new URLSearchParams();
|
|
465
|
+
for (const [k, v] of Object.entries(params)) {
|
|
466
|
+
if (v !== void 0 && v !== null) q.set(k, String(v));
|
|
467
|
+
}
|
|
468
|
+
const s = q.toString();
|
|
469
|
+
return s ? `?${s}` : "";
|
|
470
|
+
}
|
|
471
|
+
function taskId(idOrTask) {
|
|
472
|
+
const id = typeof idOrTask === "string" ? idOrTask : idOrTask?.id;
|
|
473
|
+
if (!id) throw new SerpKiteError(0, "invalid_request", "a task id is required");
|
|
474
|
+
return id;
|
|
475
|
+
}
|
|
287
476
|
function parseRetryAfter(v) {
|
|
288
477
|
if (!v) return void 0;
|
|
289
478
|
const secs = Number(v);
|
|
@@ -404,6 +593,29 @@ async function verifyWebhook(secret, payload, headers, options = {}) {
|
|
|
404
593
|
for (let i = 0; i < want.length; i++) diff |= want.charCodeAt(i) ^ got.charCodeAt(i);
|
|
405
594
|
return diff === 0;
|
|
406
595
|
}
|
|
596
|
+
async function parseWebhook(secret, payload, headers, options = {}) {
|
|
597
|
+
if (!await verifyWebhook(secret, payload, headers, options)) {
|
|
598
|
+
throw new SerpKiteError(400, "invalid_signature", "webhook signature or timestamp is invalid");
|
|
599
|
+
}
|
|
600
|
+
const text = typeof payload === "string" ? payload : new TextDecoder().decode(payload);
|
|
601
|
+
let data;
|
|
602
|
+
try {
|
|
603
|
+
data = JSON.parse(text);
|
|
604
|
+
} catch (err) {
|
|
605
|
+
throw new SerpKiteError(
|
|
606
|
+
400,
|
|
607
|
+
"invalid_payload",
|
|
608
|
+
"webhook body is not JSON",
|
|
609
|
+
void 0,
|
|
610
|
+
void 0,
|
|
611
|
+
{
|
|
612
|
+
cause: err
|
|
613
|
+
}
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
const type = header(headers, "x-serpkite-event") ?? (typeof data?.event === "string" ? data.event : "batch.completed");
|
|
617
|
+
return { type, deliveryId: header(headers, "x-serpkite-delivery"), data };
|
|
618
|
+
}
|
|
407
619
|
|
|
408
620
|
exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
|
|
409
621
|
exports.SerpKite = SerpKite;
|
|
@@ -411,6 +623,7 @@ exports.SerpKiteError = SerpKiteError;
|
|
|
411
623
|
exports.VERSION = VERSION;
|
|
412
624
|
exports.isBatchError = isBatchError;
|
|
413
625
|
exports.parseRetryAfter = parseRetryAfter;
|
|
626
|
+
exports.parseWebhook = parseWebhook;
|
|
414
627
|
exports.verifyWebhook = verifyWebhook;
|
|
415
628
|
//# sourceMappingURL=index.cjs.map
|
|
416
629
|
//# sourceMappingURL=index.cjs.map
|