@zksecurity/zkao-sdk 0.2.0 → 0.5.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/dist/index.d.ts CHANGED
@@ -73,6 +73,50 @@ interface paths {
73
73
  patch?: never;
74
74
  trace?: never;
75
75
  };
76
+ "/projects/{projectId}/repositories/{repositoryId}/audit-areas": {
77
+ parameters: {
78
+ query?: never;
79
+ header?: never;
80
+ path?: never;
81
+ cookie?: never;
82
+ };
83
+ /**
84
+ * List a repository's audit areas
85
+ * @description Requires scope: `read`. Audit areas are the named subsystems a scan can be scoped to (`auditAreaKeys` on launch). Each scan that maps the repository keeps the list current; sizes come from one branch's map, so an area that map no longer names is returned without one.
86
+ */
87
+ get: operations["listAuditAreas"];
88
+ put?: never;
89
+ /**
90
+ * Add a custom audit area
91
+ * @description Requires scope: `guidance:write`. An area is a name and a description, never a file list: a scan resolves it against the code at the commit it runs on. The `key` is derived from the name and disambiguated on collision.
92
+ */
93
+ post: operations["createAuditArea"];
94
+ delete?: never;
95
+ options?: never;
96
+ head?: never;
97
+ patch?: never;
98
+ trace?: never;
99
+ };
100
+ "/projects/{projectId}/repositories/{repositoryId}/audit-areas/{areaKey}": {
101
+ parameters: {
102
+ query?: never;
103
+ header?: never;
104
+ path?: never;
105
+ cookie?: never;
106
+ };
107
+ get?: never;
108
+ put?: never;
109
+ post?: never;
110
+ /**
111
+ * Delete a custom audit area
112
+ * @description Requires scope: `guidance:write`. Only an area with `source: custom` can be deleted; a discovered one is the map's own account of the repository and is refused with `409`. Scans that already ran keep the scope they ran with.
113
+ */
114
+ delete: operations["deleteAuditArea"];
115
+ options?: never;
116
+ head?: never;
117
+ patch?: never;
118
+ trace?: never;
119
+ };
76
120
  "/projects/{projectId}/scans": {
77
121
  parameters: {
78
122
  query?: never;
@@ -93,6 +137,10 @@ interface paths {
93
137
  * ACTIVE. Discover valid `presetRef` / `optInFlowIds` via the discovery
94
138
  * endpoints. Per-token spend limits are enforced, and the project must have
95
139
  * enough prepaid credits to cover the budget (otherwise `402`).
140
+ *
141
+ * A repository added moments ago may still be analyzing, in which case
142
+ * this returns `409 repository_initializing`. Wait for its `readiness` to
143
+ * be `ready` in `GET /repositories` and launch again.
96
144
  */
97
145
  post: operations["launchScan"];
98
146
  delete?: never;
@@ -330,7 +378,7 @@ interface paths {
330
378
  };
331
379
  /**
332
380
  * List opt-in flows available at launch
333
- * @description Requires scope: `read`. Pass the chosen ids as `optInFlowIds` when launching.
381
+ * @description Requires scope: `read`. Pass the chosen ids as `optInFlowIds` when launching. A flow marked `rotating` may run without being asked for.
334
382
  */
335
383
  get: operations["listOptionalFlows"];
336
384
  put?: never;
@@ -445,7 +493,7 @@ interface components {
445
493
  Error: {
446
494
  error: {
447
495
  /** @enum {string} */
448
- code: "unauthorized" | "forbidden" | "not_found" | "bad_request" | "conflict" | "insufficient_credits" | "rate_limited" | "internal";
496
+ code: "unauthorized" | "forbidden" | "not_found" | "bad_request" | "conflict" | "insufficient_credits" | "repository_initializing" | "rate_limited" | "internal";
449
497
  message: string;
450
498
  };
451
499
  };
@@ -465,6 +513,11 @@ interface components {
465
513
  defaultBranch?: string | null;
466
514
  /** @description ACTIVE | MARKED_FOR_DELETION */
467
515
  status: string;
516
+ /**
517
+ * @description Whether a scan can start on this repository. A newly added repository is `analyzing` until a one-time pass over it finishes; launching a scan meanwhile fails with `repository_initializing`. Poll this endpoint rather than retrying the launch blind.
518
+ * @enum {string}
519
+ */
520
+ readiness: "ready" | "analyzing";
468
521
  };
469
522
  Scan: {
470
523
  id: string;
@@ -567,6 +620,8 @@ interface components {
567
620
  flowId: string;
568
621
  title: string;
569
622
  description: string;
623
+ /** @description True when this flow belongs to a group the preset rotates through. Some of the group runs on every scan without being asked for, and which members depends on the repository. Naming it in `optInFlowIds` adds it whether or not the rotation picked it, and raises the scan's minimum budget. */
624
+ rotating: boolean;
570
625
  };
571
626
  LaunchScanRequest: {
572
627
  repositoryId: string;
@@ -579,8 +634,10 @@ interface components {
579
634
  /** @description Pin a specific commit SHA (7-40 hex chars). */
580
635
  commitHash?: string | null;
581
636
  commitMessage?: string | null;
582
- /** @description Optional-flow ids from /optional-flows. */
637
+ /** @description Optional-flow ids from /optional-flows, added to the scan on top of whatever a rotating group already contributes. */
583
638
  optInFlowIds?: string[];
639
+ /** @description Area keys from /audit-areas to scope this scan to. Omit or send an empty array to scan the whole repository. A key the repository's current map no longer names fails the launch rather than being dropped, so a scan budgeted for one subsystem never silently runs against everything. */
640
+ auditAreaKeys?: string[];
584
641
  /** @description Guidance for this scan only, replacing the repository's configured guidance layer (it is still layered over any committed zkao.md). Omit the field to inherit the repository's guidance; send null to scan with no guidance layer. */
585
642
  guidance?: string | null;
586
643
  };
@@ -611,6 +668,44 @@ interface components {
611
668
  /** @description True when the content already matched (no revision recorded). */
612
669
  unchanged: boolean;
613
670
  };
671
+ AuditArea: {
672
+ /** @description Stable slug, unique per repository. Pass it in `auditAreaKeys` when launching a scan. */
673
+ key: string;
674
+ name: string;
675
+ description: string | null;
676
+ /**
677
+ * @description `discovered` was named by a scan's map of the repository; `custom` was added through this API or the app.
678
+ * @enum {string}
679
+ */
680
+ source: "discovered" | "custom";
681
+ /** @description Whether the branch's latest map still names this area. */
682
+ inLatestMap: boolean;
683
+ /** @description Files this area covers at that map. Null when the map does not name it. */
684
+ files: number | null;
685
+ /** @description Lines this area covers at that map. Null when the map does not name it. */
686
+ lines: number | null;
687
+ };
688
+ AuditAreaList: {
689
+ repositoryId: string;
690
+ /** @description Branch the sizes were read from. */
691
+ branch: string | null;
692
+ /** @description Whether that branch has a stored map at all. */
693
+ mapped: boolean;
694
+ totalFiles: number | null;
695
+ totalLines: number | null;
696
+ /** @description The map's own audit order first, then the areas it no longer names. */
697
+ areas: components["schemas"]["AuditArea"][];
698
+ };
699
+ CreateAuditAreaRequest: {
700
+ name: string;
701
+ /** @description What this part of the code does. */
702
+ description?: string | null;
703
+ };
704
+ DeleteAuditAreaResult: {
705
+ repositoryId: string;
706
+ key: string;
707
+ deleted: boolean;
708
+ };
614
709
  CancelScanResult: {
615
710
  scanId: string;
616
711
  /** @description Terminal status after cancellation (always CANCELLED). */
@@ -696,6 +791,15 @@ interface components {
696
791
  "application/json": components["schemas"]["Error"];
697
792
  };
698
793
  };
794
+ /** @description The repository is still being analyzed and cannot be scanned yet. The same request succeeds once `GET /repositories` reports its `readiness` as `ready`. (code `repository_initializing`) */
795
+ RepositoryInitializing: {
796
+ headers: {
797
+ [name: string]: unknown;
798
+ };
799
+ content: {
800
+ "application/json": components["schemas"]["Error"];
801
+ };
802
+ };
699
803
  /** @description The token lacks the required scope */
700
804
  Forbidden: {
701
805
  headers: {
@@ -833,6 +937,97 @@ interface operations {
833
937
  409: components["responses"]["Conflict"];
834
938
  };
835
939
  };
940
+ listAuditAreas: {
941
+ parameters: {
942
+ query?: {
943
+ /** @description Branch whose map the sizes come from. Defaults to the repository's default branch. */
944
+ branch?: string;
945
+ };
946
+ header?: never;
947
+ path: {
948
+ projectId: components["parameters"]["ProjectId"];
949
+ repositoryId: components["parameters"]["RepositoryId"];
950
+ };
951
+ cookie?: never;
952
+ };
953
+ requestBody?: never;
954
+ responses: {
955
+ /** @description OK */
956
+ 200: {
957
+ headers: {
958
+ [name: string]: unknown;
959
+ };
960
+ content: {
961
+ "application/json": components["schemas"]["AuditAreaList"];
962
+ };
963
+ };
964
+ 401: components["responses"]["Unauthorized"];
965
+ 403: components["responses"]["Forbidden"];
966
+ 404: components["responses"]["NotFound"];
967
+ };
968
+ };
969
+ createAuditArea: {
970
+ parameters: {
971
+ query?: never;
972
+ header?: never;
973
+ path: {
974
+ projectId: components["parameters"]["ProjectId"];
975
+ repositoryId: components["parameters"]["RepositoryId"];
976
+ };
977
+ cookie?: never;
978
+ };
979
+ requestBody: {
980
+ content: {
981
+ "application/json": components["schemas"]["CreateAuditAreaRequest"];
982
+ };
983
+ };
984
+ responses: {
985
+ /** @description Created */
986
+ 201: {
987
+ headers: {
988
+ [name: string]: unknown;
989
+ };
990
+ content: {
991
+ "application/json": {
992
+ area: components["schemas"]["AuditArea"];
993
+ };
994
+ };
995
+ };
996
+ 400: components["responses"]["BadRequest"];
997
+ 401: components["responses"]["Unauthorized"];
998
+ 403: components["responses"]["Forbidden"];
999
+ 404: components["responses"]["NotFound"];
1000
+ 409: components["responses"]["Conflict"];
1001
+ };
1002
+ };
1003
+ deleteAuditArea: {
1004
+ parameters: {
1005
+ query?: never;
1006
+ header?: never;
1007
+ path: {
1008
+ projectId: components["parameters"]["ProjectId"];
1009
+ repositoryId: components["parameters"]["RepositoryId"];
1010
+ areaKey: string;
1011
+ };
1012
+ cookie?: never;
1013
+ };
1014
+ requestBody?: never;
1015
+ responses: {
1016
+ /** @description OK */
1017
+ 200: {
1018
+ headers: {
1019
+ [name: string]: unknown;
1020
+ };
1021
+ content: {
1022
+ "application/json": components["schemas"]["DeleteAuditAreaResult"];
1023
+ };
1024
+ };
1025
+ 401: components["responses"]["Unauthorized"];
1026
+ 403: components["responses"]["Forbidden"];
1027
+ 404: components["responses"]["NotFound"];
1028
+ 409: components["responses"]["Conflict"];
1029
+ };
1030
+ };
836
1031
  listScans: {
837
1032
  parameters: {
838
1033
  query?: {
@@ -890,6 +1085,7 @@ interface operations {
890
1085
  402: components["responses"]["InsufficientCredits"];
891
1086
  403: components["responses"]["Forbidden"];
892
1087
  404: components["responses"]["NotFound"];
1088
+ 409: components["responses"]["RepositoryInitializing"];
893
1089
  429: components["responses"]["RateLimited"];
894
1090
  };
895
1091
  };
@@ -1368,6 +1564,9 @@ type TriageStatus = Schemas["TriageStatus"];
1368
1564
  type ScanStatus = Schemas["ScanStatus"];
1369
1565
  type PublishArtifactResult = Schemas["PublishArtifactResult"];
1370
1566
  type RepositoryGuidance = Schemas["RepositoryGuidance"];
1567
+ type AuditArea = Schemas["AuditArea"];
1568
+ type AuditAreaList = Schemas["AuditAreaList"];
1569
+ type DeleteAuditAreaResult = Schemas["DeleteAuditAreaResult"];
1371
1570
  type SetGuidanceResult = Schemas["SetGuidanceResult"];
1372
1571
  type BillingBalance = Schemas["BillingBalance"];
1373
1572
  type BillingUsage = Schemas["BillingUsage"];
@@ -1398,6 +1597,16 @@ type ZkaoClientOptions = {
1398
1597
  /** Terminal scan statuses: polling can stop once a scan reaches one of these. */
1399
1598
  declare const TERMINAL_SCAN_STATUSES: readonly ScanStatus[];
1400
1599
  declare function isTerminalScanStatus(status: ScanStatus): boolean;
1600
+ type WaitForRepositoryReadyOptions = {
1601
+ /** First poll delay, in ms (default 5000). Grows with backoff up to maxIntervalMs. */
1602
+ intervalMs?: number;
1603
+ /** Cap on the poll delay, in ms (default 30000). */
1604
+ maxIntervalMs?: number;
1605
+ /** Give up after this long, in ms (default 30m). Throws on timeout. */
1606
+ timeoutMs?: number;
1607
+ /** Abort the wait (e.g. on Ctrl-C). Rejects with the abort reason. */
1608
+ signal?: AbortSignal;
1609
+ };
1401
1610
  type WaitForScanOptions = {
1402
1611
  /** First poll delay, in ms (default 5000). Grows with backoff up to maxIntervalMs. */
1403
1612
  intervalMs?: number;
@@ -1426,6 +1635,15 @@ declare class ZkaoClient {
1426
1635
  constructor(options: ZkaoClientOptions);
1427
1636
  private get path();
1428
1637
  listRepositories(): Promise<Repository[]>;
1638
+ /**
1639
+ * Block until a repository is ready to scan, then return it.
1640
+ *
1641
+ * A repository added moments ago is still being analyzed, and launching a
1642
+ * scan against it fails with `repository_initializing`. Call this between
1643
+ * adding a repository and launching its first scan rather than retrying the
1644
+ * launch and swallowing the error.
1645
+ */
1646
+ waitForRepositoryReady(repositoryId: string, opts?: WaitForRepositoryReadyOptions): Promise<Repository>;
1429
1647
  /** Read a repository's configured guidance (requires the `read` scope). */
1430
1648
  getRepositoryGuidance(repositoryId: string): Promise<RepositoryGuidance>;
1431
1649
  /**
@@ -1438,6 +1656,30 @@ declare class ZkaoClient {
1438
1656
  setRepositoryGuidance(repositoryId: string, content: string | null, opts?: {
1439
1657
  expectedContent?: string | null;
1440
1658
  }): Promise<SetGuidanceResult>;
1659
+ /**
1660
+ * List a repository's audit areas (requires the `read` scope).
1661
+ *
1662
+ * An area is a named subsystem a scan can be scoped to: pass its `key` in
1663
+ * `auditAreaKeys` when launching. Sizes come from one branch's map, so an
1664
+ * area that map no longer names is returned without one. Defaults to the
1665
+ * repository's default branch.
1666
+ */
1667
+ listAuditAreas(repositoryId: string, opts?: {
1668
+ branch?: string;
1669
+ }): Promise<AuditAreaList>;
1670
+ /**
1671
+ * Add a custom audit area (requires the `guidance:write` scope).
1672
+ *
1673
+ * An area is a name and a description, never a file list: a scan resolves it
1674
+ * against the code at the commit it runs on. The returned `key` is derived
1675
+ * from the name.
1676
+ */
1677
+ createAuditArea(repositoryId: string, name: string, description?: string | null): Promise<AuditArea>;
1678
+ /**
1679
+ * Delete a custom audit area (requires the `guidance:write` scope). An area
1680
+ * a scan's map named is refused with a `409`.
1681
+ */
1682
+ deleteAuditArea(repositoryId: string, areaKey: string): Promise<DeleteAuditAreaResult>;
1441
1683
  listScans(opts?: {
1442
1684
  page?: number;
1443
1685
  limit?: number;
@@ -1530,4 +1772,4 @@ declare class ZkaoClient {
1530
1772
  }): Promise<UsageMonthSummary[]>;
1531
1773
  }
1532
1774
 
1533
- export { type BillingBalance, type BillingSummary, type BillingUsage, type CancelScanResult, type ChangeNote, DEFAULT_BASE_URL, type Finding, type FindingDetail, type FindingNote, type LaunchScanRequest, type LaunchScanResult, type OptionalFlow, type Paginated, type PublishArtifactResult, type Repository, type RepositoryGuidance, type ResolutionReason, type ResolutionStatus, type Scan, type ScanDetail, type ScanPreset, type ScanStatus, type SetGuidanceResult, type Severity, TERMINAL_SCAN_STATUSES, type TriageStatus, type UsageEvent, type UsageEventType, type UsageMonthSummary, type WaitForScanOptions, ZkaoApiError, ZkaoClient, type ZkaoClientOptions, type components, isTerminalScanStatus, normalizeBaseUrl, type paths, resolveBaseUrlFromEnv };
1775
+ export { type BillingBalance, type BillingSummary, type BillingUsage, type CancelScanResult, type ChangeNote, DEFAULT_BASE_URL, type Finding, type FindingDetail, type FindingNote, type LaunchScanRequest, type LaunchScanResult, type OptionalFlow, type Paginated, type PublishArtifactResult, type Repository, type RepositoryGuidance, type ResolutionReason, type ResolutionStatus, type Scan, type ScanDetail, type ScanPreset, type ScanStatus, type SetGuidanceResult, type Severity, TERMINAL_SCAN_STATUSES, type TriageStatus, type UsageEvent, type UsageEventType, type UsageMonthSummary, type WaitForRepositoryReadyOptions, type WaitForScanOptions, ZkaoApiError, ZkaoClient, type ZkaoClientOptions, type components, isTerminalScanStatus, normalizeBaseUrl, type paths, resolveBaseUrlFromEnv };
package/dist/index.js CHANGED
@@ -120,6 +120,55 @@ var ZkaoClient = class {
120
120
  });
121
121
  return unwrap(res).repositories;
122
122
  }
123
+ /**
124
+ * Block until a repository is ready to scan, then return it.
125
+ *
126
+ * A repository added moments ago is still being analyzed, and launching a
127
+ * scan against it fails with `repository_initializing`. Call this between
128
+ * adding a repository and launching its first scan rather than retrying the
129
+ * launch and swallowing the error.
130
+ */
131
+ async waitForRepositoryReady(repositoryId, opts = {}) {
132
+ const intervalMs = opts.intervalMs ?? 5e3;
133
+ const maxIntervalMs = opts.maxIntervalMs ?? 3e4;
134
+ const timeoutMs = opts.timeoutMs ?? 30 * 60 * 1e3;
135
+ const deadline = Date.now() + timeoutMs;
136
+ for (let attempt = 0; ; attempt++) {
137
+ throwIfAborted(opts.signal);
138
+ const res = await this.http.GET("/projects/{projectId}/repositories", {
139
+ params: { path: this.path },
140
+ signal: opts.signal
141
+ });
142
+ const retryAfterMs = parseRetryAfterMs(
143
+ res.response.headers.get("retry-after")
144
+ );
145
+ if (res.response.status !== 429) {
146
+ const repository = unwrap(res).repositories.find(
147
+ (candidate) => candidate.id === repositoryId
148
+ );
149
+ if (!repository) {
150
+ throw new Error(
151
+ `Repository ${repositoryId} is not in this project (or this token's allowlist)`
152
+ );
153
+ }
154
+ if (repository.readiness === "ready") {
155
+ return repository;
156
+ }
157
+ }
158
+ const backoffMs = Math.min(
159
+ maxIntervalMs,
160
+ Math.round(intervalMs * 1.5 ** attempt)
161
+ );
162
+ const baseMs = retryAfterMs ?? backoffMs;
163
+ const delayMs = baseMs + Math.round(Math.random() * baseMs * 0.2);
164
+ if (Date.now() + delayMs > deadline) {
165
+ throw new Error(
166
+ `Timed out waiting for repository ${repositoryId} to be ready after ${timeoutMs}ms`
167
+ );
168
+ }
169
+ await sleep(delayMs, opts.signal);
170
+ }
171
+ }
123
172
  /** Read a repository's configured guidance (requires the `read` scope). */
124
173
  async getRepositoryGuidance(repositoryId) {
125
174
  const res = await this.http.GET(
@@ -145,6 +194,55 @@ var ZkaoClient = class {
145
194
  );
146
195
  return unwrap(res);
147
196
  }
197
+ // --- Audit areas --------------------------------------------------------
198
+ /**
199
+ * List a repository's audit areas (requires the `read` scope).
200
+ *
201
+ * An area is a named subsystem a scan can be scoped to: pass its `key` in
202
+ * `auditAreaKeys` when launching. Sizes come from one branch's map, so an
203
+ * area that map no longer names is returned without one. Defaults to the
204
+ * repository's default branch.
205
+ */
206
+ async listAuditAreas(repositoryId, opts = {}) {
207
+ const res = await this.http.GET(
208
+ "/projects/{projectId}/repositories/{repositoryId}/audit-areas",
209
+ {
210
+ params: {
211
+ path: { ...this.path, repositoryId },
212
+ query: opts.branch === void 0 ? {} : { branch: opts.branch }
213
+ }
214
+ }
215
+ );
216
+ return unwrap(res);
217
+ }
218
+ /**
219
+ * Add a custom audit area (requires the `guidance:write` scope).
220
+ *
221
+ * An area is a name and a description, never a file list: a scan resolves it
222
+ * against the code at the commit it runs on. The returned `key` is derived
223
+ * from the name.
224
+ */
225
+ async createAuditArea(repositoryId, name, description) {
226
+ const res = await this.http.POST(
227
+ "/projects/{projectId}/repositories/{repositoryId}/audit-areas",
228
+ {
229
+ params: { path: { ...this.path, repositoryId } },
230
+ body: { name, description: description ?? null }
231
+ }
232
+ );
233
+ return unwrap(res).area;
234
+ }
235
+ /**
236
+ * Delete a custom audit area (requires the `guidance:write` scope). An area
237
+ * a scan's map named is refused with a `409`.
238
+ */
239
+ async deleteAuditArea(repositoryId, areaKey) {
240
+ const res = await this.http.DELETE(
241
+ "/projects/{projectId}/repositories/{repositoryId}/audit-areas/{areaKey}",
242
+ { params: { path: { ...this.path, repositoryId, areaKey } } }
243
+ );
244
+ return unwrap(res);
245
+ }
148
246
  // --- Scans --------------------------------------------------------------
149
247
  async listScans(opts = {}) {
150
248
  const res = await this.http.GET("/projects/{projectId}/scans", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zksecurity/zkao-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
4
  "description": "TypeScript SDK for the zkao Project API",
5
5
  "license": "MIT",
6
6
  "author": "zkSecurity",
@@ -29,7 +29,11 @@
29
29
  "import": "./dist/index.js"
30
30
  }
31
31
  },
32
- "files": ["dist", "LICENSE", "README.md"],
32
+ "files": [
33
+ "dist",
34
+ "LICENSE",
35
+ "README.md"
36
+ ],
33
37
  "engines": {
34
38
  "node": ">=18"
35
39
  },