@scanverra/sdk 0.0.0-stage → 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 +58 -2
- package/dist/index.d.ts +193 -0
- package/dist/index.js +139 -0
- package/package.json +31 -4
package/README.md
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @scanverra/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A small, dependency-free client for the [Scanverra API](https://www.scanverra.com/docs). Node 18+, Deno and Bun.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Scanverra, verifyWebhook } from "@scanverra/sdk";
|
|
7
|
+
|
|
8
|
+
const scanverra = new Scanverra({ apiKey: process.env.SCANVERRA_API_KEY! });
|
|
9
|
+
|
|
10
|
+
// The Website Audit answers with the finished report.
|
|
11
|
+
const audit = await scanverra.scans.audit({ url: "https://example.com" });
|
|
12
|
+
|
|
13
|
+
// Security, Browser and SEO scans answer with an id; wait() polls until the report is ready.
|
|
14
|
+
const { scanId } = await scanverra.scans.security({ url: "https://example.com" });
|
|
15
|
+
const report = await scanverra.wait("security", scanId);
|
|
16
|
+
|
|
17
|
+
// Properties: each product's sites and repos, scored together.
|
|
18
|
+
const { properties } = await scanverra.properties.list();
|
|
19
|
+
const detail = await scanverra.properties.get(properties[0].id);
|
|
20
|
+
|
|
21
|
+
// Subscribe to events (REST hooks). Keep the secret to verify deliveries.
|
|
22
|
+
const hook = await scanverra.hooks.create({ event: "remediation.task_created", url: "https://example.com/hooks/scanverra" });
|
|
23
|
+
|
|
24
|
+
// In your receiver, with the raw request body:
|
|
25
|
+
const ok = await verifyWebhook({
|
|
26
|
+
secret: hook.secret,
|
|
27
|
+
body: rawBody,
|
|
28
|
+
timestamp: req.headers["x-scanverra-timestamp"],
|
|
29
|
+
signature: req.headers["x-scanverra-signature"],
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
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).
|
|
56
|
+
|
|
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.
|
|
58
|
+
|
|
59
|
+
Build: `npm run build` (writes `dist/`).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
export type ScanverraEvent = "scan.score_dropped" | "scan.policy_failed" | "scan.completed" | "payment_page.scripts_changed" | "attack_surface.changed" | "remediation.task_created" | "remediation.task_resolved" | "property.score_changed";
|
|
2
|
+
export interface ScanverraOptions {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** Defaults to https://www.scanverra.com */
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
/** Per-request timeout; scans run synchronously and can take a few minutes. Default 300000. */
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
fetch?: typeof fetch;
|
|
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
|
+
*/
|
|
15
|
+
export declare class ScanverraError extends Error {
|
|
16
|
+
status: number;
|
|
17
|
+
code: string | undefined;
|
|
18
|
+
body: unknown;
|
|
19
|
+
constructor(status: number, code: string | undefined, message: string, body: unknown, options?: {
|
|
20
|
+
cause?: unknown;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** A scan as the result endpoints return it; fields differ per tool (see /docs/api). */
|
|
24
|
+
export interface ScanResult {
|
|
25
|
+
id?: string;
|
|
26
|
+
status: "pending" | "running" | "done" | "completed" | "failed" | string;
|
|
27
|
+
errorMessage?: string | null;
|
|
28
|
+
[field: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
export interface PropertyToolScore {
|
|
31
|
+
tool: string;
|
|
32
|
+
score: number;
|
|
33
|
+
change: number | null;
|
|
34
|
+
scanned: number;
|
|
35
|
+
of: number;
|
|
36
|
+
at: string;
|
|
37
|
+
}
|
|
38
|
+
export interface PropertySummary {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
category: string;
|
|
42
|
+
client: string | null;
|
|
43
|
+
score: number | null;
|
|
44
|
+
lastScannedAt: string | null;
|
|
45
|
+
openTasks: number;
|
|
46
|
+
targets: {
|
|
47
|
+
target: string;
|
|
48
|
+
kind: "site" | "repo";
|
|
49
|
+
}[];
|
|
50
|
+
tools: PropertyToolScore[];
|
|
51
|
+
}
|
|
52
|
+
export interface PropertyDetail extends Omit<PropertySummary, "openTasks"> {
|
|
53
|
+
trend: {
|
|
54
|
+
day: string;
|
|
55
|
+
score: number;
|
|
56
|
+
}[];
|
|
57
|
+
issues: {
|
|
58
|
+
title: string;
|
|
59
|
+
severity: string;
|
|
60
|
+
tools: string[];
|
|
61
|
+
targets: string[];
|
|
62
|
+
places: number;
|
|
63
|
+
task: {
|
|
64
|
+
id: string;
|
|
65
|
+
status: string;
|
|
66
|
+
assignee: string | null;
|
|
67
|
+
} | null;
|
|
68
|
+
url: string;
|
|
69
|
+
}[];
|
|
70
|
+
nextSteps: {
|
|
71
|
+
kind: string;
|
|
72
|
+
tool: string;
|
|
73
|
+
target: string | null;
|
|
74
|
+
url: string | null;
|
|
75
|
+
}[];
|
|
76
|
+
}
|
|
77
|
+
export interface Hook {
|
|
78
|
+
id: string;
|
|
79
|
+
event: ScanverraEvent;
|
|
80
|
+
url: string;
|
|
81
|
+
source: string;
|
|
82
|
+
lastDeliveryAt: string | null;
|
|
83
|
+
lastStatus: number | null;
|
|
84
|
+
createdAt: string;
|
|
85
|
+
}
|
|
86
|
+
export interface Delivery<T = Record<string, unknown>> {
|
|
87
|
+
event: ScanverraEvent | "test";
|
|
88
|
+
sentAt: string;
|
|
89
|
+
summary: string;
|
|
90
|
+
data: T;
|
|
91
|
+
hookId?: string;
|
|
92
|
+
}
|
|
93
|
+
export declare class Scanverra {
|
|
94
|
+
private readonly options;
|
|
95
|
+
private readonly baseUrl;
|
|
96
|
+
private readonly fetchImpl;
|
|
97
|
+
constructor(options: ScanverraOptions);
|
|
98
|
+
request<T>(method: "GET" | "POST" | "DELETE", path: string, body?: unknown): Promise<T>;
|
|
99
|
+
/**
|
|
100
|
+
* Start a scan. The Website Audit and Repo Scanner answer with the finished
|
|
101
|
+
* report (pass `async: true` to get an id at once instead). The Security
|
|
102
|
+
* Scan, Browser Audit and SEO Optimizer always answer with an id straight
|
|
103
|
+
* away: read the report with `results.*` or wait for it with `wait()`.
|
|
104
|
+
*/
|
|
105
|
+
readonly scans: {
|
|
106
|
+
audit: (body: {
|
|
107
|
+
url: string;
|
|
108
|
+
budget?: Record<string, unknown>;
|
|
109
|
+
async?: boolean;
|
|
110
|
+
}) => Promise<Record<string, unknown>>;
|
|
111
|
+
security: (body: {
|
|
112
|
+
url: string;
|
|
113
|
+
}) => Promise<{
|
|
114
|
+
scanId: string;
|
|
115
|
+
}>;
|
|
116
|
+
browser: (body: {
|
|
117
|
+
url: string;
|
|
118
|
+
credentialId?: string;
|
|
119
|
+
}) => Promise<{
|
|
120
|
+
testId: string;
|
|
121
|
+
}>;
|
|
122
|
+
seo: (body: {
|
|
123
|
+
url: string;
|
|
124
|
+
keyword?: string;
|
|
125
|
+
competitorUrl?: string;
|
|
126
|
+
renderJs?: boolean;
|
|
127
|
+
checkPerformance?: boolean;
|
|
128
|
+
}) => Promise<{
|
|
129
|
+
scanId: string;
|
|
130
|
+
}>;
|
|
131
|
+
repo: (body: {
|
|
132
|
+
owner: string;
|
|
133
|
+
repo: string;
|
|
134
|
+
branch?: string;
|
|
135
|
+
provider?: "github" | "gitlab" | "bitbucket" | "azure";
|
|
136
|
+
async?: boolean;
|
|
137
|
+
}) => Promise<{
|
|
138
|
+
scanId: string;
|
|
139
|
+
}>;
|
|
140
|
+
};
|
|
141
|
+
/** Read a scan by id. `status` is "running" until it is "done"/"completed" or "failed". */
|
|
142
|
+
readonly results: {
|
|
143
|
+
audit: (id: string) => Promise<ScanResult>;
|
|
144
|
+
security: (id: string) => Promise<ScanResult>;
|
|
145
|
+
browser: (id: string) => Promise<ScanResult>;
|
|
146
|
+
seo: (id: string) => Promise<ScanResult>;
|
|
147
|
+
repo: (id: string) => Promise<ScanResult>;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Polls a scan until it finishes and returns the report. Throws a
|
|
151
|
+
* ScanverraError with code "scan_failed" when the scan failed, or
|
|
152
|
+
* "timeout" after `timeoutMs` (default 10 minutes).
|
|
153
|
+
*/
|
|
154
|
+
wait(tool: keyof Scanverra["results"], id: string, { intervalMs, timeoutMs }?: {
|
|
155
|
+
intervalMs?: number;
|
|
156
|
+
timeoutMs?: number;
|
|
157
|
+
}): Promise<ScanResult>;
|
|
158
|
+
readonly properties: {
|
|
159
|
+
list: () => Promise<{
|
|
160
|
+
properties: PropertySummary[];
|
|
161
|
+
}>;
|
|
162
|
+
get: (id: string) => Promise<PropertyDetail>;
|
|
163
|
+
};
|
|
164
|
+
readonly hooks: {
|
|
165
|
+
list: () => Promise<{
|
|
166
|
+
hooks: Hook[];
|
|
167
|
+
}>;
|
|
168
|
+
/** The returned secret signs every delivery; it is shown only once. */
|
|
169
|
+
create: (body: {
|
|
170
|
+
event: ScanverraEvent;
|
|
171
|
+
url: string;
|
|
172
|
+
source?: "zapier" | "make" | "n8n" | "api";
|
|
173
|
+
}) => Promise<{
|
|
174
|
+
id: string;
|
|
175
|
+
secret: string;
|
|
176
|
+
}>;
|
|
177
|
+
delete: (id: string) => Promise<{
|
|
178
|
+
ok: true;
|
|
179
|
+
}>;
|
|
180
|
+
sample: (event: ScanverraEvent) => Promise<Delivery<Record<string, unknown>>[]>;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Checks a delivery's X-Scanverra-Signature against the raw body and its
|
|
185
|
+
* X-Scanverra-Timestamp (rejects anything older than `toleranceSeconds`).
|
|
186
|
+
*/
|
|
187
|
+
export declare function verifyWebhook(input: {
|
|
188
|
+
secret: string;
|
|
189
|
+
body: string;
|
|
190
|
+
timestamp: string | null;
|
|
191
|
+
signature: string | null;
|
|
192
|
+
toleranceSeconds?: number;
|
|
193
|
+
}): Promise<boolean>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// @scanverra/sdk: a small typed client for the Scanverra API. No dependencies:
|
|
2
|
+
// it uses the global fetch (Node 18+, Deno, Bun, browsers for public calls)
|
|
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
|
+
*/
|
|
9
|
+
export class ScanverraError extends Error {
|
|
10
|
+
status;
|
|
11
|
+
code;
|
|
12
|
+
body;
|
|
13
|
+
constructor(status, code, message, body, options) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.body = body;
|
|
18
|
+
this.name = "ScanverraError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const isTimeout = (e) => e instanceof Error && (e.name === "TimeoutError" || e.name === "AbortError");
|
|
22
|
+
export class Scanverra {
|
|
23
|
+
options;
|
|
24
|
+
baseUrl;
|
|
25
|
+
fetchImpl;
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.options = options;
|
|
28
|
+
if (!options.apiKey)
|
|
29
|
+
throw new Error("Scanverra: apiKey is required");
|
|
30
|
+
this.baseUrl = (options.baseUrl ?? "https://www.scanverra.com").replace(/\/+$/, "");
|
|
31
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
32
|
+
}
|
|
33
|
+
async request(method, path, body) {
|
|
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
|
+
}
|
|
53
|
+
let data = null;
|
|
54
|
+
try {
|
|
55
|
+
data = text ? JSON.parse(text) : null;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
data = text;
|
|
59
|
+
}
|
|
60
|
+
if (!res.ok) {
|
|
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);
|
|
65
|
+
}
|
|
66
|
+
return data;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Start a scan. The Website Audit and Repo Scanner answer with the finished
|
|
70
|
+
* report (pass `async: true` to get an id at once instead). The Security
|
|
71
|
+
* Scan, Browser Audit and SEO Optimizer always answer with an id straight
|
|
72
|
+
* away: read the report with `results.*` or wait for it with `wait()`.
|
|
73
|
+
*/
|
|
74
|
+
scans = {
|
|
75
|
+
audit: (body) => this.request("POST", "/api/audit", body),
|
|
76
|
+
security: (body) => this.request("POST", "/api/security-scan", body),
|
|
77
|
+
browser: (body) => this.request("POST", "/api/browser-test", body),
|
|
78
|
+
seo: (body) => this.request("POST", "/api/seo-optimizer", body),
|
|
79
|
+
repo: (body) => this.request("POST", "/api/repo/scan", body),
|
|
80
|
+
};
|
|
81
|
+
/** Read a scan by id. `status` is "running" until it is "done"/"completed" or "failed". */
|
|
82
|
+
results = {
|
|
83
|
+
audit: (id) => this.request("GET", `/api/reports/${encodeURIComponent(id)}`),
|
|
84
|
+
security: (id) => this.request("GET", `/api/security-scan?id=${encodeURIComponent(id)}`),
|
|
85
|
+
browser: (id) => this.request("GET", `/api/browser-test?id=${encodeURIComponent(id)}`),
|
|
86
|
+
seo: (id) => this.request("GET", `/api/seo-optimizer?id=${encodeURIComponent(id)}`),
|
|
87
|
+
repo: (id) => this.request("GET", `/api/repo/result?id=${encodeURIComponent(id)}`),
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Polls a scan until it finishes and returns the report. Throws a
|
|
91
|
+
* ScanverraError with code "scan_failed" when the scan failed, or
|
|
92
|
+
* "timeout" after `timeoutMs` (default 10 minutes).
|
|
93
|
+
*/
|
|
94
|
+
async wait(tool, id, { intervalMs = 4000, timeoutMs = 600_000 } = {}) {
|
|
95
|
+
const deadline = Date.now() + timeoutMs;
|
|
96
|
+
for (;;) {
|
|
97
|
+
const result = await this.results[tool](id);
|
|
98
|
+
if (result.status === "failed")
|
|
99
|
+
throw new ScanverraError(200, "scan_failed", String(result.errorMessage ?? "The scan failed"), result);
|
|
100
|
+
if (result.status !== "running" && result.status !== "pending")
|
|
101
|
+
return result;
|
|
102
|
+
if (Date.now() > deadline)
|
|
103
|
+
throw new ScanverraError(0, "timeout", "The scan didn't finish in time", result);
|
|
104
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
properties = {
|
|
108
|
+
list: () => this.request("GET", "/api/v1/properties"),
|
|
109
|
+
get: (id) => this.request("GET", `/api/v1/properties/${encodeURIComponent(id)}`),
|
|
110
|
+
};
|
|
111
|
+
hooks = {
|
|
112
|
+
list: () => this.request("GET", "/api/hooks"),
|
|
113
|
+
/** The returned secret signs every delivery; it is shown only once. */
|
|
114
|
+
create: (body) => this.request("POST", "/api/hooks", body),
|
|
115
|
+
delete: (id) => this.request("DELETE", `/api/hooks/${encodeURIComponent(id)}`),
|
|
116
|
+
sample: (event) => this.request("GET", `/api/hooks/sample?event=${encodeURIComponent(event)}`),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
120
|
+
/**
|
|
121
|
+
* Checks a delivery's X-Scanverra-Signature against the raw body and its
|
|
122
|
+
* X-Scanverra-Timestamp (rejects anything older than `toleranceSeconds`).
|
|
123
|
+
*/
|
|
124
|
+
export async function verifyWebhook(input) {
|
|
125
|
+
const { secret, body, timestamp, signature } = input;
|
|
126
|
+
if (!timestamp || !signature?.startsWith("sha256="))
|
|
127
|
+
return false;
|
|
128
|
+
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > (input.toleranceSeconds ?? 300))
|
|
129
|
+
return false;
|
|
130
|
+
const enc = new TextEncoder();
|
|
131
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
132
|
+
const expected = `sha256=${hex(await crypto.subtle.sign("HMAC", key, enc.encode(`${timestamp}.${body}`)))}`;
|
|
133
|
+
if (expected.length !== signature.length)
|
|
134
|
+
return false;
|
|
135
|
+
let diff = 0;
|
|
136
|
+
for (let i = 0; i < expected.length; i++)
|
|
137
|
+
diff |= expected.charCodeAt(i) ^ signature.charCodeAt(i);
|
|
138
|
+
return diff === 0;
|
|
139
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scanverra/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Typed client for the Scanverra API: run scans, read properties and subscribe to events.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"homepage": "https://www.scanverra.com/docs",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
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
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.json",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"typescript": "^5.6.0"
|
|
32
|
+
}
|
|
33
|
+
}
|