@websitescreenshotworld/sdk 0.0.0-stage → 0.1.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +399 -2
  4. package/dist/cjs/auto-schedule.d.ts +20 -0
  5. package/dist/cjs/auto-schedule.js +34 -0
  6. package/dist/cjs/client.d.ts +41 -0
  7. package/dist/cjs/client.js +58 -0
  8. package/dist/cjs/error-codes.d.ts +54 -0
  9. package/dist/cjs/error-codes.js +94 -0
  10. package/dist/cjs/errors.d.ts +56 -0
  11. package/dist/cjs/errors.js +112 -0
  12. package/dist/cjs/files.d.ts +6 -0
  13. package/dist/cjs/files.js +45 -0
  14. package/dist/cjs/index.d.ts +16 -0
  15. package/dist/cjs/index.js +37 -0
  16. package/dist/cjs/instant-snap.d.ts +99 -0
  17. package/dist/cjs/instant-snap.js +205 -0
  18. package/dist/cjs/models/capture-file.d.ts +35 -0
  19. package/dist/cjs/models/capture-file.js +34 -0
  20. package/dist/cjs/models/capture-options.d.ts +144 -0
  21. package/dist/cjs/models/capture-options.js +2 -0
  22. package/dist/cjs/models/capture.d.ts +108 -0
  23. package/dist/cjs/models/capture.js +63 -0
  24. package/dist/cjs/models/defaults.d.ts +82 -0
  25. package/dist/cjs/models/defaults.js +46 -0
  26. package/dist/cjs/models/tokens.d.ts +92 -0
  27. package/dist/cjs/models/tokens.js +85 -0
  28. package/dist/cjs/models/trigger-result.d.ts +20 -0
  29. package/dist/cjs/models/trigger-result.js +13 -0
  30. package/dist/cjs/package.json +3 -0
  31. package/dist/cjs/transport.d.ts +55 -0
  32. package/dist/cjs/transport.js +206 -0
  33. package/dist/cjs/version.d.ts +2 -0
  34. package/dist/cjs/version.js +5 -0
  35. package/dist/cjs/webhooks/events.d.ts +128 -0
  36. package/dist/cjs/webhooks/events.js +13 -0
  37. package/dist/cjs/webhooks/parse-event.d.ts +10 -0
  38. package/dist/cjs/webhooks/parse-event.js +149 -0
  39. package/dist/cjs/webhooks/webhook.d.ts +26 -0
  40. package/dist/cjs/webhooks/webhook.js +78 -0
  41. package/dist/cjs/wire.d.ts +15 -0
  42. package/dist/cjs/wire.js +71 -0
  43. package/dist/esm/auto-schedule.d.ts +20 -0
  44. package/dist/esm/auto-schedule.js +30 -0
  45. package/dist/esm/client.d.ts +41 -0
  46. package/dist/esm/client.js +54 -0
  47. package/dist/esm/error-codes.d.ts +54 -0
  48. package/dist/esm/error-codes.js +91 -0
  49. package/dist/esm/errors.d.ts +56 -0
  50. package/dist/esm/errors.js +98 -0
  51. package/dist/esm/files.d.ts +6 -0
  52. package/dist/esm/files.js +41 -0
  53. package/dist/esm/index.d.ts +16 -0
  54. package/dist/esm/index.js +7 -0
  55. package/dist/esm/instant-snap.d.ts +99 -0
  56. package/dist/esm/instant-snap.js +201 -0
  57. package/dist/esm/models/capture-file.d.ts +35 -0
  58. package/dist/esm/models/capture-file.js +31 -0
  59. package/dist/esm/models/capture-options.d.ts +144 -0
  60. package/dist/esm/models/capture-options.js +1 -0
  61. package/dist/esm/models/capture.d.ts +108 -0
  62. package/dist/esm/models/capture.js +57 -0
  63. package/dist/esm/models/defaults.d.ts +82 -0
  64. package/dist/esm/models/defaults.js +42 -0
  65. package/dist/esm/models/tokens.d.ts +92 -0
  66. package/dist/esm/models/tokens.js +82 -0
  67. package/dist/esm/models/trigger-result.d.ts +20 -0
  68. package/dist/esm/models/trigger-result.js +10 -0
  69. package/dist/esm/transport.d.ts +55 -0
  70. package/dist/esm/transport.js +201 -0
  71. package/dist/esm/version.d.ts +2 -0
  72. package/dist/esm/version.js +2 -0
  73. package/dist/esm/webhooks/events.d.ts +128 -0
  74. package/dist/esm/webhooks/events.js +10 -0
  75. package/dist/esm/webhooks/parse-event.d.ts +10 -0
  76. package/dist/esm/webhooks/parse-event.js +146 -0
  77. package/dist/esm/webhooks/webhook.d.ts +26 -0
  78. package/dist/esm/webhooks/webhook.js +75 -0
  79. package/dist/esm/wire.d.ts +15 -0
  80. package/dist/esm/wire.js +66 -0
  81. package/package.json +68 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the package uses
5
+ [semantic versioning](https://semver.org/).
6
+
7
+ ## [0.1.0] - 2026-11-09
8
+
9
+ The first release.
10
+
11
+ ### Added
12
+
13
+ - `WebsiteScreenshotWorld`, one client for both products, with keys read from
14
+ `WSW_INSTANT_SNAP_API_KEY` and `WSW_AUTO_SCHEDULE_API_KEY` when not passed, and an optional
15
+ `fetch` of your own.
16
+ - Instant Snap: `capture`, `captureFile` (the file as a stream, with its metadata, `arrayBuffer()` and
17
+ `saveTo()`), `captureToBucket`, `get`, `download`, `defaults` and `catalog`, with every capture
18
+ option typed in camelCase.
19
+ - Instant Snap async captures: `submit` and `submitToBucket`, answered at once with the capture
20
+ `queued`, and `wait`, which polls `get` until it has finished; the `timeout` capture option (30 to
21
+ 300 seconds, async only); the `queued` and `running` statuses; and the `queue_full` (a
22
+ `RateLimitError`, not retried) and `capture_expired` error codes.
23
+ - Auto Schedule: `triggerNow`.
24
+ - The `pressEscape` capture option, and the `sitemap_schedule_not_triggerable` error code (an
25
+ `InvalidRequestError`: a sitemap schedule runs only on its schedule), from API version 1.1.0.
26
+ - Redirects are never followed, so the API key is never sent anywhere else; a 3xx answer is raised
27
+ as an error.
28
+ - `Webhook.verify`, which checks the `X-WSW-Signature` header and returns a typed event for each of
29
+ `capture.succeeded`, `capture.failed`, `delivery.completed`, `capture.changed`, `schedule.paused`,
30
+ `sitemap_run.completed` and `ping`, and an unknown event, with its raw data, for types added later.
31
+ - `WebsiteScreenshotWorldError` and a subclass for each kind of error, with `errorCode`, `httpStatus`,
32
+ `retryAfterSeconds` and the raw body, plus `ErrorCode` constants.
33
+ - Retries of rate limits, busy or unavailable service and lost connections, with an `Idempotency-Key`
34
+ on every capture; timeouts and `AbortSignal` cancellation per call.
35
+ - Named constants for the API's tokens, such as `CaptureStatus` and `CaptureFormat`, with unknown
36
+ values passed through as strings.
37
+ - ES module and CommonJS builds with type definitions, for Node.js 22 and later.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Silhouettec Solutions Private Limited
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,400 @@
1
- # Temporary Holding Version
1
+ # Website Screenshot World for Node.js
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The official Node.js client for [Website Screenshot World](https://websitescreenshotworld.com): capture
4
+ web pages on demand with **Instant Snap**, start a scheduled page's capture with **Auto Schedule**, and
5
+ verify the webhooks both products send.
6
+
7
+ - Written in TypeScript, with types for every option, answer and webhook event
8
+ - ES modules and CommonJS, no runtime dependencies
9
+ - Retries with idempotency keys, timeouts and `AbortSignal` cancellation built in
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm install @websitescreenshotworld/sdk
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ Create a client once and reuse it. The keys default to the `WSW_INSTANT_SNAP_API_KEY` and
20
+ `WSW_AUTO_SCHEDULE_API_KEY` environment variables; you only need the key of the product you use.
21
+
22
+ ### Instant Snap
23
+
24
+ ```ts
25
+ import { WebsiteScreenshotWorld } from "@websitescreenshotworld/sdk";
26
+
27
+ const client = new WebsiteScreenshotWorld();
28
+
29
+ const capture = await client.instantSnap.capture({
30
+ url: "https://example.com",
31
+ format: "webp",
32
+ viewport: { width: 1440 },
33
+ });
34
+
35
+ console.log(capture.id, capture.width, capture.height);
36
+ console.log(capture.downloadUrl); // valid for 60 minutes
37
+ ```
38
+
39
+ Every option of the API is available in camelCase: `mediaCategory`, `scrollMode`, `blockAds`,
40
+ `timestampWatermark`, `pageSource` and the rest. Options you leave out take the API's defaults, which
41
+ `client.instantSnap.defaults()` lists; `client.instantSnap.catalog()` lists the devices and proxy
42
+ countries you can ask for. Names in `headers` and `cookies` are sent exactly as you write them.
43
+
44
+ ### Auto Schedule
45
+
46
+ Web pages are set up in the dashboard. Start one's capture now with its id; the outcome arrives by
47
+ webhook.
48
+
49
+ ```ts
50
+ const result = await client.autoSchedule.triggerNow(
51
+ "7d4e2b1a-6c5f-4a3e-9d8c-2b1a0f9e8d7c",
52
+ );
53
+
54
+ if (result.status === "already_pending") {
55
+ console.log("A capture was already on its way:", result.message);
56
+ }
57
+ ```
58
+
59
+ ## Saving a file
60
+
61
+ `captureFile` returns the file in the answer itself, as a stream, along with its metadata. The API does
62
+ not keep the file, so save or read it before you drop the result.
63
+
64
+ ```ts
65
+ const file = await client.instantSnap.captureFile({
66
+ url: "https://example.com",
67
+ format: "png",
68
+ });
69
+
70
+ console.log(file.filename, file.contentType, file.sha256);
71
+ await file.saveTo(`./${file.filename ?? "capture.png"}`);
72
+ ```
73
+
74
+ `file.body` is a web `ReadableStream` you can pipe anywhere instead, and `file.arrayBuffer()` reads the
75
+ whole file into memory. The stream can be read once.
76
+
77
+ ## Uploading to a bucket
78
+
79
+ `captureToBucket` uploads the file to your own S3-compatible storage. Give it a `region` for AWS, or an
80
+ `endpointUrl` for other providers.
81
+
82
+ ```ts
83
+ const capture = await client.instantSnap.captureToBucket({
84
+ url: "https://example.com",
85
+ s3Delivery: {
86
+ accessKeyId: process.env.S3_ACCESS_KEY_ID ?? "",
87
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY ?? "",
88
+ bucket: "captures",
89
+ region: "eu-west-2",
90
+ keyPrefix: "example/",
91
+ waitForSync: true,
92
+ },
93
+ });
94
+
95
+ console.log(capture.s3Delivery?.status, capture.s3Delivery?.key);
96
+ ```
97
+
98
+ ## Async captures
99
+
100
+ `submit` and `submitToBucket` do not wait for the capture: the API accepts it at once and returns it with
101
+ `status: "queued"`, and it runs when a browser is free (`running`, then `succeeded` or `failed`). `wait`
102
+ reads it with `get` every few seconds until it has finished.
103
+
104
+ ```ts
105
+ import { CaptureStatus } from "@websitescreenshotworld/sdk";
106
+
107
+ const queued = await client.instantSnap.submit({
108
+ url: "https://example.com",
109
+ timeout: 240, // seconds of browser time, 30 to 300; the API uses 120 when left out
110
+ });
111
+
112
+ const capture = await client.instantSnap.wait(queued.id, {
113
+ timeoutSeconds: 600, // default 360
114
+ pollIntervalSeconds: 5, // default 3
115
+ });
116
+
117
+ if (capture.status === CaptureStatus.Succeeded) {
118
+ await client.instantSnap.download(capture, "./capture.png");
119
+ }
120
+
121
+ const toBucket = await client.instantSnap.submitToBucket({
122
+ url: "https://example.com",
123
+ s3Delivery: {
124
+ accessKeyId: process.env.S3_ACCESS_KEY_ID ?? "",
125
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY ?? "",
126
+ bucket: "captures",
127
+ region: "eu-west-2",
128
+ },
129
+ });
130
+ console.log(toBucket.status); // "queued"; wait for it the same way
131
+ ```
132
+
133
+ When `timeoutSeconds` runs out first, `wait` returns the capture as last read, still `queued` or
134
+ `running`, rather than throwing, so you can wait again. It takes an `AbortSignal` as `signal`. Instead of
135
+ waiting, you can also set `callbackUrl` and receive the `capture.succeeded` or `capture.failed` webhook.
136
+
137
+ `timeout` is only for `submit` and `submitToBucket`; the API refuses it on the other capture calls. The
138
+ submit call itself uses the client's `timeout` (30 seconds), not `captureTimeout`, since it is answered at
139
+ once. A capture still queued 15 minutes after it was accepted ends `failed` with `capture_expired`, and is
140
+ not charged. When your organization already has as many async captures waiting or running as it may, the
141
+ API answers `queue_full`: the client throws it at once as a `RateLimitError` with `retryAfterSeconds`,
142
+ without retrying.
143
+
144
+ ## Reading a capture back
145
+
146
+ Files are kept for 24 hours. `get` returns the capture with a fresh `downloadUrl`, and `download` writes
147
+ the file (or the page source) to a path, a Node.js stream or a web `WritableStream`.
148
+
149
+ ```ts
150
+ const capture = await client.instantSnap.get(
151
+ "3f6c2a1e-9b8d-4c7e-a5f4-1d2e3c4b5a69",
152
+ );
153
+
154
+ await client.instantSnap.download(capture, "./capture.png");
155
+ if (capture.pageSource !== null) {
156
+ await client.instantSnap.download(capture.pageSource, "./capture.html");
157
+ }
158
+ ```
159
+
160
+ ## Errors and retries
161
+
162
+ Every error is a `WebsiteScreenshotWorldError`, with `errorCode`, `message`, `httpStatus`,
163
+ `retryAfterSeconds` and `rawBody`. Each kind of refusal has its own subclass:
164
+
165
+ | Class | When |
166
+ | ---------------------------- | ------------------------------------------------------------ |
167
+ | `AuthenticationError` | The API key is missing, invalid or for the other product |
168
+ | `InvalidRequestError` | The request was invalid, such as an out-of-range option |
169
+ | `PlanLimitError` | The plan has no captures left, or does not include a feature |
170
+ | `NotFoundError` | No such capture or web page for this account |
171
+ | `RateLimitError` | Too many requests even after retrying, or the queue is full |
172
+ | `CaptureError` | The website could not be captured, or the file was too large |
173
+ | `ServiceError` | The service was busy or failed on its side |
174
+ | `ConnectionError` | No answer came back, or the call ran out of time |
175
+ | `SignatureVerificationError` | A webhook's signature did not check out |
176
+
177
+ Decide what to do from `errorCode`, never from the status or the message. The `ErrorCode` constants name
178
+ the codes the API sends today; a code added later arrives as the base class with its new code.
179
+
180
+ ```ts
181
+ import {
182
+ CaptureError,
183
+ ErrorCode,
184
+ PlanLimitError,
185
+ WebsiteScreenshotWorldError,
186
+ } from "@websitescreenshotworld/sdk";
187
+
188
+ try {
189
+ await client.instantSnap.capture({ url: "https://example.com" });
190
+ } catch (error) {
191
+ if (
192
+ error instanceof CaptureError &&
193
+ error.errorCode === ErrorCode.CaptureTimeout
194
+ ) {
195
+ // The page took too long to load: a longer pageTimeoutMs may help.
196
+ } else if (error instanceof PlanLimitError) {
197
+ // Out of captures, or the plan does not include an option that was asked for.
198
+ } else if (error instanceof WebsiteScreenshotWorldError) {
199
+ console.error(error.errorCode, error.message);
200
+ } else {
201
+ throw error;
202
+ }
203
+ }
204
+ ```
205
+
206
+ The client retries what is safe and likely to succeed: `rate_limited`, `capture_busy`,
207
+ `service_unavailable`, `idempotency_key_in_use` and requests that got no answer. It waits as long as the
208
+ API asks (up to a minute), otherwise 0.5 seconds and then 1 second, with a little jitter. It retries twice
209
+ by default; set `maxRetries` to change that. A failed or timed-out capture is not retried, and neither is
210
+ `queue_full` from `submit`: the queue stays full until captures finish, so you decide when to try again.
211
+
212
+ ## Idempotency keys
213
+
214
+ Every capture carries an `Idempotency-Key`, and retries of that call reuse it, so a retry never captures
215
+ twice. Pass your own key to make a capture safe to repeat across calls and processes for 24 hours:
216
+
217
+ ```ts
218
+ const hero = await client.instantSnap.capture(
219
+ { url: "https://example.com" },
220
+ { idempotencyKey: "order-1234-hero" },
221
+ );
222
+ ```
223
+
224
+ Trigger-now needs no key: a second call while the first capture is pending answers `already_pending`.
225
+
226
+ ## Timeouts and cancellation
227
+
228
+ A capture may take up to 120 seconds on the API's side, so capture calls time out after 180
229
+ seconds and other calls after 30, retries included. Change them for the client or per call, and pass an
230
+ `AbortSignal` to cancel:
231
+
232
+ ```ts
233
+ const quick = new WebsiteScreenshotWorld({ captureTimeout: 60_000 });
234
+
235
+ const controller = new AbortController();
236
+ const pending = quick.instantSnap.capture(
237
+ { url: "https://example.com" },
238
+ { timeout: 90_000, signal: controller.signal },
239
+ );
240
+ controller.abort(); // `pending` rejects with the signal's reason
241
+ ```
242
+
243
+ `submit` and `submitToBucket` use `timeout`, not `captureTimeout`. A timeout raises `ConnectionError`. Cancelling stops the call and any retry it would make at once.
244
+
245
+ ## Webhooks
246
+
247
+ Each delivery is signed in its `X-WSW-Signature` header. `Webhook.verify` checks the signature against
248
+ the raw body and returns the event, typed by `type` and, for captures, by `product`:
249
+
250
+ ```ts
251
+ import { Webhook, type WebhookEvent } from "@websitescreenshotworld/sdk";
252
+
253
+ const secret = process.env.WSW_WEBHOOK_SECRET ?? "";
254
+
255
+ function handle(event: WebhookEvent): void {
256
+ switch (event.type) {
257
+ case "capture.succeeded":
258
+ if (event.product === "instant_snap") {
259
+ console.log(
260
+ "Instant Snap capture",
261
+ event.data.id,
262
+ event.data.mediaSha256,
263
+ );
264
+ } else if (event.product === "auto_schedule") {
265
+ console.log("Scheduled capture of", event.data.scheduleName);
266
+ }
267
+ break;
268
+ case "capture.changed":
269
+ console.log(event.data.url, "changed by", event.data.changePercent, "%");
270
+ break;
271
+ case "ping":
272
+ console.log(event.data.message);
273
+ break;
274
+ default:
275
+ // An event type added after this version: its type and raw data still arrive.
276
+ if ("rawData" in event) console.log(event.type, event.rawData);
277
+ }
278
+ }
279
+ ```
280
+
281
+ The other events are `capture.failed`, `delivery.completed`, `schedule.paused` and
282
+ `sitemap_run.completed`; `WebhookEventType` names them all. `verify` throws `SignatureVerificationError`
283
+ when the signature does not match or its timestamp is more than five minutes from now; pass
284
+ `{ tolerance }` in seconds to change that.
285
+
286
+ The signature covers the exact bytes that were sent, so give `verify` the raw body, not JSON your
287
+ framework parsed and serialised again. The examples below reuse `secret` and `handle` from above.
288
+
289
+ ### Express
290
+
291
+ ```ts
292
+ import express from "express";
293
+
294
+ const app = express();
295
+
296
+ app.post(
297
+ "/webhooks/wsw",
298
+ express.raw({ type: "application/json" }),
299
+ (request, response) => {
300
+ try {
301
+ const event = Webhook.verify(
302
+ request.body as Buffer,
303
+ request.get("X-WSW-Signature"),
304
+ secret,
305
+ );
306
+ handle(event);
307
+ response.sendStatus(204);
308
+ } catch {
309
+ response.sendStatus(400);
310
+ }
311
+ },
312
+ );
313
+ ```
314
+
315
+ ### Fastify
316
+
317
+ ```ts
318
+ import Fastify from "fastify";
319
+
320
+ const fastify = Fastify();
321
+
322
+ fastify.register(async (webhooks) => {
323
+ webhooks.addContentTypeParser(
324
+ "application/json",
325
+ { parseAs: "buffer" },
326
+ (_request, body, done) => {
327
+ done(null, body);
328
+ },
329
+ );
330
+
331
+ webhooks.post("/webhooks/wsw", async (request, reply) => {
332
+ try {
333
+ const event = Webhook.verify(
334
+ request.body as Buffer,
335
+ request.headers["x-wsw-signature"],
336
+ secret,
337
+ );
338
+ handle(event);
339
+ return reply.code(204).send();
340
+ } catch {
341
+ return reply.code(400).send();
342
+ }
343
+ });
344
+ });
345
+ ```
346
+
347
+ ### Next.js (App Router)
348
+
349
+ ```ts
350
+ // app/webhooks/wsw/route.ts
351
+ export async function POST(request: Request): Promise<Response> {
352
+ try {
353
+ const event = Webhook.verify(
354
+ await request.text(),
355
+ request.headers.get("X-WSW-Signature"),
356
+ secret,
357
+ );
358
+ handle(event);
359
+ return new Response(null, { status: 204 });
360
+ } catch {
361
+ return new Response(null, { status: 400 });
362
+ }
363
+ }
364
+ ```
365
+
366
+ ## Configuration
367
+
368
+ | Option | Default |
369
+ | --------------------- | -------------------------------------------- |
370
+ | `instantSnapApiKey` | `WSW_INSTANT_SNAP_API_KEY` |
371
+ | `autoScheduleApiKey` | `WSW_AUTO_SCHEDULE_API_KEY` |
372
+ | `instantSnapBaseUrl` | `https://api.websitescreenshotworld.com` |
373
+ | `autoScheduleBaseUrl` | `https://autoapi.websitescreenshotworld.com` |
374
+ | `captureTimeout` | `180_000` milliseconds |
375
+ | `timeout` | `30_000` milliseconds |
376
+ | `maxRetries` | `2` |
377
+ | `fetch` | the global `fetch` |
378
+
379
+ Either key can be left out. Calling a product without its key throws `AuthenticationError` before
380
+ anything is sent.
381
+
382
+ ## Supported runtimes
383
+
384
+ Node.js 22 and later, from ES modules or CommonJS. The type definitions need TypeScript 5.0 or later.
385
+
386
+ ## Development
387
+
388
+ ```sh
389
+ npm ci
390
+ npm test # builds, then runs the unit tests and the shared conformance cases
391
+ npm run lint # ESLint, Prettier and the type checker
392
+ ```
393
+
394
+ The tests start `spec/conformance/server.js` and read the shared cases in `spec/`, the
395
+ [websitescreenshotworld-spec](https://github.com/websitescreenshotworld/websitescreenshotworld-spec) submodule, so
396
+ clone with `git clone --recurse-submodules`, or run `git submodule update --init` in an existing clone.
397
+
398
+ ## License
399
+
400
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,20 @@
1
+ import { type TriggerResult } from "./models/trigger-result.js";
2
+ import type { RequestOptions, Transport } from "./transport.js";
3
+ /** What {@link AutoSchedule} needs from the client. */
4
+ export interface AutoScheduleConfig {
5
+ transport: Transport;
6
+ apiKey: string | undefined;
7
+ baseUrl: string;
8
+ timeout: number;
9
+ }
10
+ /** Scheduled captures of the web pages set up in the dashboard. */
11
+ export declare class AutoSchedule {
12
+ #private;
13
+ /**
14
+ * Starts a web page's capture now. The outcome arrives by webhook. Calling again while that
15
+ * capture is pending starts nothing new and answers `already_pending`.
16
+ *
17
+ * @param webPageId - The web page's id, which webhooks call `scheduleId`.
18
+ */
19
+ triggerNow(webPageId: string, requestOptions?: RequestOptions): Promise<TriggerResult>;
20
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AutoSchedule = void 0;
4
+ const errors_js_1 = require("./errors.js");
5
+ const trigger_result_js_1 = require("./models/trigger-result.js");
6
+ /** Scheduled captures of the web pages set up in the dashboard. */
7
+ class AutoSchedule {
8
+ #config;
9
+ /** @internal Use `client.autoSchedule`. */
10
+ constructor(config) {
11
+ this.#config = config;
12
+ }
13
+ /**
14
+ * Starts a web page's capture now. The outcome arrives by webhook. Calling again while that
15
+ * capture is pending starts nothing new and answers `already_pending`.
16
+ *
17
+ * @param webPageId - The web page's id, which webhooks call `scheduleId`.
18
+ */
19
+ async triggerNow(webPageId, requestOptions = {}) {
20
+ const { apiKey, baseUrl, transport, timeout } = this.#config;
21
+ if (apiKey === undefined) {
22
+ throw new errors_js_1.AuthenticationError("No Auto Schedule API key: pass autoScheduleApiKey to the client or set WSW_AUTO_SCHEDULE_API_KEY.");
23
+ }
24
+ const wire = await transport.json({
25
+ method: "POST",
26
+ url: `${baseUrl}/auto-schedule/v1/web-pages/${encodeURIComponent(webPageId)}/trigger-now`,
27
+ apiKey,
28
+ timeout: requestOptions.timeout ?? timeout,
29
+ signal: requestOptions.signal,
30
+ });
31
+ return (0, trigger_result_js_1.parseTriggerResult)(wire);
32
+ }
33
+ }
34
+ exports.AutoSchedule = AutoSchedule;
@@ -0,0 +1,41 @@
1
+ import { AutoSchedule } from "./auto-schedule.js";
2
+ import { InstantSnap } from "./instant-snap.js";
3
+ /** Settings for {@link WebsiteScreenshotWorld}. Every one is optional. */
4
+ export interface ClientOptions {
5
+ /** Defaults to the `WSW_INSTANT_SNAP_API_KEY` environment variable. */
6
+ instantSnapApiKey?: string;
7
+ /** Defaults to the `WSW_AUTO_SCHEDULE_API_KEY` environment variable. */
8
+ autoScheduleApiKey?: string;
9
+ /** Default `https://api.websitescreenshotworld.com`. */
10
+ instantSnapBaseUrl?: string;
11
+ /** Default `https://autoapi.websitescreenshotworld.com`. */
12
+ autoScheduleBaseUrl?: string;
13
+ /**
14
+ * Milliseconds a capture call may take, retries included. Default 180 000: the API itself may
15
+ * take up to 120 seconds.
16
+ */
17
+ captureTimeout?: number;
18
+ /** Milliseconds any other call may take, retries included. Default 30 000. */
19
+ timeout?: number;
20
+ /** How many times a failed attempt is retried when retrying can help. Default 2. */
21
+ maxRetries?: number;
22
+ /** A `fetch` to send requests with, for a proxy or instrumentation. Defaults to the global one. */
23
+ fetch?: typeof fetch;
24
+ }
25
+ /**
26
+ * The Website Screenshot World client. One client covers both products; either key may be left
27
+ * out, and calling a product without its key fails before anything is sent.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const client = new WebsiteScreenshotWorld();
32
+ * const capture = await client.instantSnap.capture({ url: "https://example.com" });
33
+ * ```
34
+ */
35
+ export declare class WebsiteScreenshotWorld {
36
+ /** On-demand captures. */
37
+ readonly instantSnap: InstantSnap;
38
+ /** Captures of scheduled web pages. */
39
+ readonly autoSchedule: AutoSchedule;
40
+ constructor(options?: ClientOptions);
41
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebsiteScreenshotWorld = void 0;
4
+ const auto_schedule_js_1 = require("./auto-schedule.js");
5
+ const instant_snap_js_1 = require("./instant-snap.js");
6
+ const transport_js_1 = require("./transport.js");
7
+ const version_js_1 = require("./version.js");
8
+ const DEFAULT_INSTANT_SNAP_BASE_URL = "https://api.websitescreenshotworld.com";
9
+ const DEFAULT_AUTO_SCHEDULE_BASE_URL = "https://autoapi.websitescreenshotworld.com";
10
+ const DEFAULT_CAPTURE_TIMEOUT_MS = 180_000;
11
+ const DEFAULT_TIMEOUT_MS = 30_000;
12
+ const DEFAULT_MAX_RETRIES = 2;
13
+ /**
14
+ * The Website Screenshot World client. One client covers both products; either key may be left
15
+ * out, and calling a product without its key fails before anything is sent.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const client = new WebsiteScreenshotWorld();
20
+ * const capture = await client.instantSnap.capture({ url: "https://example.com" });
21
+ * ```
22
+ */
23
+ class WebsiteScreenshotWorld {
24
+ /** On-demand captures. */
25
+ instantSnap;
26
+ /** Captures of scheduled web pages. */
27
+ autoSchedule;
28
+ constructor(options = {}) {
29
+ const transport = new transport_js_1.Transport({
30
+ fetch: options.fetch,
31
+ maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
32
+ userAgent: `websitescreenshotworld-node/${version_js_1.VERSION} (node ${process.versions.node})`,
33
+ });
34
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
35
+ this.instantSnap = new instant_snap_js_1.InstantSnap({
36
+ transport,
37
+ apiKey: apiKey(options.instantSnapApiKey, "WSW_INSTANT_SNAP_API_KEY"),
38
+ baseUrl: withoutTrailingSlash(options.instantSnapBaseUrl ?? DEFAULT_INSTANT_SNAP_BASE_URL),
39
+ timeout,
40
+ captureTimeout: options.captureTimeout ?? DEFAULT_CAPTURE_TIMEOUT_MS,
41
+ });
42
+ this.autoSchedule = new auto_schedule_js_1.AutoSchedule({
43
+ transport,
44
+ apiKey: apiKey(options.autoScheduleApiKey, "WSW_AUTO_SCHEDULE_API_KEY"),
45
+ baseUrl: withoutTrailingSlash(options.autoScheduleBaseUrl ?? DEFAULT_AUTO_SCHEDULE_BASE_URL),
46
+ timeout,
47
+ });
48
+ }
49
+ }
50
+ exports.WebsiteScreenshotWorld = WebsiteScreenshotWorld;
51
+ /** The key given, else the environment's; an empty string counts as no key. */
52
+ function apiKey(given, variable) {
53
+ const key = given === undefined || given === "" ? process.env[variable] : given;
54
+ return key === "" ? undefined : key;
55
+ }
56
+ function withoutTrailingSlash(url) {
57
+ return url.replace(/\/+$/, "");
58
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The `error_code` tokens the APIs answer with. Switch on these, never on the HTTP status or the
3
+ * message. A code added to the API later still comes through as a plain string.
4
+ */
5
+ export declare const ErrorCode: {
6
+ readonly ApiKeyInvalid: "api_key_invalid";
7
+ readonly ValidationFailed: "validation_failed";
8
+ readonly S3DestinationInvalid: "s3_destination_invalid";
9
+ readonly WebPageIdInvalid: "web_page_id_invalid";
10
+ readonly IdempotencyKeyReused: "idempotency_key_reused";
11
+ /** The web page is a sitemap schedule, which runs only on its schedule. Not retried. */
12
+ readonly SitemapScheduleNotTriggerable: "sitemap_schedule_not_triggerable";
13
+ readonly QuotaExhausted: "quota_exhausted";
14
+ readonly FeatureNotAvailable: "feature_not_available";
15
+ readonly NoTriggerAllowance: "no_trigger_allowance";
16
+ readonly Forbidden: "forbidden";
17
+ readonly CaptureNotFound: "capture_not_found";
18
+ readonly WebPageNotFound: "web_page_not_found";
19
+ readonly NotFound: "not_found";
20
+ readonly RateLimited: "rate_limited";
21
+ /** Too many async captures are already waiting or running. Not retried. */
22
+ readonly QueueFull: "queue_full";
23
+ readonly CaptureFailed: "capture_failed";
24
+ readonly CaptureTimeout: "capture_timeout";
25
+ readonly PayloadTooLarge: "payload_too_large";
26
+ /** An async capture still waited for a browser 15 minutes after it was accepted. Not charged. */
27
+ readonly CaptureExpired: "capture_expired";
28
+ readonly CaptureBusy: "capture_busy";
29
+ readonly ServiceUnavailable: "service_unavailable";
30
+ readonly InternalError: "internal_error";
31
+ readonly RequestCancelled: "request_cancelled";
32
+ readonly IdempotencyKeyInUse: "idempotency_key_in_use";
33
+ readonly UnknownError: "unknown_error";
34
+ };
35
+ export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode] | (string & {});
36
+ /** The kind of error each known code raises. Codes not listed raise the base error. */
37
+ export declare const ERROR_KINDS: {
38
+ readonly authentication: readonly ["api_key_invalid"];
39
+ readonly invalid_request: readonly ["validation_failed", "s3_destination_invalid", "web_page_id_invalid", "idempotency_key_reused", "sitemap_schedule_not_triggerable"];
40
+ readonly plan_limit: readonly ["quota_exhausted", "feature_not_available", "no_trigger_allowance", "forbidden"];
41
+ readonly not_found: readonly ["capture_not_found", "web_page_not_found", "not_found"];
42
+ readonly rate_limit: readonly ["rate_limited", "queue_full"];
43
+ readonly capture: readonly ["capture_failed", "capture_timeout", "payload_too_large", "capture_expired"];
44
+ readonly service: readonly ["capture_busy", "service_unavailable", "internal_error", "request_cancelled", "idempotency_key_in_use"];
45
+ };
46
+ export type ErrorKind = keyof typeof ERROR_KINDS;
47
+ /**
48
+ * Codes that are worth sending the same request again for. `queue_full` is not one: the async queue
49
+ * stays full until captures finish, so it is raised for the caller with `retryAfterSeconds`.
50
+ */
51
+ export declare const RETRYABLE_CODES: ReadonlySet<string>;
52
+ /** The code for a refusal that came without a JSON error body, usually from a proxy. */
53
+ export declare const FALLBACK_CODE_BY_STATUS: Readonly<Record<number, string>>;
54
+ export declare const FALLBACK_CODE_FOR_OTHER_STATUS = "unknown_error";