@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.
- package/CHANGELOG.md +37 -0
- package/LICENSE +21 -0
- package/README.md +399 -2
- package/dist/cjs/auto-schedule.d.ts +20 -0
- package/dist/cjs/auto-schedule.js +34 -0
- package/dist/cjs/client.d.ts +41 -0
- package/dist/cjs/client.js +58 -0
- package/dist/cjs/error-codes.d.ts +54 -0
- package/dist/cjs/error-codes.js +94 -0
- package/dist/cjs/errors.d.ts +56 -0
- package/dist/cjs/errors.js +112 -0
- package/dist/cjs/files.d.ts +6 -0
- package/dist/cjs/files.js +45 -0
- package/dist/cjs/index.d.ts +16 -0
- package/dist/cjs/index.js +37 -0
- package/dist/cjs/instant-snap.d.ts +99 -0
- package/dist/cjs/instant-snap.js +205 -0
- package/dist/cjs/models/capture-file.d.ts +35 -0
- package/dist/cjs/models/capture-file.js +34 -0
- package/dist/cjs/models/capture-options.d.ts +144 -0
- package/dist/cjs/models/capture-options.js +2 -0
- package/dist/cjs/models/capture.d.ts +108 -0
- package/dist/cjs/models/capture.js +63 -0
- package/dist/cjs/models/defaults.d.ts +82 -0
- package/dist/cjs/models/defaults.js +46 -0
- package/dist/cjs/models/tokens.d.ts +92 -0
- package/dist/cjs/models/tokens.js +85 -0
- package/dist/cjs/models/trigger-result.d.ts +20 -0
- package/dist/cjs/models/trigger-result.js +13 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/transport.d.ts +55 -0
- package/dist/cjs/transport.js +206 -0
- package/dist/cjs/version.d.ts +2 -0
- package/dist/cjs/version.js +5 -0
- package/dist/cjs/webhooks/events.d.ts +128 -0
- package/dist/cjs/webhooks/events.js +13 -0
- package/dist/cjs/webhooks/parse-event.d.ts +10 -0
- package/dist/cjs/webhooks/parse-event.js +149 -0
- package/dist/cjs/webhooks/webhook.d.ts +26 -0
- package/dist/cjs/webhooks/webhook.js +78 -0
- package/dist/cjs/wire.d.ts +15 -0
- package/dist/cjs/wire.js +71 -0
- package/dist/esm/auto-schedule.d.ts +20 -0
- package/dist/esm/auto-schedule.js +30 -0
- package/dist/esm/client.d.ts +41 -0
- package/dist/esm/client.js +54 -0
- package/dist/esm/error-codes.d.ts +54 -0
- package/dist/esm/error-codes.js +91 -0
- package/dist/esm/errors.d.ts +56 -0
- package/dist/esm/errors.js +98 -0
- package/dist/esm/files.d.ts +6 -0
- package/dist/esm/files.js +41 -0
- package/dist/esm/index.d.ts +16 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/instant-snap.d.ts +99 -0
- package/dist/esm/instant-snap.js +201 -0
- package/dist/esm/models/capture-file.d.ts +35 -0
- package/dist/esm/models/capture-file.js +31 -0
- package/dist/esm/models/capture-options.d.ts +144 -0
- package/dist/esm/models/capture-options.js +1 -0
- package/dist/esm/models/capture.d.ts +108 -0
- package/dist/esm/models/capture.js +57 -0
- package/dist/esm/models/defaults.d.ts +82 -0
- package/dist/esm/models/defaults.js +42 -0
- package/dist/esm/models/tokens.d.ts +92 -0
- package/dist/esm/models/tokens.js +82 -0
- package/dist/esm/models/trigger-result.d.ts +20 -0
- package/dist/esm/models/trigger-result.js +10 -0
- package/dist/esm/transport.d.ts +55 -0
- package/dist/esm/transport.js +201 -0
- package/dist/esm/version.d.ts +2 -0
- package/dist/esm/version.js +2 -0
- package/dist/esm/webhooks/events.d.ts +128 -0
- package/dist/esm/webhooks/events.js +10 -0
- package/dist/esm/webhooks/parse-event.d.ts +10 -0
- package/dist/esm/webhooks/parse-event.js +146 -0
- package/dist/esm/webhooks/webhook.d.ts +26 -0
- package/dist/esm/webhooks/webhook.js +75 -0
- package/dist/esm/wire.d.ts +15 -0
- package/dist/esm/wire.js +66 -0
- package/package.json +68 -4
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { WebhookEvent } from "./events.js";
|
|
2
|
+
export interface WireEnvelope {
|
|
3
|
+
id: string;
|
|
4
|
+
type: string;
|
|
5
|
+
created_at: string;
|
|
6
|
+
product: string;
|
|
7
|
+
data: unknown;
|
|
8
|
+
}
|
|
9
|
+
/** Turns a verified webhook body into its typed event. */
|
|
10
|
+
export declare function parseEvent(wire: WireEnvelope): WebhookEvent;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseEvent = parseEvent;
|
|
4
|
+
const capture_js_1 = require("../models/capture.js");
|
|
5
|
+
const wire_js_1 = require("../wire.js");
|
|
6
|
+
/** Turns a verified webhook body into its typed event. */
|
|
7
|
+
function parseEvent(wire) {
|
|
8
|
+
const { type, product, data } = wire;
|
|
9
|
+
const envelope = { id: wire.id, createdAt: (0, wire_js_1.parseDate)(wire.created_at) };
|
|
10
|
+
if (type === "capture.succeeded" || type === "capture.failed") {
|
|
11
|
+
if (product === "instant_snap") {
|
|
12
|
+
const capture = instantSnapCapture(data);
|
|
13
|
+
return { ...envelope, type, product, data: capture };
|
|
14
|
+
}
|
|
15
|
+
if (product === "auto_schedule") {
|
|
16
|
+
const capture = scheduledCapture(data);
|
|
17
|
+
return { ...envelope, type, product, data: capture };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
switch (type) {
|
|
21
|
+
case "delivery.completed":
|
|
22
|
+
return {
|
|
23
|
+
...envelope,
|
|
24
|
+
type,
|
|
25
|
+
product: "auto_schedule",
|
|
26
|
+
data: deliveryCompleted(data),
|
|
27
|
+
};
|
|
28
|
+
case "capture.changed":
|
|
29
|
+
return {
|
|
30
|
+
...envelope,
|
|
31
|
+
type,
|
|
32
|
+
product: "auto_schedule",
|
|
33
|
+
data: captureChanged(data),
|
|
34
|
+
};
|
|
35
|
+
case "schedule.paused":
|
|
36
|
+
return {
|
|
37
|
+
...envelope,
|
|
38
|
+
type,
|
|
39
|
+
product: "auto_schedule",
|
|
40
|
+
data: schedulePaused(data),
|
|
41
|
+
};
|
|
42
|
+
case "sitemap_run.completed":
|
|
43
|
+
return {
|
|
44
|
+
...envelope,
|
|
45
|
+
type,
|
|
46
|
+
product: "auto_schedule",
|
|
47
|
+
data: sitemapRunCompleted(data),
|
|
48
|
+
};
|
|
49
|
+
case "ping":
|
|
50
|
+
return { ...envelope, type, product, data: ping(data) };
|
|
51
|
+
default:
|
|
52
|
+
// Includes a known capture type from a product this version does not know.
|
|
53
|
+
return {
|
|
54
|
+
...envelope,
|
|
55
|
+
type,
|
|
56
|
+
product,
|
|
57
|
+
rawData: data,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function instantSnapCapture(wire) {
|
|
62
|
+
return {
|
|
63
|
+
...(0, capture_js_1.parseCaptureFields)(wire),
|
|
64
|
+
pageSource: wire.page_source
|
|
65
|
+
? (0, capture_js_1.parsePageSourceFields)(wire.page_source)
|
|
66
|
+
: null,
|
|
67
|
+
s3Delivery: wire.s3_delivery
|
|
68
|
+
? (0, capture_js_1.parseS3DeliveryFields)(wire.s3_delivery)
|
|
69
|
+
: null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function scheduledCapture(wire) {
|
|
73
|
+
return {
|
|
74
|
+
id: wire.id,
|
|
75
|
+
scheduleId: wire.schedule_id,
|
|
76
|
+
scheduleName: wire.schedule_name,
|
|
77
|
+
url: wire.url,
|
|
78
|
+
status: wire.status,
|
|
79
|
+
mediaCategory: wire.media_category,
|
|
80
|
+
format: wire.format ?? null,
|
|
81
|
+
pageHttpStatus: wire.page_http_status ?? null,
|
|
82
|
+
capturedAt: (0, wire_js_1.parseDate)(wire.captured_at),
|
|
83
|
+
finalUrl: wire.final_url ?? null,
|
|
84
|
+
mediaSha256: wire.media_sha256 ?? null,
|
|
85
|
+
pageSource: wire.page_source
|
|
86
|
+
? {
|
|
87
|
+
format: wire.page_source.format,
|
|
88
|
+
sha256: wire.page_source.sha256 ?? null,
|
|
89
|
+
}
|
|
90
|
+
: null,
|
|
91
|
+
sitemapRunId: wire.sitemap_run_id ?? null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function deliveryCompleted(wire) {
|
|
95
|
+
return {
|
|
96
|
+
...scheduledCapture(wire),
|
|
97
|
+
destinations: (wire.destinations ?? []).map((destination) => ({
|
|
98
|
+
slot: destination.slot,
|
|
99
|
+
name: destination.name,
|
|
100
|
+
provider: destination.provider,
|
|
101
|
+
status: destination.status,
|
|
102
|
+
completedAt: (0, wire_js_1.parseDate)(destination.completed_at),
|
|
103
|
+
})),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function captureChanged(wire) {
|
|
107
|
+
return {
|
|
108
|
+
...scheduledCapture(wire),
|
|
109
|
+
changePercent: wire.change_percent ?? null,
|
|
110
|
+
comparedToCaptureId: wire.compared_to_capture_id ?? null,
|
|
111
|
+
changeImageAvailable: wire.change_image_available ?? false,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function schedulePaused(wire) {
|
|
115
|
+
return {
|
|
116
|
+
scheduleId: wire.schedule_id,
|
|
117
|
+
scheduleName: wire.schedule_name,
|
|
118
|
+
url: wire.url,
|
|
119
|
+
reason: wire.reason,
|
|
120
|
+
pausedAt: (0, wire_js_1.parseDate)(wire.paused_at),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function sitemapRunCompleted(wire) {
|
|
124
|
+
return {
|
|
125
|
+
sitemapRunId: wire.sitemap_run_id,
|
|
126
|
+
scheduleId: wire.schedule_id,
|
|
127
|
+
scheduleName: wire.schedule_name,
|
|
128
|
+
sitemapUrl: wire.sitemap_url,
|
|
129
|
+
status: wire.status,
|
|
130
|
+
runAt: (0, wire_js_1.parseDate)(wire.run_at),
|
|
131
|
+
completedAt: (0, wire_js_1.parseDate)(wire.completed_at),
|
|
132
|
+
pagesListed: wire.pages_listed ?? null,
|
|
133
|
+
pagesMatched: wire.pages_matched ?? null,
|
|
134
|
+
pagesQueued: wire.pages_queued ?? null,
|
|
135
|
+
pagesSucceeded: wire.pages_succeeded ?? null,
|
|
136
|
+
pagesFailed: wire.pages_failed ?? null,
|
|
137
|
+
pagesOverLimit: wire.pages_over_limit ?? null,
|
|
138
|
+
pagesOverQuota: wire.pages_over_quota ?? null,
|
|
139
|
+
message: wire.message ?? null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function ping(wire) {
|
|
143
|
+
return {
|
|
144
|
+
endpointId: wire.endpoint_id,
|
|
145
|
+
endpointName: wire.endpoint_name,
|
|
146
|
+
message: wire.message,
|
|
147
|
+
sentAt: (0, wire_js_1.parseDate)(wire.sent_at),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { WebhookEvent } from "./events.js";
|
|
2
|
+
/** Options for {@link Webhook.verify}. */
|
|
3
|
+
export interface VerifyOptions {
|
|
4
|
+
/** How far the signature's timestamp may be from now, in seconds. Default 300. */
|
|
5
|
+
tolerance?: number;
|
|
6
|
+
/** The time to check the timestamp against. Defaults to the current time. */
|
|
7
|
+
now?: Date;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Checks a webhook delivery's `X-WSW-Signature` header against its raw body and returns the event.
|
|
11
|
+
*
|
|
12
|
+
* Pass the body exactly as received. A framework that parses the JSON and serialises it again
|
|
13
|
+
* changes the bytes, and the signature no longer matches.
|
|
14
|
+
*
|
|
15
|
+
* @param payload - The raw request body.
|
|
16
|
+
* @param signatureHeader - The `X-WSW-Signature` header.
|
|
17
|
+
* @param secret - The endpoint's signing secret, starting `whsec_`.
|
|
18
|
+
* @throws {@link SignatureVerificationError} when the header is missing or malformed, the timestamp
|
|
19
|
+
* is outside the tolerance, or no signature matches.
|
|
20
|
+
*/
|
|
21
|
+
declare function verify(payload: string | Uint8Array, signatureHeader: string | readonly string[] | null | undefined, secret: string, options?: VerifyOptions): WebhookEvent;
|
|
22
|
+
/** Verifies webhook deliveries from both products. */
|
|
23
|
+
export declare const Webhook: {
|
|
24
|
+
readonly verify: typeof verify;
|
|
25
|
+
};
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Webhook = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const errors_js_1 = require("../errors.js");
|
|
6
|
+
const parse_event_js_1 = require("./parse-event.js");
|
|
7
|
+
const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
8
|
+
const SIGNATURE_HEX = /^[0-9a-f]{64}$/;
|
|
9
|
+
/**
|
|
10
|
+
* Checks a webhook delivery's `X-WSW-Signature` header against its raw body and returns the event.
|
|
11
|
+
*
|
|
12
|
+
* Pass the body exactly as received. A framework that parses the JSON and serialises it again
|
|
13
|
+
* changes the bytes, and the signature no longer matches.
|
|
14
|
+
*
|
|
15
|
+
* @param payload - The raw request body.
|
|
16
|
+
* @param signatureHeader - The `X-WSW-Signature` header.
|
|
17
|
+
* @param secret - The endpoint's signing secret, starting `whsec_`.
|
|
18
|
+
* @throws {@link SignatureVerificationError} when the header is missing or malformed, the timestamp
|
|
19
|
+
* is outside the tolerance, or no signature matches.
|
|
20
|
+
*/
|
|
21
|
+
function verify(payload, signatureHeader, secret, options = {}) {
|
|
22
|
+
const header = parseSignatureHeader(typeof signatureHeader === "string"
|
|
23
|
+
? signatureHeader
|
|
24
|
+
: (signatureHeader ?? []).join(","));
|
|
25
|
+
if (header.timestamp === null) {
|
|
26
|
+
throw new errors_js_1.SignatureVerificationError("The signature header has no valid timestamp.");
|
|
27
|
+
}
|
|
28
|
+
if (header.signatures.length === 0) {
|
|
29
|
+
throw new errors_js_1.SignatureVerificationError("The signature header has no v1 signature.");
|
|
30
|
+
}
|
|
31
|
+
const tolerance = options.tolerance ?? DEFAULT_TOLERANCE_SECONDS;
|
|
32
|
+
const now = Math.floor((options.now ?? new Date()).getTime() / 1000);
|
|
33
|
+
if (Math.abs(now - header.timestamp) > tolerance) {
|
|
34
|
+
throw new errors_js_1.SignatureVerificationError("The signature's timestamp is outside the tolerance.");
|
|
35
|
+
}
|
|
36
|
+
const body = typeof payload === "string" ? Buffer.from(payload, "utf8") : payload;
|
|
37
|
+
const expected = Buffer.from((0, node_crypto_1.createHmac)("sha256", Buffer.from(secret, "utf8"))
|
|
38
|
+
.update(`${String(header.timestamp)}.`, "utf8")
|
|
39
|
+
.update(body)
|
|
40
|
+
.digest("hex"), "utf8");
|
|
41
|
+
const matches = header.signatures.some((signature) => (0, node_crypto_1.timingSafeEqual)(expected, Buffer.from(signature, "utf8")));
|
|
42
|
+
if (!matches) {
|
|
43
|
+
throw new errors_js_1.SignatureVerificationError("No signature in the header matches the body.");
|
|
44
|
+
}
|
|
45
|
+
return (0, parse_event_js_1.parseEvent)(parseBody(new TextDecoder().decode(body)));
|
|
46
|
+
}
|
|
47
|
+
function parseSignatureHeader(value) {
|
|
48
|
+
const header = { timestamp: null, signatures: [] };
|
|
49
|
+
let sawTimestamp = false;
|
|
50
|
+
for (const part of value.split(",")) {
|
|
51
|
+
const separator = part.indexOf("=");
|
|
52
|
+
if (separator === -1)
|
|
53
|
+
continue;
|
|
54
|
+
const name = part.slice(0, separator).trim();
|
|
55
|
+
const content = part.slice(separator + 1).trim();
|
|
56
|
+
if (name === "t" && !sawTimestamp) {
|
|
57
|
+
sawTimestamp = true;
|
|
58
|
+
header.timestamp = /^\d+$/.test(content) ? Number(content) : null;
|
|
59
|
+
}
|
|
60
|
+
else if (name === "v1") {
|
|
61
|
+
const signature = content.toLowerCase();
|
|
62
|
+
// A value that cannot be a digest is skipped so the comparison always sees equal lengths.
|
|
63
|
+
if (SIGNATURE_HEX.test(signature))
|
|
64
|
+
header.signatures.push(signature);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return header;
|
|
68
|
+
}
|
|
69
|
+
function parseBody(text) {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(text);
|
|
72
|
+
}
|
|
73
|
+
catch (cause) {
|
|
74
|
+
throw new errors_js_1.WebsiteScreenshotWorldError("The webhook body is signed but is not JSON.", { cause });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Verifies webhook deliveries from both products. */
|
|
78
|
+
exports.Webhook = { verify };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** A JSON object as the API sends or receives it, with snake_case keys. */
|
|
2
|
+
export type WireObject = Record<string, unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* Turns camelCase options into the API's snake_case body. Options left `undefined` or `null` are not
|
|
5
|
+
* sent; `false`, `0` and `""` are.
|
|
6
|
+
*/
|
|
7
|
+
export declare function toWire(options: object): WireObject;
|
|
8
|
+
/** Re-cases the keys of an object the library does not model field by field. */
|
|
9
|
+
export declare function camelCaseKeys(value: unknown): unknown;
|
|
10
|
+
/**
|
|
11
|
+
* Parses an ISO 8601 date-time with any number of fraction digits and either `Z` or an offset.
|
|
12
|
+
* Returns `null` for `null` or a missing value.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseDate(value: string): Date;
|
|
15
|
+
export declare function parseDate(value: string | null | undefined): Date | null;
|
package/dist/cjs/wire.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toWire = toWire;
|
|
4
|
+
exports.camelCaseKeys = camelCaseKeys;
|
|
5
|
+
exports.parseDate = parseDate;
|
|
6
|
+
/** Maps whose keys are data, not field names, and so are never re-cased. */
|
|
7
|
+
const VERBATIM_KEYS = new Set(["headers", "cookies"]);
|
|
8
|
+
function isPlainObject(value) {
|
|
9
|
+
return (typeof value === "object" &&
|
|
10
|
+
value !== null &&
|
|
11
|
+
!Array.isArray(value) &&
|
|
12
|
+
!(value instanceof Date));
|
|
13
|
+
}
|
|
14
|
+
function snakeCase(key) {
|
|
15
|
+
return key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
16
|
+
}
|
|
17
|
+
function camelCase(key) {
|
|
18
|
+
return key.replace(/_([a-z0-9])/g, (_, letter) => letter.toUpperCase());
|
|
19
|
+
}
|
|
20
|
+
function withoutUnset(map) {
|
|
21
|
+
return Object.fromEntries(Object.entries(map).filter(([, value]) => value !== undefined));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Turns camelCase options into the API's snake_case body. Options left `undefined` or `null` are not
|
|
25
|
+
* sent; `false`, `0` and `""` are.
|
|
26
|
+
*/
|
|
27
|
+
function toWire(options) {
|
|
28
|
+
const body = {};
|
|
29
|
+
for (const [key, value] of Object.entries(options)) {
|
|
30
|
+
if (value === undefined || value === null)
|
|
31
|
+
continue;
|
|
32
|
+
if (VERBATIM_KEYS.has(key) && isPlainObject(value)) {
|
|
33
|
+
body[snakeCase(key)] = withoutUnset(value);
|
|
34
|
+
}
|
|
35
|
+
else if (isPlainObject(value)) {
|
|
36
|
+
body[snakeCase(key)] = toWire(value);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
body[snakeCase(key)] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return body;
|
|
43
|
+
}
|
|
44
|
+
/** Re-cases the keys of an object the library does not model field by field. */
|
|
45
|
+
function camelCaseKeys(value) {
|
|
46
|
+
if (Array.isArray(value))
|
|
47
|
+
return value.map(camelCaseKeys);
|
|
48
|
+
if (!isPlainObject(value))
|
|
49
|
+
return value;
|
|
50
|
+
return Object.fromEntries(Object.entries(value).map(([key, inner]) => [
|
|
51
|
+
camelCase(key),
|
|
52
|
+
camelCaseKeys(inner),
|
|
53
|
+
]));
|
|
54
|
+
}
|
|
55
|
+
const ISO_DATE_TIME = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})(?:[.,](\d+))?(Z|[+-]\d{2}(?::?\d{2})?)$/i;
|
|
56
|
+
function parseDate(value) {
|
|
57
|
+
if (value === null || value === undefined)
|
|
58
|
+
return null;
|
|
59
|
+
const match = ISO_DATE_TIME.exec(value);
|
|
60
|
+
if (match === null)
|
|
61
|
+
return new Date(value);
|
|
62
|
+
const [, date = "", time = "", fraction = "", zone = ""] = match;
|
|
63
|
+
const milliseconds = fraction.padEnd(3, "0").slice(0, 3);
|
|
64
|
+
return new Date(`${date}T${time}.${milliseconds}${normalizeZone(zone.toUpperCase())}`);
|
|
65
|
+
}
|
|
66
|
+
function normalizeZone(zone) {
|
|
67
|
+
if (zone === "Z")
|
|
68
|
+
return zone;
|
|
69
|
+
const digits = zone.slice(1).replace(":", "");
|
|
70
|
+
return `${zone[0] ?? "+"}${digits.slice(0, 2)}:${digits.slice(2, 4).padEnd(2, "0")}`;
|
|
71
|
+
}
|
|
@@ -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,30 @@
|
|
|
1
|
+
import { AuthenticationError } from "./errors.js";
|
|
2
|
+
import { parseTriggerResult, } from "./models/trigger-result.js";
|
|
3
|
+
/** Scheduled captures of the web pages set up in the dashboard. */
|
|
4
|
+
export class AutoSchedule {
|
|
5
|
+
#config;
|
|
6
|
+
/** @internal Use `client.autoSchedule`. */
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.#config = config;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Starts a web page's capture now. The outcome arrives by webhook. Calling again while that
|
|
12
|
+
* capture is pending starts nothing new and answers `already_pending`.
|
|
13
|
+
*
|
|
14
|
+
* @param webPageId - The web page's id, which webhooks call `scheduleId`.
|
|
15
|
+
*/
|
|
16
|
+
async triggerNow(webPageId, requestOptions = {}) {
|
|
17
|
+
const { apiKey, baseUrl, transport, timeout } = this.#config;
|
|
18
|
+
if (apiKey === undefined) {
|
|
19
|
+
throw new AuthenticationError("No Auto Schedule API key: pass autoScheduleApiKey to the client or set WSW_AUTO_SCHEDULE_API_KEY.");
|
|
20
|
+
}
|
|
21
|
+
const wire = await transport.json({
|
|
22
|
+
method: "POST",
|
|
23
|
+
url: `${baseUrl}/auto-schedule/v1/web-pages/${encodeURIComponent(webPageId)}/trigger-now`,
|
|
24
|
+
apiKey,
|
|
25
|
+
timeout: requestOptions.timeout ?? timeout,
|
|
26
|
+
signal: requestOptions.signal,
|
|
27
|
+
});
|
|
28
|
+
return parseTriggerResult(wire);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -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,54 @@
|
|
|
1
|
+
import { AutoSchedule } from "./auto-schedule.js";
|
|
2
|
+
import { InstantSnap } from "./instant-snap.js";
|
|
3
|
+
import { Transport } from "./transport.js";
|
|
4
|
+
import { VERSION } from "./version.js";
|
|
5
|
+
const DEFAULT_INSTANT_SNAP_BASE_URL = "https://api.websitescreenshotworld.com";
|
|
6
|
+
const DEFAULT_AUTO_SCHEDULE_BASE_URL = "https://autoapi.websitescreenshotworld.com";
|
|
7
|
+
const DEFAULT_CAPTURE_TIMEOUT_MS = 180_000;
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
9
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
10
|
+
/**
|
|
11
|
+
* The Website Screenshot World client. One client covers both products; either key may be left
|
|
12
|
+
* out, and calling a product without its key fails before anything is sent.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const client = new WebsiteScreenshotWorld();
|
|
17
|
+
* const capture = await client.instantSnap.capture({ url: "https://example.com" });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export class WebsiteScreenshotWorld {
|
|
21
|
+
/** On-demand captures. */
|
|
22
|
+
instantSnap;
|
|
23
|
+
/** Captures of scheduled web pages. */
|
|
24
|
+
autoSchedule;
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
const transport = new Transport({
|
|
27
|
+
fetch: options.fetch,
|
|
28
|
+
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
29
|
+
userAgent: `websitescreenshotworld-node/${VERSION} (node ${process.versions.node})`,
|
|
30
|
+
});
|
|
31
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
32
|
+
this.instantSnap = new InstantSnap({
|
|
33
|
+
transport,
|
|
34
|
+
apiKey: apiKey(options.instantSnapApiKey, "WSW_INSTANT_SNAP_API_KEY"),
|
|
35
|
+
baseUrl: withoutTrailingSlash(options.instantSnapBaseUrl ?? DEFAULT_INSTANT_SNAP_BASE_URL),
|
|
36
|
+
timeout,
|
|
37
|
+
captureTimeout: options.captureTimeout ?? DEFAULT_CAPTURE_TIMEOUT_MS,
|
|
38
|
+
});
|
|
39
|
+
this.autoSchedule = new AutoSchedule({
|
|
40
|
+
transport,
|
|
41
|
+
apiKey: apiKey(options.autoScheduleApiKey, "WSW_AUTO_SCHEDULE_API_KEY"),
|
|
42
|
+
baseUrl: withoutTrailingSlash(options.autoScheduleBaseUrl ?? DEFAULT_AUTO_SCHEDULE_BASE_URL),
|
|
43
|
+
timeout,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** The key given, else the environment's; an empty string counts as no key. */
|
|
48
|
+
function apiKey(given, variable) {
|
|
49
|
+
const key = given === undefined || given === "" ? process.env[variable] : given;
|
|
50
|
+
return key === "" ? undefined : key;
|
|
51
|
+
}
|
|
52
|
+
function withoutTrailingSlash(url) {
|
|
53
|
+
return url.replace(/\/+$/, "");
|
|
54
|
+
}
|
|
@@ -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";
|
|
@@ -0,0 +1,91 @@
|
|
|
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 const ErrorCode = {
|
|
6
|
+
ApiKeyInvalid: "api_key_invalid",
|
|
7
|
+
ValidationFailed: "validation_failed",
|
|
8
|
+
S3DestinationInvalid: "s3_destination_invalid",
|
|
9
|
+
WebPageIdInvalid: "web_page_id_invalid",
|
|
10
|
+
IdempotencyKeyReused: "idempotency_key_reused",
|
|
11
|
+
/** The web page is a sitemap schedule, which runs only on its schedule. Not retried. */
|
|
12
|
+
SitemapScheduleNotTriggerable: "sitemap_schedule_not_triggerable",
|
|
13
|
+
QuotaExhausted: "quota_exhausted",
|
|
14
|
+
FeatureNotAvailable: "feature_not_available",
|
|
15
|
+
NoTriggerAllowance: "no_trigger_allowance",
|
|
16
|
+
Forbidden: "forbidden",
|
|
17
|
+
CaptureNotFound: "capture_not_found",
|
|
18
|
+
WebPageNotFound: "web_page_not_found",
|
|
19
|
+
NotFound: "not_found",
|
|
20
|
+
RateLimited: "rate_limited",
|
|
21
|
+
/** Too many async captures are already waiting or running. Not retried. */
|
|
22
|
+
QueueFull: "queue_full",
|
|
23
|
+
CaptureFailed: "capture_failed",
|
|
24
|
+
CaptureTimeout: "capture_timeout",
|
|
25
|
+
PayloadTooLarge: "payload_too_large",
|
|
26
|
+
/** An async capture still waited for a browser 15 minutes after it was accepted. Not charged. */
|
|
27
|
+
CaptureExpired: "capture_expired",
|
|
28
|
+
CaptureBusy: "capture_busy",
|
|
29
|
+
ServiceUnavailable: "service_unavailable",
|
|
30
|
+
InternalError: "internal_error",
|
|
31
|
+
RequestCancelled: "request_cancelled",
|
|
32
|
+
IdempotencyKeyInUse: "idempotency_key_in_use",
|
|
33
|
+
UnknownError: "unknown_error",
|
|
34
|
+
};
|
|
35
|
+
/** The kind of error each known code raises. Codes not listed raise the base error. */
|
|
36
|
+
export const ERROR_KINDS = {
|
|
37
|
+
authentication: ["api_key_invalid"],
|
|
38
|
+
invalid_request: [
|
|
39
|
+
"validation_failed",
|
|
40
|
+
"s3_destination_invalid",
|
|
41
|
+
"web_page_id_invalid",
|
|
42
|
+
"idempotency_key_reused",
|
|
43
|
+
"sitemap_schedule_not_triggerable",
|
|
44
|
+
],
|
|
45
|
+
plan_limit: [
|
|
46
|
+
"quota_exhausted",
|
|
47
|
+
"feature_not_available",
|
|
48
|
+
"no_trigger_allowance",
|
|
49
|
+
"forbidden",
|
|
50
|
+
],
|
|
51
|
+
not_found: ["capture_not_found", "web_page_not_found", "not_found"],
|
|
52
|
+
rate_limit: ["rate_limited", "queue_full"],
|
|
53
|
+
capture: [
|
|
54
|
+
"capture_failed",
|
|
55
|
+
"capture_timeout",
|
|
56
|
+
"payload_too_large",
|
|
57
|
+
"capture_expired",
|
|
58
|
+
],
|
|
59
|
+
service: [
|
|
60
|
+
"capture_busy",
|
|
61
|
+
"service_unavailable",
|
|
62
|
+
"internal_error",
|
|
63
|
+
"request_cancelled",
|
|
64
|
+
"idempotency_key_in_use",
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Codes that are worth sending the same request again for. `queue_full` is not one: the async queue
|
|
69
|
+
* stays full until captures finish, so it is raised for the caller with `retryAfterSeconds`.
|
|
70
|
+
*/
|
|
71
|
+
export const RETRYABLE_CODES = new Set([
|
|
72
|
+
"rate_limited",
|
|
73
|
+
"capture_busy",
|
|
74
|
+
"service_unavailable",
|
|
75
|
+
"idempotency_key_in_use",
|
|
76
|
+
]);
|
|
77
|
+
/** The code for a refusal that came without a JSON error body, usually from a proxy. */
|
|
78
|
+
export const FALLBACK_CODE_BY_STATUS = {
|
|
79
|
+
400: "validation_failed",
|
|
80
|
+
401: "api_key_invalid",
|
|
81
|
+
403: "forbidden",
|
|
82
|
+
404: "not_found",
|
|
83
|
+
409: "idempotency_key_in_use",
|
|
84
|
+
413: "payload_too_large",
|
|
85
|
+
429: "rate_limited",
|
|
86
|
+
500: "internal_error",
|
|
87
|
+
502: "service_unavailable",
|
|
88
|
+
503: "service_unavailable",
|
|
89
|
+
504: "service_unavailable",
|
|
90
|
+
};
|
|
91
|
+
export const FALLBACK_CODE_FOR_OTHER_STATUS = "unknown_error";
|