@scanverra/sdk 0.1.0 → 0.1.2

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 CHANGED
@@ -30,7 +30,29 @@ const ok = await verifyWebhook({
30
30
  });
31
31
  ```
32
32
 
33
- Errors throw `ScanverraError` with `status`, `code` and the response `body`. A scan refused for quota answers 402 with `code` set.
33
+ ## Errors
34
+
35
+ Every failure throws `ScanverraError`:
36
+
37
+ - `status`: the HTTP status, or `0` when no answer arrived.
38
+ - `code`: a stable value to branch on, such as `free_limit_reached` (402), `SCAN_COOLDOWN` (429), `network_error` or `timeout`. The full list is at https://www.scanverra.com/docs/errors.
39
+ - `message`: a sentence you can show to people.
40
+ - `body`: the response as sent; `cause`: the original error for `network_error` and `timeout`.
41
+
42
+ ```ts
43
+ import { ScanverraError } from "@scanverra/sdk";
44
+
45
+ try {
46
+ await scanverra.scans.audit({ url: "https://example.com" });
47
+ } catch (e) {
48
+ if (!(e instanceof ScanverraError)) throw e;
49
+ if (e.code === "SCAN_COOLDOWN") console.log("Try again later:", e.message);
50
+ else if (e.status === 0 || e.status >= 500) console.log("Retry with backoff:", e.code ?? e.status);
51
+ else throw e;
52
+ }
53
+ ```
54
+
55
+ The key is sent in the `X-API-Key` header. Requests time out after `timeoutMs` (default 300000, since the Website Audit answers when the scan is done).
34
56
 
35
57
  Events: `scan.score_dropped`, `scan.policy_failed`, `scan.completed`, `payment_page.scripts_changed`, `attack_surface.changed`, `remediation.task_created`, `remediation.task_resolved`, `property.score_changed`. Payloads are described at https://www.scanverra.com/docs/integrations.
36
58
 
package/dist/index.d.ts CHANGED
@@ -7,11 +7,18 @@ export interface ScanverraOptions {
7
7
  timeoutMs?: number;
8
8
  fetch?: typeof fetch;
9
9
  }
10
+ /**
11
+ * Every failure the client raises. `status` is the HTTP status, or 0 when no
12
+ * answer arrived: then `code` is "network_error" or "timeout" and `cause`
13
+ * holds the original error. API codes are listed at /docs/errors.
14
+ */
10
15
  export declare class ScanverraError extends Error {
11
16
  status: number;
12
17
  code: string | undefined;
13
18
  body: unknown;
14
- constructor(status: number, code: string | undefined, message: string, body: unknown);
19
+ constructor(status: number, code: string | undefined, message: string, body: unknown, options?: {
20
+ cause?: unknown;
21
+ });
15
22
  }
16
23
  /** A scan as the result endpoints return it; fields differ per tool (see /docs/api). */
17
24
  export interface ScanResult {
package/dist/index.js CHANGED
@@ -1,18 +1,24 @@
1
1
  // @scanverra/sdk: a small typed client for the Scanverra API. No dependencies:
2
2
  // it uses the global fetch (Node 18+, Deno, Bun, browsers for public calls)
3
3
  // and Web Crypto for webhook signatures.
4
+ /**
5
+ * Every failure the client raises. `status` is the HTTP status, or 0 when no
6
+ * answer arrived: then `code` is "network_error" or "timeout" and `cause`
7
+ * holds the original error. API codes are listed at /docs/errors.
8
+ */
4
9
  export class ScanverraError extends Error {
5
10
  status;
6
11
  code;
7
12
  body;
8
- constructor(status, code, message, body) {
9
- super(message);
13
+ constructor(status, code, message, body, options) {
14
+ super(message, options);
10
15
  this.status = status;
11
16
  this.code = code;
12
17
  this.body = body;
13
18
  this.name = "ScanverraError";
14
19
  }
15
20
  }
21
+ const isTimeout = (e) => e instanceof Error && (e.name === "TimeoutError" || e.name === "AbortError");
16
22
  export class Scanverra {
17
23
  options;
18
24
  baseUrl;
@@ -25,13 +31,25 @@ export class Scanverra {
25
31
  this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
26
32
  }
27
33
  async request(method, path, body) {
28
- const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
29
- method,
30
- headers: { Authorization: `Bearer ${this.options.apiKey}`, ...(body === undefined ? {} : { "Content-Type": "application/json" }) },
31
- body: body === undefined ? undefined : JSON.stringify(body),
32
- signal: AbortSignal.timeout(this.options.timeoutMs ?? 300_000),
33
- });
34
- const text = await res.text();
34
+ const timeoutMs = this.options.timeoutMs ?? 300_000;
35
+ let res;
36
+ let text;
37
+ try {
38
+ // X-API-Key, not Authorization: clients drop Authorization when following
39
+ // the scanverra.com -> www.scanverra.com redirect.
40
+ res = await this.fetchImpl(`${this.baseUrl}${path}`, {
41
+ method,
42
+ headers: { "X-API-Key": this.options.apiKey, ...(body === undefined ? {} : { "Content-Type": "application/json" }) },
43
+ body: body === undefined ? undefined : JSON.stringify(body),
44
+ signal: AbortSignal.timeout(timeoutMs),
45
+ });
46
+ text = await res.text();
47
+ }
48
+ catch (e) {
49
+ if (isTimeout(e))
50
+ throw new ScanverraError(0, "timeout", `No answer from Scanverra within ${timeoutMs} ms`, null, { cause: e });
51
+ throw new ScanverraError(0, "network_error", `Could not reach Scanverra: ${e instanceof Error ? e.message : String(e)}`, null, { cause: e });
52
+ }
35
53
  let data = null;
36
54
  try {
37
55
  data = text ? JSON.parse(text) : null;
@@ -40,8 +58,10 @@ export class Scanverra {
40
58
  data = text;
41
59
  }
42
60
  if (!res.ok) {
43
- const err = (data ?? {});
44
- throw new ScanverraError(res.status, err.code, err.error ?? `Scanverra answered ${res.status}`, data);
61
+ // Most errors: { error: sentence, code }. Scan-access errors (402): { error: code, message: sentence }.
62
+ const err = (data && typeof data === "object" ? data : {});
63
+ const code = err.code ?? (err.message ? err.error : undefined);
64
+ throw new ScanverraError(res.status, code, err.message ?? err.error ?? `Scanverra answered ${res.status}`, data);
45
65
  }
46
66
  return data;
47
67
  }
package/package.json CHANGED
@@ -1,16 +1,28 @@
1
1
  {
2
2
  "name": "@scanverra/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Typed client for the Scanverra API: run scans, read properties and subscribe to events.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://www.scanverra.com/docs",
7
7
  "type": "module",
8
8
  "main": "dist/index.js",
9
9
  "types": "dist/index.d.ts",
10
- "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
11
- "files": ["dist", "README.md"],
12
- "engines": { "node": ">=18" },
13
- "publishConfig": { "access": "public" },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
14
26
  "scripts": {
15
27
  "build": "tsc -p tsconfig.json",
16
28
  "prepublishOnly": "npm run build"