@zksecurity/zkao-sdk 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/LICENSE +21 -0
- package/README.md +36 -0
- package/dist/index.d.ts +1278 -0
- package/dist/index.js +335 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// src/base-url.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "https://zkao.io/api/v1";
|
|
3
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set([
|
|
4
|
+
"localhost",
|
|
5
|
+
"127.0.0.1",
|
|
6
|
+
"0.0.0.0",
|
|
7
|
+
"::1",
|
|
8
|
+
"[::1]"
|
|
9
|
+
]);
|
|
10
|
+
function unquote(value) {
|
|
11
|
+
return value.trim().replace(/^['"]+|['"]+$/g, "").trim();
|
|
12
|
+
}
|
|
13
|
+
function isLocalHost(host) {
|
|
14
|
+
return LOCAL_HOSTS.has(host.toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
function normalizeBaseUrl(value) {
|
|
17
|
+
const cleaned = unquote(value);
|
|
18
|
+
if (cleaned.length === 0) {
|
|
19
|
+
throw new Error("ZKAO_URL is empty");
|
|
20
|
+
}
|
|
21
|
+
let withScheme = cleaned;
|
|
22
|
+
if (!/^https?:\/\//i.test(withScheme)) {
|
|
23
|
+
const slash = withScheme.indexOf("/");
|
|
24
|
+
let authority = slash === -1 ? withScheme : withScheme.slice(0, slash);
|
|
25
|
+
const rest = slash === -1 ? "" : withScheme.slice(slash);
|
|
26
|
+
let bareHost;
|
|
27
|
+
if (authority.startsWith("[")) {
|
|
28
|
+
const end = authority.indexOf("]");
|
|
29
|
+
bareHost = end === -1 ? authority : authority.slice(0, end + 1);
|
|
30
|
+
} else if (authority.split(":").length > 2) {
|
|
31
|
+
bareHost = authority;
|
|
32
|
+
authority = `[${authority}]`;
|
|
33
|
+
} else {
|
|
34
|
+
bareHost = authority.split(":")[0] ?? "";
|
|
35
|
+
}
|
|
36
|
+
withScheme = `${isLocalHost(bareHost) ? "http" : "https"}://${authority}${rest}`;
|
|
37
|
+
}
|
|
38
|
+
let url;
|
|
39
|
+
try {
|
|
40
|
+
url = new URL(withScheme);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
throw new Error(`Invalid ZKAO_URL: ${JSON.stringify(value)}`, {
|
|
43
|
+
cause: err
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (url.protocol === "http:" && !isLocalHost(url.hostname)) {
|
|
47
|
+
console.warn(
|
|
48
|
+
`zkao: ZKAO_URL points at plaintext http://${url.hostname}; the API token will be sent unencrypted.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
52
|
+
const apiPath = /\/api\/v\d+$/.test(path) ? path : `${path}/api/v1`;
|
|
53
|
+
return `${url.origin}${apiPath}`;
|
|
54
|
+
}
|
|
55
|
+
function readEnv(env) {
|
|
56
|
+
if (env) {
|
|
57
|
+
return env;
|
|
58
|
+
}
|
|
59
|
+
const proc = globalThis.process;
|
|
60
|
+
return proc?.env ?? {};
|
|
61
|
+
}
|
|
62
|
+
function resolveBaseUrlFromEnv(env) {
|
|
63
|
+
const source = readEnv(env);
|
|
64
|
+
const origin = source.ZKAO_URL ? unquote(source.ZKAO_URL) : "";
|
|
65
|
+
if (origin.length > 0) {
|
|
66
|
+
return normalizeBaseUrl(origin);
|
|
67
|
+
}
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/client.ts
|
|
72
|
+
import createClient from "openapi-fetch";
|
|
73
|
+
var TERMINAL_SCAN_STATUSES = [
|
|
74
|
+
"COMPLETED",
|
|
75
|
+
"FAILED",
|
|
76
|
+
"CANCELLED"
|
|
77
|
+
];
|
|
78
|
+
function isTerminalScanStatus(status) {
|
|
79
|
+
return TERMINAL_SCAN_STATUSES.includes(status);
|
|
80
|
+
}
|
|
81
|
+
var ZkaoApiError = class extends Error {
|
|
82
|
+
status;
|
|
83
|
+
code;
|
|
84
|
+
constructor(status, code, message) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "ZkaoApiError";
|
|
87
|
+
this.status = status;
|
|
88
|
+
this.code = code;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
function unwrap(result) {
|
|
92
|
+
if (result.response.ok && result.data !== void 0) {
|
|
93
|
+
return result.data;
|
|
94
|
+
}
|
|
95
|
+
const envelope = result.error;
|
|
96
|
+
throw new ZkaoApiError(
|
|
97
|
+
result.response.status,
|
|
98
|
+
envelope?.error?.code ?? "error",
|
|
99
|
+
envelope?.error?.message ?? result.response.statusText
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
var ZkaoClient = class {
|
|
103
|
+
http;
|
|
104
|
+
projectId;
|
|
105
|
+
constructor(options) {
|
|
106
|
+
this.projectId = options.projectId;
|
|
107
|
+
this.http = createClient({
|
|
108
|
+
baseUrl: options.baseUrl ?? resolveBaseUrlFromEnv() ?? DEFAULT_BASE_URL,
|
|
109
|
+
headers: { Authorization: `Bearer ${options.token}` },
|
|
110
|
+
...options.fetch ? { fetch: options.fetch } : {}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
get path() {
|
|
114
|
+
return { projectId: this.projectId };
|
|
115
|
+
}
|
|
116
|
+
// --- Repositories -------------------------------------------------------
|
|
117
|
+
async listRepositories() {
|
|
118
|
+
const res = await this.http.GET("/projects/{projectId}/repositories", {
|
|
119
|
+
params: { path: this.path }
|
|
120
|
+
});
|
|
121
|
+
return unwrap(res).repositories;
|
|
122
|
+
}
|
|
123
|
+
/** Read a repository's configured guidance (requires the `read` scope). */
|
|
124
|
+
async getRepositoryGuidance(repositoryId) {
|
|
125
|
+
const res = await this.http.GET(
|
|
126
|
+
"/projects/{projectId}/repositories/{repositoryId}/guidance",
|
|
127
|
+
{ params: { path: { ...this.path, repositoryId } } }
|
|
128
|
+
);
|
|
129
|
+
return unwrap(res);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Set (or clear, with `null`) a repository's guidance (requires the
|
|
133
|
+
* `guidance:write` scope). Pass `expectedContent` for an optimistic
|
|
134
|
+
* compare-and-set: send the content you last read (or `null` for "currently
|
|
135
|
+
* cleared") to get a `409` instead of clobbering a concurrent change. Writing
|
|
136
|
+
* the same content that is already stored is a no-op (`unchanged: true`).
|
|
137
|
+
*/
|
|
138
|
+
async setRepositoryGuidance(repositoryId, content, opts = {}) {
|
|
139
|
+
const res = await this.http.PUT(
|
|
140
|
+
"/projects/{projectId}/repositories/{repositoryId}/guidance",
|
|
141
|
+
{
|
|
142
|
+
params: { path: { ...this.path, repositoryId } },
|
|
143
|
+
body: "expectedContent" in opts ? { content, expectedContent: opts.expectedContent } : { content }
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
return unwrap(res);
|
|
147
|
+
}
|
|
148
|
+
// --- Scans --------------------------------------------------------------
|
|
149
|
+
async listScans(opts = {}) {
|
|
150
|
+
const res = await this.http.GET("/projects/{projectId}/scans", {
|
|
151
|
+
params: { path: this.path, query: { page: opts.page, limit: opts.limit } }
|
|
152
|
+
});
|
|
153
|
+
return unwrap(res);
|
|
154
|
+
}
|
|
155
|
+
async getScan(scanId) {
|
|
156
|
+
const res = await this.http.GET("/projects/{projectId}/scans/{scanId}", {
|
|
157
|
+
params: { path: { ...this.path, scanId } }
|
|
158
|
+
});
|
|
159
|
+
return unwrap(res).scan;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Poll a scan until it reaches a terminal status (COMPLETED / FAILED /
|
|
163
|
+
* CANCELLED) and return the final detail. Honors the server's `Retry-After`
|
|
164
|
+
* header when present; otherwise backs off exponentially with jitter between
|
|
165
|
+
* `intervalMs` and `maxIntervalMs`. A `429` is treated as "slow down", not an
|
|
166
|
+
* error. Prefer this over a hand-rolled `getScan` loop so the server is not
|
|
167
|
+
* hammered.
|
|
168
|
+
*/
|
|
169
|
+
async waitForScan(scanId, opts = {}) {
|
|
170
|
+
const intervalMs = opts.intervalMs ?? 5e3;
|
|
171
|
+
const maxIntervalMs = opts.maxIntervalMs ?? 3e4;
|
|
172
|
+
const timeoutMs = opts.timeoutMs ?? 60 * 60 * 1e3;
|
|
173
|
+
const deadline = Date.now() + timeoutMs;
|
|
174
|
+
for (let attempt = 0; ; attempt++) {
|
|
175
|
+
throwIfAborted(opts.signal);
|
|
176
|
+
const res = await this.http.GET("/projects/{projectId}/scans/{scanId}", {
|
|
177
|
+
params: { path: { ...this.path, scanId } },
|
|
178
|
+
signal: opts.signal
|
|
179
|
+
});
|
|
180
|
+
const retryAfterMs = parseRetryAfterMs(
|
|
181
|
+
res.response.headers.get("retry-after")
|
|
182
|
+
);
|
|
183
|
+
if (res.response.status !== 429) {
|
|
184
|
+
const scan = unwrap(res).scan;
|
|
185
|
+
if (isTerminalScanStatus(scan.status)) {
|
|
186
|
+
return scan;
|
|
187
|
+
}
|
|
188
|
+
opts.onPoll?.(scan);
|
|
189
|
+
}
|
|
190
|
+
const backoffMs = Math.min(
|
|
191
|
+
maxIntervalMs,
|
|
192
|
+
Math.round(intervalMs * 1.5 ** attempt)
|
|
193
|
+
);
|
|
194
|
+
const baseMs = retryAfterMs ?? backoffMs;
|
|
195
|
+
const delayMs = baseMs + Math.round(Math.random() * baseMs * 0.2);
|
|
196
|
+
if (Date.now() + delayMs > deadline) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Timed out waiting for scan ${scanId} to finish after ${timeoutMs}ms`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
await sleep(delayMs, opts.signal);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async launchScan(body) {
|
|
205
|
+
const res = await this.http.POST("/projects/{projectId}/scans", {
|
|
206
|
+
params: { path: this.path },
|
|
207
|
+
body
|
|
208
|
+
});
|
|
209
|
+
return unwrap(res);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Cancel a running or queued scan. Signals in-flight jobs to stop, marks the
|
|
213
|
+
* scan CANCELLED, and releases its reserved credits. A scan already in a
|
|
214
|
+
* terminal COMPLETED/FAILED state cannot be cancelled.
|
|
215
|
+
*/
|
|
216
|
+
async cancelScan(scanId) {
|
|
217
|
+
const res = await this.http.POST("/projects/{projectId}/scans/{scanId}/cancel", {
|
|
218
|
+
params: { path: { ...this.path, scanId } }
|
|
219
|
+
});
|
|
220
|
+
return unwrap(res);
|
|
221
|
+
}
|
|
222
|
+
async publishScan(scanId, opts = {}) {
|
|
223
|
+
const res = await this.http.POST("/projects/{projectId}/scans/{scanId}/publish", {
|
|
224
|
+
params: { path: { ...this.path, scanId } },
|
|
225
|
+
body: { withPassword: opts.withPassword }
|
|
226
|
+
});
|
|
227
|
+
return unwrap(res);
|
|
228
|
+
}
|
|
229
|
+
// --- Findings -----------------------------------------------------------
|
|
230
|
+
async listFindings(opts = {}) {
|
|
231
|
+
const res = await this.http.GET("/projects/{projectId}/findings", {
|
|
232
|
+
params: {
|
|
233
|
+
path: this.path,
|
|
234
|
+
query: { page: opts.page, limit: opts.limit, scanId: opts.scanId }
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
return unwrap(res);
|
|
238
|
+
}
|
|
239
|
+
async getFinding(findingId) {
|
|
240
|
+
const res = await this.http.GET("/projects/{projectId}/findings/{findingId}", {
|
|
241
|
+
params: { path: { ...this.path, findingId } }
|
|
242
|
+
});
|
|
243
|
+
return unwrap(res).finding;
|
|
244
|
+
}
|
|
245
|
+
async addFindingNote(findingId, content) {
|
|
246
|
+
const res = await this.http.POST("/projects/{projectId}/findings/{findingId}/notes", {
|
|
247
|
+
params: { path: { ...this.path, findingId } },
|
|
248
|
+
body: { content }
|
|
249
|
+
});
|
|
250
|
+
return unwrap(res);
|
|
251
|
+
}
|
|
252
|
+
async pinFindingNote(findingId, noteId, kind = "resolution") {
|
|
253
|
+
const res = await this.http.POST(
|
|
254
|
+
"/projects/{projectId}/findings/{findingId}/notes/{noteId}/pin",
|
|
255
|
+
{ params: { path: { ...this.path, findingId, noteId } }, body: { kind } }
|
|
256
|
+
);
|
|
257
|
+
return unwrap(res);
|
|
258
|
+
}
|
|
259
|
+
async setFindingSeverity(findingId, severity) {
|
|
260
|
+
const res = await this.http.PATCH("/projects/{projectId}/findings/{findingId}/severity", {
|
|
261
|
+
params: { path: { ...this.path, findingId } },
|
|
262
|
+
body: { severity }
|
|
263
|
+
});
|
|
264
|
+
return unwrap(res);
|
|
265
|
+
}
|
|
266
|
+
async setFindingResolution(findingId, resolutionStatus, note) {
|
|
267
|
+
const res = await this.http.PATCH("/projects/{projectId}/findings/{findingId}/resolution", {
|
|
268
|
+
params: { path: { ...this.path, findingId } },
|
|
269
|
+
body: { resolutionStatus, note }
|
|
270
|
+
});
|
|
271
|
+
return unwrap(res);
|
|
272
|
+
}
|
|
273
|
+
async publishFinding(findingId, opts = {}) {
|
|
274
|
+
const res = await this.http.POST("/projects/{projectId}/findings/{findingId}/publish", {
|
|
275
|
+
params: { path: { ...this.path, findingId } },
|
|
276
|
+
body: { noteId: opts.noteId, withPassword: opts.withPassword }
|
|
277
|
+
});
|
|
278
|
+
return unwrap(res);
|
|
279
|
+
}
|
|
280
|
+
// --- Discovery ----------------------------------------------------------
|
|
281
|
+
async listScanPresets() {
|
|
282
|
+
const res = await this.http.GET("/projects/{projectId}/scan-presets", {
|
|
283
|
+
params: { path: this.path }
|
|
284
|
+
});
|
|
285
|
+
return unwrap(res).presets;
|
|
286
|
+
}
|
|
287
|
+
async listOptionalFlows() {
|
|
288
|
+
const res = await this.http.GET("/projects/{projectId}/optional-flows", {
|
|
289
|
+
params: { path: this.path }
|
|
290
|
+
});
|
|
291
|
+
return unwrap(res).optionalFlows;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
function throwIfAborted(signal) {
|
|
295
|
+
if (signal?.aborted) {
|
|
296
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("Aborted");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function parseRetryAfterMs(value) {
|
|
300
|
+
if (!value) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const seconds = Number(value);
|
|
304
|
+
if (Number.isFinite(seconds)) {
|
|
305
|
+
return Math.max(0, seconds * 1e3);
|
|
306
|
+
}
|
|
307
|
+
const date = Date.parse(value);
|
|
308
|
+
return Number.isNaN(date) ? null : Math.max(0, date - Date.now());
|
|
309
|
+
}
|
|
310
|
+
function sleep(ms, signal) {
|
|
311
|
+
return new Promise((resolve, reject) => {
|
|
312
|
+
if (signal?.aborted) {
|
|
313
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted"));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const timer = setTimeout(() => {
|
|
317
|
+
signal?.removeEventListener("abort", onAbort);
|
|
318
|
+
resolve();
|
|
319
|
+
}, ms);
|
|
320
|
+
const onAbort = () => {
|
|
321
|
+
clearTimeout(timer);
|
|
322
|
+
reject(signal?.reason instanceof Error ? signal.reason : new Error("Aborted"));
|
|
323
|
+
};
|
|
324
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
export {
|
|
328
|
+
DEFAULT_BASE_URL,
|
|
329
|
+
TERMINAL_SCAN_STATUSES,
|
|
330
|
+
ZkaoApiError,
|
|
331
|
+
ZkaoClient,
|
|
332
|
+
isTerminalScanStatus,
|
|
333
|
+
normalizeBaseUrl,
|
|
334
|
+
resolveBaseUrlFromEnv
|
|
335
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zksecurity/zkao-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for the zkao Project API",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "zkSecurity",
|
|
7
|
+
"homepage": "https://github.com/zksecurity/zkao-sdk#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/zksecurity/zkao-sdk.git",
|
|
11
|
+
"directory": "packages/sdk"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/zksecurity/zkao-sdk/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"zkao",
|
|
16
|
+
"security",
|
|
17
|
+
"audit",
|
|
18
|
+
"sdk",
|
|
19
|
+
"api",
|
|
20
|
+
"zero-knowledge"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": ["dist", "LICENSE", "README.md"],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"generate": "openapi-typescript ../../openapi/v1.yaml -o src/generated/types.ts",
|
|
41
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
42
|
+
"typecheck": "tsc --noEmit"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"openapi-fetch": "^0.13.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"openapi-typescript": "^7.4.0",
|
|
49
|
+
"tsup": "^8.3.0",
|
|
50
|
+
"typescript": "^5.7.0"
|
|
51
|
+
}
|
|
52
|
+
}
|