@attestry/sdk 0.6.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 +190 -0
- package/README.md +1269 -0
- package/dist/client.d.ts +58 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +74 -0
- package/dist/client.js.map +1 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +43 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +41 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/lines-parser.d.ts +50 -0
- package/dist/lines-parser.d.ts.map +1 -0
- package/dist/lines-parser.js +211 -0
- package/dist/lines-parser.js.map +1 -0
- package/dist/ndjson-parser.d.ts +57 -0
- package/dist/ndjson-parser.d.ts.map +1 -0
- package/dist/ndjson-parser.js +245 -0
- package/dist/ndjson-parser.js.map +1 -0
- package/dist/resources/abac-policies.d.ts +1034 -0
- package/dist/resources/abac-policies.d.ts.map +1 -0
- package/dist/resources/abac-policies.js +1519 -0
- package/dist/resources/abac-policies.js.map +1 -0
- package/dist/resources/audit-log.d.ts +588 -0
- package/dist/resources/audit-log.d.ts.map +1 -0
- package/dist/resources/audit-log.js +629 -0
- package/dist/resources/audit-log.js.map +1 -0
- package/dist/resources/batch.d.ts +845 -0
- package/dist/resources/batch.d.ts.map +1 -0
- package/dist/resources/batch.js +1074 -0
- package/dist/resources/batch.js.map +1 -0
- package/dist/resources/chat.d.ts +151 -0
- package/dist/resources/chat.d.ts.map +1 -0
- package/dist/resources/chat.js +124 -0
- package/dist/resources/chat.js.map +1 -0
- package/dist/resources/check.d.ts +348 -0
- package/dist/resources/check.d.ts.map +1 -0
- package/dist/resources/check.js +543 -0
- package/dist/resources/check.js.map +1 -0
- package/dist/resources/compliance-check.d.ts +330 -0
- package/dist/resources/compliance-check.d.ts.map +1 -0
- package/dist/resources/compliance-check.js +402 -0
- package/dist/resources/compliance-check.js.map +1 -0
- package/dist/resources/decisions.d.ts +1208 -0
- package/dist/resources/decisions.d.ts.map +1 -0
- package/dist/resources/decisions.js +1362 -0
- package/dist/resources/decisions.js.map +1 -0
- package/dist/resources/evidence-pack.d.ts +1080 -0
- package/dist/resources/evidence-pack.d.ts.map +1 -0
- package/dist/resources/evidence-pack.js +1789 -0
- package/dist/resources/evidence-pack.js.map +1 -0
- package/dist/resources/gate.d.ts +613 -0
- package/dist/resources/gate.d.ts.map +1 -0
- package/dist/resources/gate.js +737 -0
- package/dist/resources/gate.js.map +1 -0
- package/dist/resources/incidents.d.ts +136 -0
- package/dist/resources/incidents.d.ts.map +1 -0
- package/dist/resources/incidents.js +229 -0
- package/dist/resources/incidents.js.map +1 -0
- package/dist/resources/regulatory-changes.d.ts +307 -0
- package/dist/resources/regulatory-changes.d.ts.map +1 -0
- package/dist/resources/regulatory-changes.js +365 -0
- package/dist/resources/regulatory-changes.js.map +1 -0
- package/dist/resources/safe-input-read.d.ts +21 -0
- package/dist/resources/safe-input-read.d.ts.map +1 -0
- package/dist/resources/safe-input-read.js +57 -0
- package/dist/resources/safe-input-read.js.map +1 -0
- package/dist/resources/ship-gate.d.ts +475 -0
- package/dist/resources/ship-gate.d.ts.map +1 -0
- package/dist/resources/ship-gate.js +727 -0
- package/dist/resources/ship-gate.js.map +1 -0
- package/dist/resources/vision.d.ts +540 -0
- package/dist/resources/vision.d.ts.map +1 -0
- package/dist/resources/vision.js +1036 -0
- package/dist/resources/vision.js.map +1 -0
- package/dist/retry.d.ts +103 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +224 -0
- package/dist/retry.js.map +1 -0
- package/dist/sse-parser.d.ts +64 -0
- package/dist/sse-parser.d.ts.map +1 -0
- package/dist/sse-parser.js +271 -0
- package/dist/sse-parser.js.map +1 -0
- package/dist/transport.d.ts +142 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +455 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,1789 @@
|
|
|
1
|
+
// ─── EvidencePack resource ──────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Wraps the KE2 P1 evidence-pack surface (P1.2 + P1.3 generator + REST,
|
|
4
|
+
// + the P1.4 lifecycle/export routes added to the SDK in P1.8):
|
|
5
|
+
//
|
|
6
|
+
// - POST /api/v1/evidence-packs create a draft pack
|
|
7
|
+
// - GET /api/v1/evidence-packs/{id} get pack + bundle list
|
|
8
|
+
// - GET /api/v1/evidence-packs list packs (cursor-paginated)
|
|
9
|
+
// - POST /api/v1/evidence-packs/{id}/bundles append a reperformance bundle
|
|
10
|
+
// - POST /api/v1/evidence-packs/{id}/sign sign a draft pack (P1.8)
|
|
11
|
+
// - POST /api/v1/evidence-packs/{id}/supersede supersede a signed pack (P1.8)
|
|
12
|
+
// - POST /api/v1/evidence-packs/{id}/revoke revoke a signed pack (P1.8)
|
|
13
|
+
// - GET /api/v1/evidence-packs/{id}/export export an artifact (json/pdf/zip) (P1.8)
|
|
14
|
+
//
|
|
15
|
+
// Tenth resource on `@attestry/sdk`. Sibling to `IncidentsResource`,
|
|
16
|
+
// `DecisionsResource`, `ChatResource`, `AuditLogResource`,
|
|
17
|
+
// `RegulatoryChangesResource`, `ComplianceCheckResource`, `CheckResource`,
|
|
18
|
+
// `GateResource`, `VisionResource`. Resource-class-per-kernel-resource
|
|
19
|
+
// convention (carry-forward invariant #43).
|
|
20
|
+
//
|
|
21
|
+
// **Scope** — P1.6 (2026-05-18) shipped the 4 core methods (`create`,
|
|
22
|
+
// `get`, `list`, `addBundle`). P1.8 (founder-ratified 2026-05-23) adds
|
|
23
|
+
// the 4 P1.4 lifecycle/export methods (`sign` / `supersede` / `revoke`
|
|
24
|
+
// / `export`), mirroring the shipped REST routes (P1.4) + MCP tools
|
|
25
|
+
// (P1.7) — eight methods total. The MCP `confirm` intentionality gate on
|
|
26
|
+
// `sign`/`revoke` (P1.7 DQ-1) is an MCP-layer affordance with NO
|
|
27
|
+
// REST/SDK equivalent and is NOT mirrored here.
|
|
28
|
+
//
|
|
29
|
+
// **`export` returns a non-JSON artifact** (P1.8 DEV-73) — unlike every
|
|
30
|
+
// other method (JSON `{success,data}` envelope via `_request`), the
|
|
31
|
+
// kernel export route returns the RAW artifact on success (json =
|
|
32
|
+
// `{export,pack,bundles}`; pdf = `Uint8Array`; zip = `ReadableStream`)
|
|
33
|
+
// with a download `Content-Disposition`, and the standard error
|
|
34
|
+
// envelope on failure. `export` therefore routes through the transport's
|
|
35
|
+
// `_streamRequest` (un-consumed `Response`; per-format content-type
|
|
36
|
+
// guard; non-2xx → `AttestryAPIError`) and returns a faithful-courier
|
|
37
|
+
// wrapper `EvidencePackExportResult` — it does NOT consume/`validatePack`
|
|
38
|
+
// the body (same discipline as `decisions.export` / `auditLog.export`).
|
|
39
|
+
//
|
|
40
|
+
// **`list` is single-page per call** (DEV-63) — cursor in / `nextCursor`
|
|
41
|
+
// out. The P1.6 spec's hostile concern #3 asked for an auto-paginating
|
|
42
|
+
// async iterator "per existing SDK convention" — but that misdiagnoses
|
|
43
|
+
// the convention. The SDK reserves async iterators for STREAMING
|
|
44
|
+
// endpoints (`chat.send` stream, `decisions.stream`, `decisions.export`,
|
|
45
|
+
// `auditLog.export`); every CURSOR-PAGINATED list method
|
|
46
|
+
// (`incidents.list`, `decisions.list`, and now `evidencePack.list`)
|
|
47
|
+
// returns a single page `{items, nextCursor}` and the caller pages
|
|
48
|
+
// manually. Auto-paginating ONLY `evidencePack.list` would make it the
|
|
49
|
+
// lone inconsistent paginated resource — the opposite of "per existing
|
|
50
|
+
// SDK convention". Cross-resource auto-pagination, if wanted, belongs
|
|
51
|
+
// in a dedicated SDK-wide prompt (residual R-1).
|
|
52
|
+
//
|
|
53
|
+
// **Method name `addBundle`** (DEV-62) — short verb matching the kernel
|
|
54
|
+
// internal fn (`addBundleToPack`). The MCP wire tool name is
|
|
55
|
+
// `append_bundle` (P1.5 wire-shape choice); the SDK reserves the shorter
|
|
56
|
+
// `addBundle` for the method to align with the SDK's verb-method
|
|
57
|
+
// convention (`decisions.ingest`, `chat.send`, `gate.evaluate`, etc.).
|
|
58
|
+
//
|
|
59
|
+
// **`Idempotency-Key` HTTP header is NOT exposed in P1.6** — same
|
|
60
|
+
// carry-forward as `vision.ts`. The kernel accepts `Idempotency-Key` on
|
|
61
|
+
// `POST /evidence-packs` and `POST /{id}/bundles`; the SDK's
|
|
62
|
+
// `RequestOptions` does not surface extra headers for JSON POSTs. Clean
|
|
63
|
+
// future extension; consumers who need idempotency today should retry
|
|
64
|
+
// with their own client-side dedupe.
|
|
65
|
+
//
|
|
66
|
+
// **Symmetric prototype-pollution defense** — module-load snapshot of
|
|
67
|
+
// `Object.hasOwn` applied to BOTH input AND response sides (carry-
|
|
68
|
+
// forward of session-16 second-hostile-review MEDIUM #3 generalization,
|
|
69
|
+
// freshest implementation in `gate.ts` / `vision.ts`). Without the
|
|
70
|
+
// snapshot, a late-loading hostile/buggy npm dep that overrides the
|
|
71
|
+
// global would defeat the defense.
|
|
72
|
+
//
|
|
73
|
+
// **No URIError defense on body fields** — POST bodies use
|
|
74
|
+
// `JSON.stringify` (handles lone UTF-16 surrogates as `\uDxxx` escapes).
|
|
75
|
+
// URL-path segments (`get`, `addBundle`) carry `packId` which the SDK
|
|
76
|
+
// pre-validates as a hyphen-only RFC 4122 UUID BEFORE
|
|
77
|
+
// `encodeURIComponent` runs — so a malformed input rejects with
|
|
78
|
+
// `TypeError` before the encoder sees it.
|
|
79
|
+
//
|
|
80
|
+
// **P3 content-type guard** — already in the SDK transport
|
|
81
|
+
// (`packages/attestry-sdk/src/transport.ts:271-291`). A non-JSON 200
|
|
82
|
+
// response surfaces as `AttestryAPIError`, NOT an opaque `SyntaxError`.
|
|
83
|
+
// HR-4(b) carry-forward applies to `mcp-server/src/client.ts`, NOT to
|
|
84
|
+
// the SDK transport.
|
|
85
|
+
import { AttestryError } from "../errors.js";
|
|
86
|
+
import { readInputField } from "./safe-input-read.js";
|
|
87
|
+
// Module-load snapshot of `Object.hasOwn`. Mirror of `gate.ts`/`vision.ts`.
|
|
88
|
+
// Used symmetrically on input AND response sides — defense on both
|
|
89
|
+
// boundaries against a late-loading hostile/buggy dep overriding the
|
|
90
|
+
// global.
|
|
91
|
+
const objectHasOwn = Object.hasOwn;
|
|
92
|
+
// RFC 4122 hyphenated UUID (8-4-4-4-12 hex, case-insensitive). Matches
|
|
93
|
+
// Zod's `z.string().uuid()` regex effectively. Mirror of `gate.ts`'s
|
|
94
|
+
// `UUID_REGEX`; drift-pinned in `evidence-pack.drift.test.ts` (Round 2)
|
|
95
|
+
// so a kernel-side switch to a different UUID flavor (ULID, KSUID) fires
|
|
96
|
+
// before consumer regressions.
|
|
97
|
+
const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
98
|
+
// ─── Closed-enum frozen tuples (drift-pinned in Round 2) ────────────────────
|
|
99
|
+
/**
|
|
100
|
+
* The five evidence-pack types the kernel accepts. Mirrors
|
|
101
|
+
* `PACK_TYPES` in kernel `src/lib/evidence-pack/types.ts:150-156`.
|
|
102
|
+
* Frozen so consumer code can safely use
|
|
103
|
+
* `PACK_TYPES.includes(...)` without mutation risk (P1 hardening —
|
|
104
|
+
* defends against a hostile/buggy npm dep mutating the array between
|
|
105
|
+
* SDK import and method call).
|
|
106
|
+
*
|
|
107
|
+
* Drift-pinned in the spec-diff round (`evidence-pack.drift.test.ts`)
|
|
108
|
+
* by text-comparing this declaration with the kernel's. An addition /
|
|
109
|
+
* removal / reordering on either side trips the test, **satisfying P1
|
|
110
|
+
* checkpoint AC7** ("SDK drift pin: `pack_type` enum in SDK matches
|
|
111
|
+
* kernel").
|
|
112
|
+
*/
|
|
113
|
+
export const PACK_TYPES = Object.freeze([
|
|
114
|
+
"annex_iv",
|
|
115
|
+
"agentic_reperformance",
|
|
116
|
+
"red_team_cycle",
|
|
117
|
+
"pccp_evidence",
|
|
118
|
+
"underwriting_evidence",
|
|
119
|
+
]);
|
|
120
|
+
/**
|
|
121
|
+
* The five pack-status values the kernel emits + accepts as a filter.
|
|
122
|
+
* Mirrors `PACK_STATUSES` in kernel `src/lib/evidence-pack/types.ts:160-166`.
|
|
123
|
+
* Frozen; drift-pinned identically to `PACK_TYPES`.
|
|
124
|
+
*/
|
|
125
|
+
export const PACK_STATUSES = Object.freeze([
|
|
126
|
+
"draft",
|
|
127
|
+
"signed",
|
|
128
|
+
"superseded",
|
|
129
|
+
"revoked",
|
|
130
|
+
"expired",
|
|
131
|
+
]);
|
|
132
|
+
/**
|
|
133
|
+
* The three artifact formats `evidencePack.export` accepts. Mirrors
|
|
134
|
+
* `EXPORT_FORMATS` in kernel `src/lib/evidence-pack/types.ts:584`
|
|
135
|
+
* (`["json","pdf","zip"] as const`). Frozen; drift-pinned byte-equal to
|
|
136
|
+
* the kernel in `evidence-pack.drift.test.ts` (P1.8 DEV-76).
|
|
137
|
+
*
|
|
138
|
+
* The kernel route's `exportQuerySchema` requires `format` (no default,
|
|
139
|
+
* spec concern E1 — unknown/absent → 422). The SDK pre-validates
|
|
140
|
+
* `format` against this frozen tuple, so an absent/unknown format
|
|
141
|
+
* rejects with a synchronous `TypeError` before the request is sent.
|
|
142
|
+
*/
|
|
143
|
+
export const EXPORT_FORMATS = Object.freeze(["json", "pdf", "zip"]);
|
|
144
|
+
/**
|
|
145
|
+
* Per-format `Content-Type` the kernel export route emits on success.
|
|
146
|
+
* Mirrors `EXPORT_CONTENT_TYPES` in kernel
|
|
147
|
+
* `src/lib/evidence-pack/export.ts:38-42`. Module-local (the consumer
|
|
148
|
+
* gets the value via `EvidencePackExportResult.contentType`); drift-
|
|
149
|
+
* pinned against the kernel mapping in `evidence-pack.drift.test.ts`.
|
|
150
|
+
*
|
|
151
|
+
* Used as `export`'s per-format `expectedContentType` for the transport
|
|
152
|
+
* `_streamRequest` content-type guard (a wrong-content-type 200 → a
|
|
153
|
+
* clear `AttestryAPIError`, not an opaque downstream parse crash) AND
|
|
154
|
+
* as the canonical `contentType` surfaced on the result — the guard
|
|
155
|
+
* guarantees the response MIME equals this value, so it is accurate.
|
|
156
|
+
*/
|
|
157
|
+
const EXPORT_CONTENT_TYPES = {
|
|
158
|
+
json: "application/json",
|
|
159
|
+
pdf: "application/pdf",
|
|
160
|
+
zip: "application/zip",
|
|
161
|
+
};
|
|
162
|
+
// ─── Closed-spec ceiling constants (drift-pinned in Round 2) ────────────────
|
|
163
|
+
/**
|
|
164
|
+
* Maximum `limit` accepted by `GET /api/v1/evidence-packs`. Mirrors the
|
|
165
|
+
* kernel `listEvidencePacksQuerySchema` `z.coerce.number().int().min(1)
|
|
166
|
+
* .max(200)` cap. The SDK rejects an over-cap `limit` synchronously to
|
|
167
|
+
* save a billed 422 round-trip.
|
|
168
|
+
*/
|
|
169
|
+
const MAX_LIST_LIMIT = 200;
|
|
170
|
+
/**
|
|
171
|
+
* Maximum length of `inputsHash` / `outputsHash` on `addBundleToPack`.
|
|
172
|
+
* Mirrors the kernel `addBundleToPackInputSchema` `z.string().min(1)
|
|
173
|
+
* .max(500)` rule.
|
|
174
|
+
*/
|
|
175
|
+
const MAX_HASH_LENGTH = 500;
|
|
176
|
+
/**
|
|
177
|
+
* Maximum length of `traceContent` array on `addBundleToPack`. Mirrors
|
|
178
|
+
* the kernel `z.array(traceEntrySchema).max(1000)` rule.
|
|
179
|
+
*/
|
|
180
|
+
const MAX_TRACE_CONTENT_LENGTH = 1000;
|
|
181
|
+
/**
|
|
182
|
+
* Maximum length of `storageUri` on `addBundleToPack`. Mirrors the
|
|
183
|
+
* kernel `httpsOnlyUrl(2000)` length cap. Scheme validation
|
|
184
|
+
* (`http(s)://`) is kernel-authoritative (faithful courier — the SDK
|
|
185
|
+
* does not duplicate the regex; same convention as
|
|
186
|
+
* `vision.extract.imageUri`).
|
|
187
|
+
*/
|
|
188
|
+
const MAX_STORAGE_URI_LENGTH = 2000;
|
|
189
|
+
/**
|
|
190
|
+
* Maximum length of `frameworkBindings` array on `createEvidencePack`.
|
|
191
|
+
* Mirrors the kernel `z.array(frameworkBindingSchema).max(50)` rule.
|
|
192
|
+
*/
|
|
193
|
+
const MAX_FRAMEWORK_BINDINGS_LENGTH = 50;
|
|
194
|
+
/**
|
|
195
|
+
* Maximum length of `reason` on `revoke`. Mirrors the kernel
|
|
196
|
+
* `revokePackInputSchema` `z.string().min(1).max(500)` rule (P1.8).
|
|
197
|
+
*/
|
|
198
|
+
const MAX_REASON_LENGTH = 500;
|
|
199
|
+
// ─── Resource class ─────────────────────────────────────────────────────────
|
|
200
|
+
/**
|
|
201
|
+
* `evidencePack` resource — sibling to `IncidentsResource`,
|
|
202
|
+
* `DecisionsResource`, `ChatResource`, `AuditLogResource`,
|
|
203
|
+
* `RegulatoryChangesResource`, `ComplianceCheckResource`,
|
|
204
|
+
* `CheckResource`, `GateResource`, `VisionResource`.
|
|
205
|
+
*
|
|
206
|
+
* Eight methods: the P1.6 core (`create`, `get`, `list`, `addBundle`)
|
|
207
|
+
* plus the P1.8 lifecycle/export ops (`sign`, `supersede`, `revoke`,
|
|
208
|
+
* `export`). All are JSON request/response (`{success,data}` envelope
|
|
209
|
+
* via `_request`) EXCEPT `export`, which returns a downloadable artifact
|
|
210
|
+
* (json/pdf/zip) via the streaming transport `_streamRequest`.
|
|
211
|
+
*/
|
|
212
|
+
export class EvidencePackResource {
|
|
213
|
+
client;
|
|
214
|
+
constructor(client) {
|
|
215
|
+
this.client = client;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Create a new draft evidence pack for the authenticated organization.
|
|
219
|
+
* Wraps `POST /api/v1/evidence-packs`.
|
|
220
|
+
*
|
|
221
|
+
* `orgId` and `userId` are derived server-side from the API key; they
|
|
222
|
+
* are never accepted on the wire. The kernel applies defaults for
|
|
223
|
+
* `frameworkBindings` (`[]`), `consumerHints` (`{}`), `metadata`
|
|
224
|
+
* (`{}`), and `status` (`"draft"`) when fields are omitted.
|
|
225
|
+
*
|
|
226
|
+
* **Idempotency**: the kernel accepts `Idempotency-Key` on this
|
|
227
|
+
* endpoint, but the SDK does NOT expose the header in P1.6 (see
|
|
228
|
+
* resource header comment). Consumers needing safe retry today
|
|
229
|
+
* should dedupe client-side.
|
|
230
|
+
*
|
|
231
|
+
* Errors — ordered by kernel firing precedence (rate-limit → auth →
|
|
232
|
+
* body parse → Zod → DB):
|
|
233
|
+
* - `AttestryAPIError` (status 429) — rate limit FIRES FIRST
|
|
234
|
+
* (auto-retried by default — invariant #18).
|
|
235
|
+
* - `AttestryAPIError` (status 401) — no API key OR invalid key.
|
|
236
|
+
* - `AttestryAPIError` (status 403) — authenticated key lacks
|
|
237
|
+
* `WRITE_ASSESSMENTS` permission.
|
|
238
|
+
* - `AttestryAPIError` (status 400) — JSON parse failure on the
|
|
239
|
+
* body OR a malformed `Idempotency-Key` header (the kernel
|
|
240
|
+
* emits 400 for both transport-shape failures).
|
|
241
|
+
* - `AttestryAPIError` (status 409) — `Idempotency-Key` conflict
|
|
242
|
+
* (same key, different body hash; `details.code` ===
|
|
243
|
+
* `"evidence_pack.idempotency_key_conflict"`). Not reachable
|
|
244
|
+
* from P1.6's SDK directly.
|
|
245
|
+
* - `AttestryAPIError` (status 422) — Zod validation failed
|
|
246
|
+
* (`details.code` === `"evidence_pack.validation_failed"`;
|
|
247
|
+
* `details.issues` carries the field paths).
|
|
248
|
+
* - `AttestryAPIError` (status 500) — internal kernel error.
|
|
249
|
+
* - `AttestryError` ("request aborted by caller") — caller-
|
|
250
|
+
* supplied `options.signal` fired (pre-aborted or mid-flight).
|
|
251
|
+
* - `AttestryError` (P2 hardening) — kernel response failed
|
|
252
|
+
* SDK-side shape validation (not an object, wrong type on any
|
|
253
|
+
* field).
|
|
254
|
+
* - `AttestryAPIError` (P3 hardening) — kernel response had a
|
|
255
|
+
* wrong Content-Type (transport-level guard, before body
|
|
256
|
+
* parsing).
|
|
257
|
+
* - `TypeError` (synchronous, no fetch issued) — input failed
|
|
258
|
+
* SDK-side validation (null/array/non-object input; missing
|
|
259
|
+
* `packType`; bad `packType` enum; bad `systemId` UUID; bad
|
|
260
|
+
* `frameworkBindings` array shape; bad `metadata` shape).
|
|
261
|
+
*
|
|
262
|
+
* **SDK-side validation** (synchronous `TypeError`, no fetch issued):
|
|
263
|
+
* - `input`: required; non-null, non-array object.
|
|
264
|
+
* - `input.packType`: required own-property; member of `PACK_TYPES`.
|
|
265
|
+
* - `input.systemId` (when own-present, value not undefined): non-
|
|
266
|
+
* empty string matching `UUID_REGEX`.
|
|
267
|
+
* - `input.frameworkBindings` (when own-present, value not
|
|
268
|
+
* undefined): array of length ≤50 (kernel cap); per-entry shape
|
|
269
|
+
* is open-spec and forwarded to the kernel as-is.
|
|
270
|
+
* - `input.metadata` (when own-present, value not undefined):
|
|
271
|
+
* non-null, non-array object.
|
|
272
|
+
*
|
|
273
|
+
* **Response-shape validation** (P2 hardening; symmetric defense on
|
|
274
|
+
* response side via the `objectHasOwn` snapshot): every documented
|
|
275
|
+
* `EvidencePack` field is type-checked. Rejects with `AttestryError`
|
|
276
|
+
* on shape violation.
|
|
277
|
+
*
|
|
278
|
+
* @example Minimum viable pack (org-level, no system, no bindings)
|
|
279
|
+
* ```ts
|
|
280
|
+
* const pack = await client.evidencePack.create({
|
|
281
|
+
* packType: "underwriting_evidence",
|
|
282
|
+
* });
|
|
283
|
+
* console.log("created:", pack.id, "status:", pack.status); // "draft"
|
|
284
|
+
* ```
|
|
285
|
+
*
|
|
286
|
+
* @example Annex IV pack scoped to a specific AI system
|
|
287
|
+
* ```ts
|
|
288
|
+
* const pack = await client.evidencePack.create({
|
|
289
|
+
* packType: "annex_iv",
|
|
290
|
+
* systemId: "11111111-1111-1111-1111-111111111111",
|
|
291
|
+
* frameworkBindings: [
|
|
292
|
+
* { framework: "eu_ai_act", identifier: "Annex.IV.1" },
|
|
293
|
+
* { framework: "iso_42001", identifier: "8.2" },
|
|
294
|
+
* ],
|
|
295
|
+
* metadata: { author: "compliance-bot", version: 1 },
|
|
296
|
+
* });
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
create(input, options) {
|
|
300
|
+
// Top-level shape — input is REQUIRED. typeof null === "object" and
|
|
301
|
+
// typeof [] === "object", so guard both explicitly.
|
|
302
|
+
if (input === null ||
|
|
303
|
+
typeof input !== "object" ||
|
|
304
|
+
Array.isArray(input)) {
|
|
305
|
+
throw new TypeError("evidencePack.create: `input` must be a non-null object with `packType`");
|
|
306
|
+
}
|
|
307
|
+
// Snapshot each field's value EXACTLY ONCE up front via the own-
|
|
308
|
+
// property indexer. Three motivations (carry-forward from gate.ts /
|
|
309
|
+
// vision.ts):
|
|
310
|
+
// 1. Prototype-pollution defense (generalization of invariant #48):
|
|
311
|
+
// `Object.prototype.packType = "..."` cannot trick the SDK into
|
|
312
|
+
// silently sending the polluted value when `{}` is passed.
|
|
313
|
+
// 2. TOCTOU defense: a Proxy / getter-defining input could yield
|
|
314
|
+
// different values across multiple reads.
|
|
315
|
+
// 3. Explicit `{}` is treated as those-fields-omitted —
|
|
316
|
+
// `objectHasOwn` returns false on missing keys.
|
|
317
|
+
// 4. Throwing-getter defense — each read goes through
|
|
318
|
+
// `readInputField`, converting a throwing accessor's exception
|
|
319
|
+
// into the documented synchronous `TypeError` input contract
|
|
320
|
+
// (session-22 hostile MEDIUM-1).
|
|
321
|
+
const hasPackType = objectHasOwn(input, "packType");
|
|
322
|
+
const packTypeRaw = hasPackType
|
|
323
|
+
? readInputField(input, "packType", "evidencePack.create")
|
|
324
|
+
: undefined;
|
|
325
|
+
const hasSystemId = objectHasOwn(input, "systemId");
|
|
326
|
+
const systemIdRaw = hasSystemId
|
|
327
|
+
? readInputField(input, "systemId", "evidencePack.create")
|
|
328
|
+
: undefined;
|
|
329
|
+
const hasFrameworkBindings = objectHasOwn(input, "frameworkBindings");
|
|
330
|
+
const frameworkBindingsRaw = hasFrameworkBindings
|
|
331
|
+
? readInputField(input, "frameworkBindings", "evidencePack.create")
|
|
332
|
+
: undefined;
|
|
333
|
+
const hasMetadata = objectHasOwn(input, "metadata");
|
|
334
|
+
const metadataRaw = hasMetadata
|
|
335
|
+
? readInputField(input, "metadata", "evidencePack.create")
|
|
336
|
+
: undefined;
|
|
337
|
+
// packType REQUIRED + closed-enum membership.
|
|
338
|
+
if (!hasPackType || packTypeRaw === undefined) {
|
|
339
|
+
throw new TypeError("evidencePack.create: `packType` is required");
|
|
340
|
+
}
|
|
341
|
+
if (typeof packTypeRaw !== "string") {
|
|
342
|
+
throw new TypeError(`evidencePack.create: \`packType\` must be a string ` +
|
|
343
|
+
`(got ${describeType(packTypeRaw)})`);
|
|
344
|
+
}
|
|
345
|
+
if (!PACK_TYPES.includes(packTypeRaw)) {
|
|
346
|
+
throw new TypeError(`evidencePack.create: \`packType\` must be one of ` +
|
|
347
|
+
`${JSON.stringify(PACK_TYPES)} (got ${JSON.stringify(packTypeRaw)})`);
|
|
348
|
+
}
|
|
349
|
+
const validatedPackType = packTypeRaw;
|
|
350
|
+
// Optional systemId — UUID pre-validation when own-present.
|
|
351
|
+
let validatedSystemId;
|
|
352
|
+
if (hasSystemId && systemIdRaw !== undefined) {
|
|
353
|
+
if (typeof systemIdRaw !== "string") {
|
|
354
|
+
throw new TypeError(`evidencePack.create: \`systemId\` must be a string when provided ` +
|
|
355
|
+
`(got ${describeType(systemIdRaw)})`);
|
|
356
|
+
}
|
|
357
|
+
if (!UUID_REGEX.test(systemIdRaw)) {
|
|
358
|
+
throw new TypeError("evidencePack.create: `systemId` must be an RFC 4122 hyphenated UUID");
|
|
359
|
+
}
|
|
360
|
+
validatedSystemId = systemIdRaw;
|
|
361
|
+
}
|
|
362
|
+
// Optional frameworkBindings — array + length cap. Per-entry shape
|
|
363
|
+
// is open-spec (kernel `frameworkBindingSchema` is the deep
|
|
364
|
+
// validator with `.strict()` rejection of unknown keys).
|
|
365
|
+
let validatedFrameworkBindings;
|
|
366
|
+
if (hasFrameworkBindings && frameworkBindingsRaw !== undefined) {
|
|
367
|
+
if (!Array.isArray(frameworkBindingsRaw)) {
|
|
368
|
+
throw new TypeError(`evidencePack.create: \`frameworkBindings\` must be an array when ` +
|
|
369
|
+
`provided (got ${describeType(frameworkBindingsRaw)})`);
|
|
370
|
+
}
|
|
371
|
+
// Snapshot via Array.from so a Proxy whose `.length` or `[i]`
|
|
372
|
+
// changes between reads can't slip past validation. Per-entry
|
|
373
|
+
// shape is forwarded as-is to the kernel.
|
|
374
|
+
const snapshot = Array.from(frameworkBindingsRaw);
|
|
375
|
+
if (snapshot.length > MAX_FRAMEWORK_BINDINGS_LENGTH) {
|
|
376
|
+
throw new TypeError(`evidencePack.create: \`frameworkBindings\` array exceeds the ` +
|
|
377
|
+
`kernel's max length of ${MAX_FRAMEWORK_BINDINGS_LENGTH} (got ` +
|
|
378
|
+
`${snapshot.length})`);
|
|
379
|
+
}
|
|
380
|
+
validatedFrameworkBindings = snapshot;
|
|
381
|
+
}
|
|
382
|
+
// Optional metadata — non-null non-array object when present.
|
|
383
|
+
let validatedMetadata;
|
|
384
|
+
if (hasMetadata && metadataRaw !== undefined) {
|
|
385
|
+
if (metadataRaw === null ||
|
|
386
|
+
typeof metadataRaw !== "object" ||
|
|
387
|
+
Array.isArray(metadataRaw)) {
|
|
388
|
+
throw new TypeError(`evidencePack.create: \`metadata\` must be a non-null object when ` +
|
|
389
|
+
`provided (got ${describeType(metadataRaw)})`);
|
|
390
|
+
}
|
|
391
|
+
validatedMetadata = metadataRaw;
|
|
392
|
+
}
|
|
393
|
+
// Build the body from explicitly-named fields. Omit optional fields
|
|
394
|
+
// the consumer omitted so the kernel applies its `.default(...)`.
|
|
395
|
+
const body = {
|
|
396
|
+
packType: validatedPackType,
|
|
397
|
+
};
|
|
398
|
+
if (validatedSystemId !== undefined)
|
|
399
|
+
body.systemId = validatedSystemId;
|
|
400
|
+
if (validatedFrameworkBindings !== undefined) {
|
|
401
|
+
body.frameworkBindings = validatedFrameworkBindings;
|
|
402
|
+
}
|
|
403
|
+
if (validatedMetadata !== undefined)
|
|
404
|
+
body.metadata = validatedMetadata;
|
|
405
|
+
return this.client
|
|
406
|
+
._request({
|
|
407
|
+
method: "POST",
|
|
408
|
+
path: "/api/v1/evidence-packs",
|
|
409
|
+
body,
|
|
410
|
+
options,
|
|
411
|
+
})
|
|
412
|
+
.then((result) => {
|
|
413
|
+
validatePack(result, "evidencePack.create", "response");
|
|
414
|
+
return result;
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Retrieve a single evidence pack's metadata together with its full
|
|
419
|
+
* reperformance-bundle list. Wraps `GET /api/v1/evidence-packs/{id}`.
|
|
420
|
+
*
|
|
421
|
+
* **Anti-enumeration 404**: a pack that doesn't exist OR exists in a
|
|
422
|
+
* different org surfaces as `AttestryAPIError` with `status === 404`
|
|
423
|
+
* and a generic "pack not found" message (faithful courier — the
|
|
424
|
+
* kernel `getEvidencePack` query intentionally collapses cross-org
|
|
425
|
+
* and missing to the same response).
|
|
426
|
+
*
|
|
427
|
+
* Errors — ordered by kernel firing precedence. The kernel route at
|
|
428
|
+
* `src/app/api/v1/evidence-packs/[id]/route.ts` validates the URL-path
|
|
429
|
+
* UUID BEFORE the auth check, so a malformed path UUID surfaces as 400
|
|
430
|
+
* BEFORE 401/403 (same ordering as `addBundle`):
|
|
431
|
+
* - `AttestryAPIError` (status 429) — rate limit (auto-retried).
|
|
432
|
+
* - `AttestryAPIError` (status 400 — path UUID) — malformed UUID in
|
|
433
|
+
* the path (kernel `packPathParamsSchema` Zod rejection).
|
|
434
|
+
* **Fires BEFORE auth.** The SDK pre-validates the UUID format so
|
|
435
|
+
* this surface is only reachable via SDK rule changes.
|
|
436
|
+
* - `AttestryAPIError` (status 401 / 403) — auth missing / wrong
|
|
437
|
+
* permission (`READ_ASSESSMENTS`).
|
|
438
|
+
* - `AttestryAPIError` (status 404) — pack missing OR cross-org.
|
|
439
|
+
* - `AttestryAPIError` (status 500) — internal kernel error.
|
|
440
|
+
* - `AttestryError` ("request aborted by caller") — abort.
|
|
441
|
+
* - `AttestryError` (P2 hardening) — kernel response shape
|
|
442
|
+
* violation.
|
|
443
|
+
* - `AttestryAPIError` (P3 hardening) — non-JSON response.
|
|
444
|
+
* - `TypeError` (synchronous, no fetch issued) — input failed
|
|
445
|
+
* SDK-side validation.
|
|
446
|
+
*
|
|
447
|
+
* **SDK-side validation**:
|
|
448
|
+
* - `input`: required; non-null, non-array object.
|
|
449
|
+
* - `input.packId`: required own-property; non-empty string;
|
|
450
|
+
* matching `UUID_REGEX`.
|
|
451
|
+
*
|
|
452
|
+
* **Response-shape validation** (P2 hardening): `pack` field is a
|
|
453
|
+
* full `EvidencePack`; `bundles` field is an array of
|
|
454
|
+
* `ReperformanceBundle` (per-element shape validated).
|
|
455
|
+
*
|
|
456
|
+
* @example
|
|
457
|
+
* ```ts
|
|
458
|
+
* const { pack, bundles } = await client.evidencePack.get({
|
|
459
|
+
* packId: "11111111-1111-1111-1111-111111111111",
|
|
460
|
+
* });
|
|
461
|
+
* console.log(`${pack.packType} pack, status: ${pack.status}`);
|
|
462
|
+
* console.log(`${bundles.length} bundles attached`);
|
|
463
|
+
* ```
|
|
464
|
+
*/
|
|
465
|
+
get(input, options) {
|
|
466
|
+
if (input === null ||
|
|
467
|
+
typeof input !== "object" ||
|
|
468
|
+
Array.isArray(input)) {
|
|
469
|
+
throw new TypeError("evidencePack.get: `input` must be a non-null object with `packId`");
|
|
470
|
+
}
|
|
471
|
+
const hasPackId = objectHasOwn(input, "packId");
|
|
472
|
+
const packIdRaw = hasPackId
|
|
473
|
+
? readInputField(input, "packId", "evidencePack.get")
|
|
474
|
+
: undefined;
|
|
475
|
+
if (!hasPackId || packIdRaw === undefined) {
|
|
476
|
+
throw new TypeError("evidencePack.get: `packId` is required");
|
|
477
|
+
}
|
|
478
|
+
if (typeof packIdRaw !== "string" || packIdRaw.length === 0) {
|
|
479
|
+
throw new TypeError("evidencePack.get: `packId` must be a non-empty string");
|
|
480
|
+
}
|
|
481
|
+
if (!UUID_REGEX.test(packIdRaw)) {
|
|
482
|
+
throw new TypeError("evidencePack.get: `packId` must be an RFC 4122 hyphenated UUID");
|
|
483
|
+
}
|
|
484
|
+
// `packId` is regex-validated to hex+hyphens only — no `/` / `.` /
|
|
485
|
+
// `..` / NUL can reach the encoder. `encodeURIComponent` is belt-
|
|
486
|
+
// and-suspenders.
|
|
487
|
+
const path = `/api/v1/evidence-packs/${encodeURIComponent(packIdRaw)}`;
|
|
488
|
+
return this.client
|
|
489
|
+
._request({
|
|
490
|
+
method: "GET",
|
|
491
|
+
path,
|
|
492
|
+
options,
|
|
493
|
+
})
|
|
494
|
+
.then((result) => {
|
|
495
|
+
if (result === null ||
|
|
496
|
+
typeof result !== "object" ||
|
|
497
|
+
Array.isArray(result)) {
|
|
498
|
+
throw new AttestryError(`evidencePack.get: expected an object response from the kernel ` +
|
|
499
|
+
`(got ${describeType(result)})`);
|
|
500
|
+
}
|
|
501
|
+
const obj = result;
|
|
502
|
+
const packRaw = objectHasOwn(obj, "pack") ? obj.pack : undefined;
|
|
503
|
+
validatePack(packRaw, "evidencePack.get", "response.pack");
|
|
504
|
+
const bundlesRaw = objectHasOwn(obj, "bundles")
|
|
505
|
+
? obj.bundles
|
|
506
|
+
: undefined;
|
|
507
|
+
if (!Array.isArray(bundlesRaw)) {
|
|
508
|
+
throw new AttestryError(`evidencePack.get: expected response.bundles to be an array ` +
|
|
509
|
+
`(got ${describeType(bundlesRaw)})`);
|
|
510
|
+
}
|
|
511
|
+
for (let i = 0; i < bundlesRaw.length; i++) {
|
|
512
|
+
validateBundle(bundlesRaw[i], "evidencePack.get", `response.bundles[${i}]`);
|
|
513
|
+
}
|
|
514
|
+
return result;
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* List the authenticated organization's evidence packs, newest first.
|
|
519
|
+
* Wraps `GET /api/v1/evidence-packs`.
|
|
520
|
+
*
|
|
521
|
+
* **Single page per call** (DEV-63). Pass `response.nextCursor` back
|
|
522
|
+
* as `cursor` to fetch the next page; `nextCursor: null` means no
|
|
523
|
+
* more pages. The kernel pages by tuple comparison over
|
|
524
|
+
* `(created_at DESC, id DESC)` so same-microsecond timestamps do
|
|
525
|
+
* not skip rows.
|
|
526
|
+
*
|
|
527
|
+
* **Filters are AND-combined kernel-side**. Omitting all filters
|
|
528
|
+
* lists the entire org's packs (newest first). Empty `cursor` (`""`)
|
|
529
|
+
* is rejected by the SDK; pass `undefined` (or omit the field) for
|
|
530
|
+
* the first page.
|
|
531
|
+
*
|
|
532
|
+
* Errors — ordered by kernel firing precedence:
|
|
533
|
+
* - `AttestryAPIError` (status 429) — rate limit (auto-retried).
|
|
534
|
+
* - `AttestryAPIError` (status 401 / 403) — auth missing / wrong
|
|
535
|
+
* permission (`READ_ASSESSMENTS`).
|
|
536
|
+
* - `AttestryAPIError` (status 400) — a length-valid but
|
|
537
|
+
* UNDECODABLE `cursor` (`details.code` ===
|
|
538
|
+
* `"evidence_pack.invalid_cursor"`). NOTE: a `cursor` that fails
|
|
539
|
+
* the kernel's Zod length cap (>500 chars) fires EARLIER as 422
|
|
540
|
+
* (below), not 400 — the 400 path is reached only after the query
|
|
541
|
+
* schema accepts the cursor's shape. Since the SDK treats `cursor`
|
|
542
|
+
* as opaque (caller passes back `nextCursor` verbatim), neither is
|
|
543
|
+
* reachable with a kernel-issued cursor.
|
|
544
|
+
* - `AttestryAPIError` (status 422) — Zod query-param validation
|
|
545
|
+
* failed, INCLUDING an over-long (>500-char) `cursor`
|
|
546
|
+
* (`details.code` === `"evidence_pack.validation_failed"`).
|
|
547
|
+
* - `AttestryAPIError` (status 500) — internal kernel error.
|
|
548
|
+
* - `AttestryError` ("request aborted by caller") — abort.
|
|
549
|
+
* - `AttestryError` (P2 hardening) — response-shape violation.
|
|
550
|
+
* - `AttestryAPIError` (P3 hardening) — non-JSON response.
|
|
551
|
+
* - `TypeError` (synchronous, no fetch issued) — input failed
|
|
552
|
+
* SDK-side validation.
|
|
553
|
+
*
|
|
554
|
+
* **SDK-side validation**:
|
|
555
|
+
* - `input` (optional): if provided, non-null, non-array object.
|
|
556
|
+
* - `input.systemId` (when own-present): UUID format.
|
|
557
|
+
* - `input.packType` (when own-present): member of `PACK_TYPES`.
|
|
558
|
+
* - `input.status` (when own-present): member of `PACK_STATUSES`.
|
|
559
|
+
* - `input.limit` (when own-present): `Number.isInteger`, range
|
|
560
|
+
* [1, 200] inclusive. Mirrors kernel `.int().min(1).max(200)`.
|
|
561
|
+
* - `input.cursor` (when own-present): non-empty string.
|
|
562
|
+
*
|
|
563
|
+
* @example First page, all filters omitted
|
|
564
|
+
* ```ts
|
|
565
|
+
* const { items, nextCursor } = await client.evidencePack.list();
|
|
566
|
+
* for (const pack of items) {
|
|
567
|
+
* console.log(pack.id, pack.packType, pack.status);
|
|
568
|
+
* }
|
|
569
|
+
* if (nextCursor) {
|
|
570
|
+
* const next = await client.evidencePack.list({ cursor: nextCursor });
|
|
571
|
+
* }
|
|
572
|
+
* ```
|
|
573
|
+
*
|
|
574
|
+
* @example Filter by system + status + cap to 25
|
|
575
|
+
* ```ts
|
|
576
|
+
* const draft = await client.evidencePack.list({
|
|
577
|
+
* systemId: "11111111-1111-1111-1111-111111111111",
|
|
578
|
+
* status: "draft",
|
|
579
|
+
* limit: 25,
|
|
580
|
+
* });
|
|
581
|
+
* ```
|
|
582
|
+
*/
|
|
583
|
+
list(input, options) {
|
|
584
|
+
// `input` is optional. Reject explicit `null` and non-object
|
|
585
|
+
// explicit values to match the input-shape discipline.
|
|
586
|
+
if (input !== undefined) {
|
|
587
|
+
if (input === null ||
|
|
588
|
+
typeof input !== "object" ||
|
|
589
|
+
Array.isArray(input)) {
|
|
590
|
+
throw new TypeError("evidencePack.list: `input` must be a non-null object when provided");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// From here on `input` is `undefined` OR a non-null non-array
|
|
594
|
+
// object. The own-property snapshots below tolerate either. Each
|
|
595
|
+
// read goes through `readInputField` so a throwing accessor surfaces
|
|
596
|
+
// as the documented synchronous `TypeError` (session-22 hostile
|
|
597
|
+
// MEDIUM-1).
|
|
598
|
+
const safeInput = (input ?? {});
|
|
599
|
+
const hasSystemId = objectHasOwn(safeInput, "systemId");
|
|
600
|
+
const systemIdRaw = hasSystemId
|
|
601
|
+
? readInputField(safeInput, "systemId", "evidencePack.list")
|
|
602
|
+
: undefined;
|
|
603
|
+
const hasPackType = objectHasOwn(safeInput, "packType");
|
|
604
|
+
const packTypeRaw = hasPackType
|
|
605
|
+
? readInputField(safeInput, "packType", "evidencePack.list")
|
|
606
|
+
: undefined;
|
|
607
|
+
const hasStatus = objectHasOwn(safeInput, "status");
|
|
608
|
+
const statusRaw = hasStatus
|
|
609
|
+
? readInputField(safeInput, "status", "evidencePack.list")
|
|
610
|
+
: undefined;
|
|
611
|
+
const hasLimit = objectHasOwn(safeInput, "limit");
|
|
612
|
+
const limitRaw = hasLimit
|
|
613
|
+
? readInputField(safeInput, "limit", "evidencePack.list")
|
|
614
|
+
: undefined;
|
|
615
|
+
const hasCursor = objectHasOwn(safeInput, "cursor");
|
|
616
|
+
const cursorRaw = hasCursor
|
|
617
|
+
? readInputField(safeInput, "cursor", "evidencePack.list")
|
|
618
|
+
: undefined;
|
|
619
|
+
let validatedSystemId;
|
|
620
|
+
if (hasSystemId && systemIdRaw !== undefined) {
|
|
621
|
+
if (typeof systemIdRaw !== "string") {
|
|
622
|
+
throw new TypeError(`evidencePack.list: \`systemId\` must be a string when provided ` +
|
|
623
|
+
`(got ${describeType(systemIdRaw)})`);
|
|
624
|
+
}
|
|
625
|
+
if (!UUID_REGEX.test(systemIdRaw)) {
|
|
626
|
+
throw new TypeError("evidencePack.list: `systemId` must be an RFC 4122 hyphenated UUID");
|
|
627
|
+
}
|
|
628
|
+
validatedSystemId = systemIdRaw;
|
|
629
|
+
}
|
|
630
|
+
let validatedPackType;
|
|
631
|
+
if (hasPackType && packTypeRaw !== undefined) {
|
|
632
|
+
if (typeof packTypeRaw !== "string") {
|
|
633
|
+
throw new TypeError(`evidencePack.list: \`packType\` must be a string when provided ` +
|
|
634
|
+
`(got ${describeType(packTypeRaw)})`);
|
|
635
|
+
}
|
|
636
|
+
if (!PACK_TYPES.includes(packTypeRaw)) {
|
|
637
|
+
throw new TypeError(`evidencePack.list: \`packType\` must be one of ` +
|
|
638
|
+
`${JSON.stringify(PACK_TYPES)} (got ${JSON.stringify(packTypeRaw)})`);
|
|
639
|
+
}
|
|
640
|
+
validatedPackType = packTypeRaw;
|
|
641
|
+
}
|
|
642
|
+
let validatedStatus;
|
|
643
|
+
if (hasStatus && statusRaw !== undefined) {
|
|
644
|
+
if (typeof statusRaw !== "string") {
|
|
645
|
+
throw new TypeError(`evidencePack.list: \`status\` must be a string when provided ` +
|
|
646
|
+
`(got ${describeType(statusRaw)})`);
|
|
647
|
+
}
|
|
648
|
+
if (!PACK_STATUSES.includes(statusRaw)) {
|
|
649
|
+
throw new TypeError(`evidencePack.list: \`status\` must be one of ` +
|
|
650
|
+
`${JSON.stringify(PACK_STATUSES)} (got ${JSON.stringify(statusRaw)})`);
|
|
651
|
+
}
|
|
652
|
+
validatedStatus = statusRaw;
|
|
653
|
+
}
|
|
654
|
+
let validatedLimit;
|
|
655
|
+
if (hasLimit && limitRaw !== undefined) {
|
|
656
|
+
if (typeof limitRaw !== "number") {
|
|
657
|
+
throw new TypeError(`evidencePack.list: \`limit\` must be a number when provided ` +
|
|
658
|
+
`(got ${describeType(limitRaw)})`);
|
|
659
|
+
}
|
|
660
|
+
if (!Number.isInteger(limitRaw)) {
|
|
661
|
+
throw new TypeError(`evidencePack.list: \`limit\` must be a finite integer ` +
|
|
662
|
+
`(got ${limitRaw})`);
|
|
663
|
+
}
|
|
664
|
+
if (limitRaw < 1 || limitRaw > MAX_LIST_LIMIT) {
|
|
665
|
+
throw new TypeError(`evidencePack.list: \`limit\` must be in the range [1, ` +
|
|
666
|
+
`${MAX_LIST_LIMIT}] (got ${limitRaw})`);
|
|
667
|
+
}
|
|
668
|
+
validatedLimit = limitRaw;
|
|
669
|
+
}
|
|
670
|
+
let validatedCursor;
|
|
671
|
+
if (hasCursor && cursorRaw !== undefined) {
|
|
672
|
+
if (typeof cursorRaw !== "string") {
|
|
673
|
+
throw new TypeError(`evidencePack.list: \`cursor\` must be a string when provided ` +
|
|
674
|
+
`(got ${describeType(cursorRaw)})`);
|
|
675
|
+
}
|
|
676
|
+
if (cursorRaw.length === 0) {
|
|
677
|
+
throw new TypeError("evidencePack.list: `cursor` must be a non-empty string when provided");
|
|
678
|
+
}
|
|
679
|
+
validatedCursor = cursorRaw;
|
|
680
|
+
}
|
|
681
|
+
// Build the query record. The transport's `encodeQuery` drops
|
|
682
|
+
// undefined values, so an omitted filter is never emitted — the
|
|
683
|
+
// kernel then applies its own `.default(50)` on `limit` etc.
|
|
684
|
+
// (closed-default invariant carry-forward).
|
|
685
|
+
const query = {
|
|
686
|
+
systemId: validatedSystemId,
|
|
687
|
+
packType: validatedPackType,
|
|
688
|
+
status: validatedStatus,
|
|
689
|
+
limit: validatedLimit,
|
|
690
|
+
cursor: validatedCursor,
|
|
691
|
+
};
|
|
692
|
+
return this.client
|
|
693
|
+
._request({
|
|
694
|
+
method: "GET",
|
|
695
|
+
path: "/api/v1/evidence-packs",
|
|
696
|
+
query,
|
|
697
|
+
options,
|
|
698
|
+
})
|
|
699
|
+
.then((result) => {
|
|
700
|
+
if (result === null ||
|
|
701
|
+
typeof result !== "object" ||
|
|
702
|
+
Array.isArray(result)) {
|
|
703
|
+
throw new AttestryError(`evidencePack.list: expected an object response from the kernel ` +
|
|
704
|
+
`(got ${describeType(result)})`);
|
|
705
|
+
}
|
|
706
|
+
const obj = result;
|
|
707
|
+
const itemsRaw = objectHasOwn(obj, "items") ? obj.items : undefined;
|
|
708
|
+
if (!Array.isArray(itemsRaw)) {
|
|
709
|
+
throw new AttestryError(`evidencePack.list: expected response.items to be an array ` +
|
|
710
|
+
`(got ${describeType(itemsRaw)})`);
|
|
711
|
+
}
|
|
712
|
+
for (let i = 0; i < itemsRaw.length; i++) {
|
|
713
|
+
validatePack(itemsRaw[i], "evidencePack.list", `response.items[${i}]`);
|
|
714
|
+
}
|
|
715
|
+
const nextCursorRaw = objectHasOwn(obj, "nextCursor")
|
|
716
|
+
? obj.nextCursor
|
|
717
|
+
: undefined;
|
|
718
|
+
if (nextCursorRaw !== null && typeof nextCursorRaw !== "string") {
|
|
719
|
+
throw new AttestryError(`evidencePack.list: expected response.nextCursor to be a string ` +
|
|
720
|
+
`or null (got ${describeType(nextCursorRaw)})`);
|
|
721
|
+
}
|
|
722
|
+
return result;
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Append a reperformance bundle to an existing **draft** evidence
|
|
727
|
+
* pack. Wraps `POST /api/v1/evidence-packs/{id}/bundles`.
|
|
728
|
+
*
|
|
729
|
+
* The kernel recomputes the pack's `content_hash` after the append
|
|
730
|
+
* and returns the updated pack alongside the new bundle. A
|
|
731
|
+
* `hashCollision` flag is set when the new `(inputs_hash,
|
|
732
|
+
* outputs_hash)` tuple matches any existing bundle on the SAME pack
|
|
733
|
+
* — flagged but NOT blocked (P1.2 DEV-17, faithful courier).
|
|
734
|
+
*
|
|
735
|
+
* **State invariant**: the pack must be in `draft` status. A
|
|
736
|
+
* non-draft pack (`signed`, `superseded`, `revoked`, `expired`)
|
|
737
|
+
* rejects with `AttestryAPIError` status 409 (`details.code` ===
|
|
738
|
+
* `"evidence_pack.invalid_state"`; `details.currentStatus` carries
|
|
739
|
+
* the pack's current state).
|
|
740
|
+
*
|
|
741
|
+
* **Method name `addBundle`** — see resource header for the
|
|
742
|
+
* `addBundle` vs `appendBundle` decision.
|
|
743
|
+
*
|
|
744
|
+
* **Idempotency**: same carry-forward as `create` — the kernel
|
|
745
|
+
* accepts `Idempotency-Key` but the SDK doesn't expose the header
|
|
746
|
+
* in P1.6.
|
|
747
|
+
*
|
|
748
|
+
* Errors — ordered by kernel firing precedence. The kernel route at
|
|
749
|
+
* `src/app/api/v1/evidence-packs/[id]/bundles/route.ts` validates the
|
|
750
|
+
* URL-path UUID BEFORE the auth check, so a malformed path UUID
|
|
751
|
+
* surfaces as 400 BEFORE 401/403. Body-parse 400s and idempotency-
|
|
752
|
+
* key 400s fire AFTER auth (matches the `get` JSDoc shape):
|
|
753
|
+
* - `AttestryAPIError` (status 429) — rate limit (auto-retried).
|
|
754
|
+
* - `AttestryAPIError` (status 400 — path UUID) — malformed
|
|
755
|
+
* URL-path packId. **Fires BEFORE auth** (the kernel
|
|
756
|
+
* `packPathParamsSchema.safeParse` runs first). The SDK
|
|
757
|
+
* pre-validates the path UUID so this surface is only reachable
|
|
758
|
+
* via SDK rule changes.
|
|
759
|
+
* - `AttestryAPIError` (status 401 / 403) — auth missing / wrong
|
|
760
|
+
* permission (`WRITE_ASSESSMENTS`).
|
|
761
|
+
* - `AttestryAPIError` (status 400 — JSON parse / idempotency-key
|
|
762
|
+
* format) — malformed JSON body OR malformed `Idempotency-Key`
|
|
763
|
+
* header. **Fires AFTER auth** (the kernel parses these after
|
|
764
|
+
* `requireSessionOrApiKey` resolves).
|
|
765
|
+
* - `AttestryAPIError` (status 404) — pack missing OR cross-org.
|
|
766
|
+
* - `AttestryAPIError` (status 409) — invalid state (carries
|
|
767
|
+
* `details.currentStatus`) OR idempotency conflict.
|
|
768
|
+
* - `AttestryAPIError` (status 413) — canonical bundle list >
|
|
769
|
+
* 256 KiB (kernel `PayloadTooLargeError`).
|
|
770
|
+
* - `AttestryAPIError` (status 422) — Zod validation failed.
|
|
771
|
+
* - `AttestryAPIError` (status 500) — internal kernel error.
|
|
772
|
+
* - `AttestryError` ("request aborted by caller") — abort.
|
|
773
|
+
* - `AttestryError` (P2 hardening) — response-shape violation.
|
|
774
|
+
* - `AttestryAPIError` (P3 hardening) — non-JSON response.
|
|
775
|
+
* - `TypeError` (synchronous, no fetch issued) — input failed
|
|
776
|
+
* SDK-side validation.
|
|
777
|
+
*
|
|
778
|
+
* **SDK-side validation**:
|
|
779
|
+
* - `input`: required; non-null, non-array object.
|
|
780
|
+
* - `input.packId`: required own-property; non-empty UUID string.
|
|
781
|
+
* - `input.traceContent`: required own-property; array of length
|
|
782
|
+
* ≤1000. Per-entry shape is open-spec (kernel deep-validates).
|
|
783
|
+
* - `input.inputsHash`: required own-property; non-empty string;
|
|
784
|
+
* length ≤500.
|
|
785
|
+
* - `input.outputsHash`: required own-property; non-empty string;
|
|
786
|
+
* length ≤500.
|
|
787
|
+
* - `input.modelBehaviorLog` (when own-present): non-null,
|
|
788
|
+
* non-array object. Inner shape open-spec.
|
|
789
|
+
* - `input.corroborationResults` (when own-present): non-null,
|
|
790
|
+
* non-array object. Inner shape open-spec.
|
|
791
|
+
* - `input.storageUri` (when own-present): non-empty string;
|
|
792
|
+
* length ≤2000. Scheme validation kernel-authoritative.
|
|
793
|
+
* - `input.metadata` (when own-present): non-null, non-array
|
|
794
|
+
* object.
|
|
795
|
+
*
|
|
796
|
+
* **Response-shape validation** (P2 hardening): `bundle` is a
|
|
797
|
+
* `ReperformanceBundle`; `pack` is an `EvidencePack`; `hashCollision`
|
|
798
|
+
* is the 3-field `HashCollision` block.
|
|
799
|
+
*
|
|
800
|
+
* @example Append a bundle to a draft pack
|
|
801
|
+
* ```ts
|
|
802
|
+
* const { bundle, pack, hashCollision } = await client.evidencePack.addBundle({
|
|
803
|
+
* packId: "11111111-1111-1111-1111-111111111111",
|
|
804
|
+
* traceContent: [
|
|
805
|
+
* { action: "ingest", timestamp: "2026-05-18T12:00:00Z" },
|
|
806
|
+
* { action: "extract", timestamp: "2026-05-18T12:00:01Z" },
|
|
807
|
+
* ],
|
|
808
|
+
* inputsHash: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
|
809
|
+
* outputsHash: "sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
|
810
|
+
* });
|
|
811
|
+
* console.log(`appended bundle ${bundle.id}; pack hash now ${pack.contentHash}`);
|
|
812
|
+
* if (hashCollision.detected) {
|
|
813
|
+
* console.warn(`duplicate bundle — ${hashCollision.count} prior matches`);
|
|
814
|
+
* }
|
|
815
|
+
* ```
|
|
816
|
+
*/
|
|
817
|
+
addBundle(input, options) {
|
|
818
|
+
if (input === null ||
|
|
819
|
+
typeof input !== "object" ||
|
|
820
|
+
Array.isArray(input)) {
|
|
821
|
+
throw new TypeError("evidencePack.addBundle: `input` must be a non-null object with " +
|
|
822
|
+
"`packId`, `traceContent`, `inputsHash`, `outputsHash`");
|
|
823
|
+
}
|
|
824
|
+
// Snapshot each field via `readInputField` so a throwing accessor
|
|
825
|
+
// surfaces as the documented synchronous `TypeError` input contract
|
|
826
|
+
// (session-22 hostile MEDIUM-1); the `objectHasOwn` presence check
|
|
827
|
+
// is a separate prototype-pollution defense.
|
|
828
|
+
const hasPackId = objectHasOwn(input, "packId");
|
|
829
|
+
const packIdRaw = hasPackId
|
|
830
|
+
? readInputField(input, "packId", "evidencePack.addBundle")
|
|
831
|
+
: undefined;
|
|
832
|
+
const hasTraceContent = objectHasOwn(input, "traceContent");
|
|
833
|
+
const traceContentRaw = hasTraceContent
|
|
834
|
+
? readInputField(input, "traceContent", "evidencePack.addBundle")
|
|
835
|
+
: undefined;
|
|
836
|
+
const hasInputsHash = objectHasOwn(input, "inputsHash");
|
|
837
|
+
const inputsHashRaw = hasInputsHash
|
|
838
|
+
? readInputField(input, "inputsHash", "evidencePack.addBundle")
|
|
839
|
+
: undefined;
|
|
840
|
+
const hasOutputsHash = objectHasOwn(input, "outputsHash");
|
|
841
|
+
const outputsHashRaw = hasOutputsHash
|
|
842
|
+
? readInputField(input, "outputsHash", "evidencePack.addBundle")
|
|
843
|
+
: undefined;
|
|
844
|
+
const hasModelBehaviorLog = objectHasOwn(input, "modelBehaviorLog");
|
|
845
|
+
const modelBehaviorLogRaw = hasModelBehaviorLog
|
|
846
|
+
? readInputField(input, "modelBehaviorLog", "evidencePack.addBundle")
|
|
847
|
+
: undefined;
|
|
848
|
+
const hasCorroborationResults = objectHasOwn(input, "corroborationResults");
|
|
849
|
+
const corroborationResultsRaw = hasCorroborationResults
|
|
850
|
+
? readInputField(input, "corroborationResults", "evidencePack.addBundle")
|
|
851
|
+
: undefined;
|
|
852
|
+
const hasStorageUri = objectHasOwn(input, "storageUri");
|
|
853
|
+
const storageUriRaw = hasStorageUri
|
|
854
|
+
? readInputField(input, "storageUri", "evidencePack.addBundle")
|
|
855
|
+
: undefined;
|
|
856
|
+
const hasMetadata = objectHasOwn(input, "metadata");
|
|
857
|
+
const metadataRaw = hasMetadata
|
|
858
|
+
? readInputField(input, "metadata", "evidencePack.addBundle")
|
|
859
|
+
: undefined;
|
|
860
|
+
// packId REQUIRED.
|
|
861
|
+
if (!hasPackId || packIdRaw === undefined) {
|
|
862
|
+
throw new TypeError("evidencePack.addBundle: `packId` is required");
|
|
863
|
+
}
|
|
864
|
+
if (typeof packIdRaw !== "string" || packIdRaw.length === 0) {
|
|
865
|
+
throw new TypeError("evidencePack.addBundle: `packId` must be a non-empty string");
|
|
866
|
+
}
|
|
867
|
+
if (!UUID_REGEX.test(packIdRaw)) {
|
|
868
|
+
throw new TypeError("evidencePack.addBundle: `packId` must be an RFC 4122 hyphenated UUID");
|
|
869
|
+
}
|
|
870
|
+
// traceContent REQUIRED + array + length cap. Snapshot via Array.from.
|
|
871
|
+
if (!hasTraceContent || traceContentRaw === undefined) {
|
|
872
|
+
throw new TypeError("evidencePack.addBundle: `traceContent` is required");
|
|
873
|
+
}
|
|
874
|
+
if (!Array.isArray(traceContentRaw)) {
|
|
875
|
+
throw new TypeError(`evidencePack.addBundle: \`traceContent\` must be an array ` +
|
|
876
|
+
`(got ${describeType(traceContentRaw)})`);
|
|
877
|
+
}
|
|
878
|
+
const validatedTraceContent = Array.from(traceContentRaw);
|
|
879
|
+
if (validatedTraceContent.length > MAX_TRACE_CONTENT_LENGTH) {
|
|
880
|
+
throw new TypeError(`evidencePack.addBundle: \`traceContent\` array exceeds the ` +
|
|
881
|
+
`kernel's max length of ${MAX_TRACE_CONTENT_LENGTH} (got ` +
|
|
882
|
+
`${validatedTraceContent.length})`);
|
|
883
|
+
}
|
|
884
|
+
// inputsHash REQUIRED + non-empty + length cap.
|
|
885
|
+
if (!hasInputsHash || inputsHashRaw === undefined) {
|
|
886
|
+
throw new TypeError("evidencePack.addBundle: `inputsHash` is required");
|
|
887
|
+
}
|
|
888
|
+
if (typeof inputsHashRaw !== "string") {
|
|
889
|
+
throw new TypeError(`evidencePack.addBundle: \`inputsHash\` must be a string ` +
|
|
890
|
+
`(got ${describeType(inputsHashRaw)})`);
|
|
891
|
+
}
|
|
892
|
+
if (inputsHashRaw.length === 0) {
|
|
893
|
+
throw new TypeError("evidencePack.addBundle: `inputsHash` must be a non-empty string");
|
|
894
|
+
}
|
|
895
|
+
if (inputsHashRaw.length > MAX_HASH_LENGTH) {
|
|
896
|
+
throw new TypeError(`evidencePack.addBundle: \`inputsHash\` exceeds the maximum length ` +
|
|
897
|
+
`of ${MAX_HASH_LENGTH} characters (got ${inputsHashRaw.length})`);
|
|
898
|
+
}
|
|
899
|
+
// Capture the validated value into a typed local (matches the
|
|
900
|
+
// `validated*` discipline used for every other field) so the body is
|
|
901
|
+
// built ONLY from validated locals — a future refactor that moved
|
|
902
|
+
// the read or changed `inputsHashRaw`'s type cannot silently ship an
|
|
903
|
+
// unvalidated value (hostile-review F-CR-1).
|
|
904
|
+
const validatedInputsHash = inputsHashRaw;
|
|
905
|
+
// outputsHash REQUIRED + non-empty + length cap.
|
|
906
|
+
if (!hasOutputsHash || outputsHashRaw === undefined) {
|
|
907
|
+
throw new TypeError("evidencePack.addBundle: `outputsHash` is required");
|
|
908
|
+
}
|
|
909
|
+
if (typeof outputsHashRaw !== "string") {
|
|
910
|
+
throw new TypeError(`evidencePack.addBundle: \`outputsHash\` must be a string ` +
|
|
911
|
+
`(got ${describeType(outputsHashRaw)})`);
|
|
912
|
+
}
|
|
913
|
+
if (outputsHashRaw.length === 0) {
|
|
914
|
+
throw new TypeError("evidencePack.addBundle: `outputsHash` must be a non-empty string");
|
|
915
|
+
}
|
|
916
|
+
if (outputsHashRaw.length > MAX_HASH_LENGTH) {
|
|
917
|
+
throw new TypeError(`evidencePack.addBundle: \`outputsHash\` exceeds the maximum length ` +
|
|
918
|
+
`of ${MAX_HASH_LENGTH} characters (got ${outputsHashRaw.length})`);
|
|
919
|
+
}
|
|
920
|
+
// Capture the validated value into a typed local (F-CR-1 — same
|
|
921
|
+
// rationale as `validatedInputsHash`).
|
|
922
|
+
const validatedOutputsHash = outputsHashRaw;
|
|
923
|
+
// Optional modelBehaviorLog — non-null non-array object.
|
|
924
|
+
let validatedModelBehaviorLog;
|
|
925
|
+
if (hasModelBehaviorLog && modelBehaviorLogRaw !== undefined) {
|
|
926
|
+
if (modelBehaviorLogRaw === null ||
|
|
927
|
+
typeof modelBehaviorLogRaw !== "object" ||
|
|
928
|
+
Array.isArray(modelBehaviorLogRaw)) {
|
|
929
|
+
throw new TypeError(`evidencePack.addBundle: \`modelBehaviorLog\` must be a non-null ` +
|
|
930
|
+
`object when provided (got ${describeType(modelBehaviorLogRaw)})`);
|
|
931
|
+
}
|
|
932
|
+
validatedModelBehaviorLog = modelBehaviorLogRaw;
|
|
933
|
+
}
|
|
934
|
+
// Optional corroborationResults — non-null non-array object.
|
|
935
|
+
let validatedCorroborationResults;
|
|
936
|
+
if (hasCorroborationResults && corroborationResultsRaw !== undefined) {
|
|
937
|
+
if (corroborationResultsRaw === null ||
|
|
938
|
+
typeof corroborationResultsRaw !== "object" ||
|
|
939
|
+
Array.isArray(corroborationResultsRaw)) {
|
|
940
|
+
throw new TypeError(`evidencePack.addBundle: \`corroborationResults\` must be a non-` +
|
|
941
|
+
`null object when provided (got ` +
|
|
942
|
+
`${describeType(corroborationResultsRaw)})`);
|
|
943
|
+
}
|
|
944
|
+
validatedCorroborationResults =
|
|
945
|
+
corroborationResultsRaw;
|
|
946
|
+
}
|
|
947
|
+
// Optional storageUri — non-empty string + length cap. Scheme is
|
|
948
|
+
// kernel-authoritative (faithful courier — same as vision.ts
|
|
949
|
+
// imageUri).
|
|
950
|
+
let validatedStorageUri;
|
|
951
|
+
if (hasStorageUri && storageUriRaw !== undefined) {
|
|
952
|
+
if (typeof storageUriRaw !== "string") {
|
|
953
|
+
throw new TypeError(`evidencePack.addBundle: \`storageUri\` must be a string when ` +
|
|
954
|
+
`provided (got ${describeType(storageUriRaw)})`);
|
|
955
|
+
}
|
|
956
|
+
if (storageUriRaw.length === 0) {
|
|
957
|
+
throw new TypeError("evidencePack.addBundle: `storageUri` must be a non-empty string " +
|
|
958
|
+
"when provided");
|
|
959
|
+
}
|
|
960
|
+
if (storageUriRaw.length > MAX_STORAGE_URI_LENGTH) {
|
|
961
|
+
throw new TypeError(`evidencePack.addBundle: \`storageUri\` exceeds the maximum ` +
|
|
962
|
+
`length of ${MAX_STORAGE_URI_LENGTH} characters (got ` +
|
|
963
|
+
`${storageUriRaw.length})`);
|
|
964
|
+
}
|
|
965
|
+
validatedStorageUri = storageUriRaw;
|
|
966
|
+
}
|
|
967
|
+
// Optional metadata — non-null non-array object.
|
|
968
|
+
let validatedMetadata;
|
|
969
|
+
if (hasMetadata && metadataRaw !== undefined) {
|
|
970
|
+
if (metadataRaw === null ||
|
|
971
|
+
typeof metadataRaw !== "object" ||
|
|
972
|
+
Array.isArray(metadataRaw)) {
|
|
973
|
+
throw new TypeError(`evidencePack.addBundle: \`metadata\` must be a non-null object ` +
|
|
974
|
+
`when provided (got ${describeType(metadataRaw)})`);
|
|
975
|
+
}
|
|
976
|
+
validatedMetadata = metadataRaw;
|
|
977
|
+
}
|
|
978
|
+
// Build the body. `packId` rides the URL path, NOT the body.
|
|
979
|
+
const body = {
|
|
980
|
+
traceContent: validatedTraceContent,
|
|
981
|
+
inputsHash: validatedInputsHash,
|
|
982
|
+
outputsHash: validatedOutputsHash,
|
|
983
|
+
};
|
|
984
|
+
if (validatedModelBehaviorLog !== undefined) {
|
|
985
|
+
body.modelBehaviorLog = validatedModelBehaviorLog;
|
|
986
|
+
}
|
|
987
|
+
if (validatedCorroborationResults !== undefined) {
|
|
988
|
+
body.corroborationResults = validatedCorroborationResults;
|
|
989
|
+
}
|
|
990
|
+
if (validatedStorageUri !== undefined) {
|
|
991
|
+
body.storageUri = validatedStorageUri;
|
|
992
|
+
}
|
|
993
|
+
if (validatedMetadata !== undefined) {
|
|
994
|
+
body.metadata = validatedMetadata;
|
|
995
|
+
}
|
|
996
|
+
const path = `/api/v1/evidence-packs/${encodeURIComponent(packIdRaw)}/bundles`;
|
|
997
|
+
return this.client
|
|
998
|
+
._request({
|
|
999
|
+
method: "POST",
|
|
1000
|
+
path,
|
|
1001
|
+
body,
|
|
1002
|
+
options,
|
|
1003
|
+
})
|
|
1004
|
+
.then((result) => {
|
|
1005
|
+
if (result === null ||
|
|
1006
|
+
typeof result !== "object" ||
|
|
1007
|
+
Array.isArray(result)) {
|
|
1008
|
+
throw new AttestryError(`evidencePack.addBundle: expected an object response from the ` +
|
|
1009
|
+
`kernel (got ${describeType(result)})`);
|
|
1010
|
+
}
|
|
1011
|
+
const obj = result;
|
|
1012
|
+
const bundleRaw = objectHasOwn(obj, "bundle") ? obj.bundle : undefined;
|
|
1013
|
+
validateBundle(bundleRaw, "evidencePack.addBundle", "response.bundle");
|
|
1014
|
+
const packRaw = objectHasOwn(obj, "pack") ? obj.pack : undefined;
|
|
1015
|
+
validatePack(packRaw, "evidencePack.addBundle", "response.pack");
|
|
1016
|
+
const hashCollisionRaw = objectHasOwn(obj, "hashCollision")
|
|
1017
|
+
? obj.hashCollision
|
|
1018
|
+
: undefined;
|
|
1019
|
+
validateHashCollision(hashCollisionRaw, "evidencePack.addBundle", "response.hashCollision");
|
|
1020
|
+
return result;
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Sign a draft evidence pack, transitioning it `draft → signed` and
|
|
1025
|
+
* finalizing it into an auditor-visible compliance artifact. Wraps
|
|
1026
|
+
* `POST /api/v1/evidence-packs/{id}/sign`.
|
|
1027
|
+
*
|
|
1028
|
+
* The kernel recomputes the pack's `content_hash` over its current
|
|
1029
|
+
* bundle list on sign (never trusting the stored column), writes
|
|
1030
|
+
* `signed_at` + `signed_by_user_id` + (when provided)
|
|
1031
|
+
* `attestation_certificate_id`, and appends an `evidence_pack.signed`
|
|
1032
|
+
* audit-log entry — all atomic inside one per-org-locked transaction.
|
|
1033
|
+
*
|
|
1034
|
+
* **Auth: ADMIN-only** — the kernel gates `sessionRoles:['admin']` +
|
|
1035
|
+
* `apiKeyPermissions:[ADMIN]`. A non-admin key → 403.
|
|
1036
|
+
*
|
|
1037
|
+
* **Empty-pack guard**: signing a pack with no bundles → 409 with
|
|
1038
|
+
* `details.code === "evidence_pack.empty"` (a dedicated `EmptyPackError`,
|
|
1039
|
+
* NOT `InvalidStateError` — so it carries NO `currentStatus`; the pack
|
|
1040
|
+
* IS in the right `draft` pre-sign state, it just has nothing to sign).
|
|
1041
|
+
*
|
|
1042
|
+
* **Idempotency**: the kernel does NOT honor `Idempotency-Key` on sign
|
|
1043
|
+
* (a replay 409s with `currentStatus='signed'`); the SDK sends none.
|
|
1044
|
+
*
|
|
1045
|
+
* Errors — ordered by kernel firing precedence. The route validates the
|
|
1046
|
+
* URL-path UUID via `packPathParamsSchema.safeParse` BEFORE
|
|
1047
|
+
* `requireSessionOrApiKey`, so a malformed path UUID surfaces as 400
|
|
1048
|
+
* BEFORE 401/403:
|
|
1049
|
+
* - `AttestryAPIError` (status 429) — rate limit (auto-retried).
|
|
1050
|
+
* - `AttestryAPIError` (status 400 — path UUID) — malformed URL-path
|
|
1051
|
+
* packId. **Fires BEFORE auth.** The SDK pre-validates the UUID, so
|
|
1052
|
+
* this surface is only reachable via SDK rule changes.
|
|
1053
|
+
* - `AttestryAPIError` (status 401 / 403) — auth missing / key is not
|
|
1054
|
+
* ADMIN.
|
|
1055
|
+
* - `AttestryAPIError` (status 400 — JSON parse) — malformed body.
|
|
1056
|
+
* **Fires AFTER auth.**
|
|
1057
|
+
* - `AttestryAPIError` (status 422) — Zod validation failed
|
|
1058
|
+
* (`details.code === "evidence_pack.validation_failed"`).
|
|
1059
|
+
* - `AttestryAPIError` (status 404) — pack missing OR cross-org OR
|
|
1060
|
+
* (when an `attestationCertificateId` is supplied) the cert is
|
|
1061
|
+
* missing / cross-org / cross-system (anti-enumeration — same
|
|
1062
|
+
* "pack not found" message).
|
|
1063
|
+
* - `AttestryAPIError` (status 409) — `InvalidStateError` (pack not in
|
|
1064
|
+
* `draft`; `details.currentStatus` carries the state) OR
|
|
1065
|
+
* `EmptyPackError` (`details.code === "evidence_pack.empty"`).
|
|
1066
|
+
* - `AttestryAPIError` (status 500) — internal kernel error.
|
|
1067
|
+
* - `AttestryError` ("request aborted by caller") — abort.
|
|
1068
|
+
* - `AttestryError` (P2 hardening) — response-shape violation.
|
|
1069
|
+
* - `AttestryAPIError` (P3 hardening) — non-JSON response.
|
|
1070
|
+
* - `TypeError` (synchronous, no fetch issued) — input failed
|
|
1071
|
+
* SDK-side validation.
|
|
1072
|
+
*
|
|
1073
|
+
* **SDK-side validation**:
|
|
1074
|
+
* - `input`: required; non-null, non-array object.
|
|
1075
|
+
* - `input.packId`: required own-property; non-empty UUID string.
|
|
1076
|
+
* - `input.attestationCertificateId` (when own-present): UUID format.
|
|
1077
|
+
*
|
|
1078
|
+
* **Response-shape validation** (P2 hardening): the signed `EvidencePack`.
|
|
1079
|
+
*
|
|
1080
|
+
* @example
|
|
1081
|
+
* ```ts
|
|
1082
|
+
* const signed = await client.evidencePack.sign({
|
|
1083
|
+
* packId: "11111111-1111-1111-1111-111111111111",
|
|
1084
|
+
* });
|
|
1085
|
+
* console.log(signed.status, signed.contentHash); // "signed", "sha256:..."
|
|
1086
|
+
* ```
|
|
1087
|
+
*/
|
|
1088
|
+
sign(input, options) {
|
|
1089
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
1090
|
+
throw new TypeError("evidencePack.sign: `input` must be a non-null object with `packId`");
|
|
1091
|
+
}
|
|
1092
|
+
const hasPackId = objectHasOwn(input, "packId");
|
|
1093
|
+
const packIdRaw = hasPackId
|
|
1094
|
+
? readInputField(input, "packId", "evidencePack.sign")
|
|
1095
|
+
: undefined;
|
|
1096
|
+
const hasAttestationCertificateId = objectHasOwn(input, "attestationCertificateId");
|
|
1097
|
+
const attestationCertificateIdRaw = hasAttestationCertificateId
|
|
1098
|
+
? readInputField(input, "attestationCertificateId", "evidencePack.sign")
|
|
1099
|
+
: undefined;
|
|
1100
|
+
if (!hasPackId || packIdRaw === undefined) {
|
|
1101
|
+
throw new TypeError("evidencePack.sign: `packId` is required");
|
|
1102
|
+
}
|
|
1103
|
+
if (typeof packIdRaw !== "string" || packIdRaw.length === 0) {
|
|
1104
|
+
throw new TypeError("evidencePack.sign: `packId` must be a non-empty string");
|
|
1105
|
+
}
|
|
1106
|
+
if (!UUID_REGEX.test(packIdRaw)) {
|
|
1107
|
+
throw new TypeError("evidencePack.sign: `packId` must be an RFC 4122 hyphenated UUID");
|
|
1108
|
+
}
|
|
1109
|
+
let validatedAttestationCertificateId;
|
|
1110
|
+
if (hasAttestationCertificateId &&
|
|
1111
|
+
attestationCertificateIdRaw !== undefined) {
|
|
1112
|
+
if (typeof attestationCertificateIdRaw !== "string") {
|
|
1113
|
+
throw new TypeError(`evidencePack.sign: \`attestationCertificateId\` must be a string ` +
|
|
1114
|
+
`when provided (got ${describeType(attestationCertificateIdRaw)})`);
|
|
1115
|
+
}
|
|
1116
|
+
if (!UUID_REGEX.test(attestationCertificateIdRaw)) {
|
|
1117
|
+
throw new TypeError("evidencePack.sign: `attestationCertificateId` must be an RFC 4122 " +
|
|
1118
|
+
"hyphenated UUID");
|
|
1119
|
+
}
|
|
1120
|
+
validatedAttestationCertificateId = attestationCertificateIdRaw;
|
|
1121
|
+
}
|
|
1122
|
+
// Body from validated locals only (F-CR-1). Always an object (never
|
|
1123
|
+
// undefined) so the transport sets Content-Type and the kernel route's
|
|
1124
|
+
// `parseBody` gets valid JSON; `{}` when no cert (mirrors the MCP tool).
|
|
1125
|
+
const body = {};
|
|
1126
|
+
if (validatedAttestationCertificateId !== undefined) {
|
|
1127
|
+
body.attestationCertificateId = validatedAttestationCertificateId;
|
|
1128
|
+
}
|
|
1129
|
+
const path = `/api/v1/evidence-packs/${encodeURIComponent(packIdRaw)}/sign`;
|
|
1130
|
+
return this.client
|
|
1131
|
+
._request({
|
|
1132
|
+
method: "POST",
|
|
1133
|
+
path,
|
|
1134
|
+
body,
|
|
1135
|
+
options,
|
|
1136
|
+
})
|
|
1137
|
+
.then((result) => {
|
|
1138
|
+
validatePack(result, "evidencePack.sign", "response");
|
|
1139
|
+
return result;
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Supersede a signed evidence pack: transitions the old pack
|
|
1144
|
+
* `signed → superseded` and creates a NEW draft pack linked to it
|
|
1145
|
+
* (`parent_pack_id = oldPackId`). Wraps
|
|
1146
|
+
* `POST /api/v1/evidence-packs/{id}/supersede`.
|
|
1147
|
+
*
|
|
1148
|
+
* Both packs are returned (`{newPack, oldPack}`, HTTP 201). The two
|
|
1149
|
+
* operations + the audit-log entry commit atomically inside one
|
|
1150
|
+
* per-org-locked transaction.
|
|
1151
|
+
*
|
|
1152
|
+
* **Auth**: WRITE_ASSESSMENTS (NOT admin — supersede is a normal write).
|
|
1153
|
+
*
|
|
1154
|
+
* **`newPack` includes `consumerHints`** (unlike `create`, which omits
|
|
1155
|
+
* it) — mirroring the kernel `supersedeNewPackPayloadSchema` and the
|
|
1156
|
+
* P1.7 MCP supersede tool.
|
|
1157
|
+
*
|
|
1158
|
+
* **Idempotency**: the kernel route honors `Idempotency-Key` on
|
|
1159
|
+
* supersede, but the SDK does NOT send it (R-2 carry-forward — same as
|
|
1160
|
+
* `create` / `addBundle`). Consumers needing safe retry today should
|
|
1161
|
+
* dedupe client-side.
|
|
1162
|
+
*
|
|
1163
|
+
* Errors — ordered by kernel firing precedence (path-uuid 400 BEFORE
|
|
1164
|
+
* auth). The SDK does not send `Idempotency-Key`, so the idempotency-
|
|
1165
|
+
* format-400 / idempotency-conflict-409 surfaces are unreachable from
|
|
1166
|
+
* the SDK:
|
|
1167
|
+
* - `AttestryAPIError` (status 429) — rate limit (auto-retried).
|
|
1168
|
+
* - `AttestryAPIError` (status 400 — path UUID) — malformed URL-path
|
|
1169
|
+
* packId. **Fires BEFORE auth.** Reachable only via SDK rule changes.
|
|
1170
|
+
* - `AttestryAPIError` (status 401 / 403) — auth missing / lacks
|
|
1171
|
+
* WRITE_ASSESSMENTS.
|
|
1172
|
+
* - `AttestryAPIError` (status 400 — JSON parse) — malformed body.
|
|
1173
|
+
* - `AttestryAPIError` (status 422) — Zod validation failed on
|
|
1174
|
+
* `newPack` (`details.code === "evidence_pack.validation_failed"`).
|
|
1175
|
+
* - `AttestryAPIError` (status 404) — old pack missing OR cross-org.
|
|
1176
|
+
* - `AttestryAPIError` (status 409) — `InvalidStateError` (old pack not
|
|
1177
|
+
* in `signed` state; `details.currentStatus` carries the state).
|
|
1178
|
+
* - `AttestryAPIError` (status 500) — internal kernel error.
|
|
1179
|
+
* - `AttestryError` ("request aborted by caller") — abort.
|
|
1180
|
+
* - `AttestryError` (P2 hardening) — response-shape violation.
|
|
1181
|
+
* - `AttestryAPIError` (P3 hardening) — non-JSON response.
|
|
1182
|
+
* - `TypeError` (synchronous, no fetch issued) — input failed
|
|
1183
|
+
* SDK-side validation.
|
|
1184
|
+
*
|
|
1185
|
+
* **SDK-side validation**:
|
|
1186
|
+
* - `input`: required; non-null, non-array object.
|
|
1187
|
+
* - `input.packId`: required own-property; non-empty UUID string.
|
|
1188
|
+
* - `input.newPack`: required own-property; non-null, non-array object.
|
|
1189
|
+
* - `input.newPack.packType`: required; member of `PACK_TYPES`.
|
|
1190
|
+
* - `input.newPack.systemId` (when own-present): UUID format.
|
|
1191
|
+
* - `input.newPack.frameworkBindings` (when own-present): array of
|
|
1192
|
+
* length ≤50. Per-entry shape is open-spec (kernel deep-validates).
|
|
1193
|
+
* - `input.newPack.consumerHints` (when own-present): non-null,
|
|
1194
|
+
* non-array object. Inner shape open-spec (kernel deep-validates).
|
|
1195
|
+
* - `input.newPack.metadata` (when own-present): non-null, non-array
|
|
1196
|
+
* object.
|
|
1197
|
+
*
|
|
1198
|
+
* **Response-shape validation** (P2 hardening): `newPack` and `oldPack`
|
|
1199
|
+
* are each a full `EvidencePack`.
|
|
1200
|
+
*
|
|
1201
|
+
* @example
|
|
1202
|
+
* ```ts
|
|
1203
|
+
* const { newPack, oldPack } = await client.evidencePack.supersede({
|
|
1204
|
+
* packId: "11111111-1111-1111-1111-111111111111", // the signed pack
|
|
1205
|
+
* newPack: {
|
|
1206
|
+
* packType: "annex_iv",
|
|
1207
|
+
* frameworkBindings: [{ framework: "eu_ai_act", identifier: "Annex.IV.1" }],
|
|
1208
|
+
* },
|
|
1209
|
+
* });
|
|
1210
|
+
* console.log(oldPack.status, newPack.status); // "superseded", "draft"
|
|
1211
|
+
* console.log(newPack.parentPackId === oldPack.id); // true
|
|
1212
|
+
* ```
|
|
1213
|
+
*/
|
|
1214
|
+
supersede(input, options) {
|
|
1215
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
1216
|
+
throw new TypeError("evidencePack.supersede: `input` must be a non-null object with " +
|
|
1217
|
+
"`packId` and `newPack`");
|
|
1218
|
+
}
|
|
1219
|
+
const hasPackId = objectHasOwn(input, "packId");
|
|
1220
|
+
const packIdRaw = hasPackId
|
|
1221
|
+
? readInputField(input, "packId", "evidencePack.supersede")
|
|
1222
|
+
: undefined;
|
|
1223
|
+
const hasNewPack = objectHasOwn(input, "newPack");
|
|
1224
|
+
const newPackRaw = hasNewPack
|
|
1225
|
+
? readInputField(input, "newPack", "evidencePack.supersede")
|
|
1226
|
+
: undefined;
|
|
1227
|
+
// packId REQUIRED + UUID.
|
|
1228
|
+
if (!hasPackId || packIdRaw === undefined) {
|
|
1229
|
+
throw new TypeError("evidencePack.supersede: `packId` is required");
|
|
1230
|
+
}
|
|
1231
|
+
if (typeof packIdRaw !== "string" || packIdRaw.length === 0) {
|
|
1232
|
+
throw new TypeError("evidencePack.supersede: `packId` must be a non-empty string");
|
|
1233
|
+
}
|
|
1234
|
+
if (!UUID_REGEX.test(packIdRaw)) {
|
|
1235
|
+
throw new TypeError("evidencePack.supersede: `packId` must be an RFC 4122 hyphenated UUID");
|
|
1236
|
+
}
|
|
1237
|
+
// newPack REQUIRED + non-null non-array object.
|
|
1238
|
+
if (!hasNewPack || newPackRaw === undefined) {
|
|
1239
|
+
throw new TypeError("evidencePack.supersede: `newPack` is required");
|
|
1240
|
+
}
|
|
1241
|
+
if (newPackRaw === null ||
|
|
1242
|
+
typeof newPackRaw !== "object" ||
|
|
1243
|
+
Array.isArray(newPackRaw)) {
|
|
1244
|
+
throw new TypeError(`evidencePack.supersede: \`newPack\` must be a non-null object ` +
|
|
1245
|
+
`(got ${describeType(newPackRaw)})`);
|
|
1246
|
+
}
|
|
1247
|
+
// newPack inner fields — snapshot each value EXACTLY ONCE, via
|
|
1248
|
+
// `readInputField` so a throwing accessor on the consumer-supplied
|
|
1249
|
+
// nested `newPack` object surfaces as the documented synchronous
|
|
1250
|
+
// `TypeError` input contract (session-22 hostile MEDIUM-1).
|
|
1251
|
+
const hasPackType = objectHasOwn(newPackRaw, "packType");
|
|
1252
|
+
const packTypeRaw = hasPackType
|
|
1253
|
+
? readInputField(newPackRaw, "packType", "evidencePack.supersede")
|
|
1254
|
+
: undefined;
|
|
1255
|
+
const hasSystemId = objectHasOwn(newPackRaw, "systemId");
|
|
1256
|
+
const systemIdRaw = hasSystemId
|
|
1257
|
+
? readInputField(newPackRaw, "systemId", "evidencePack.supersede")
|
|
1258
|
+
: undefined;
|
|
1259
|
+
const hasFrameworkBindings = objectHasOwn(newPackRaw, "frameworkBindings");
|
|
1260
|
+
const frameworkBindingsRaw = hasFrameworkBindings
|
|
1261
|
+
? readInputField(newPackRaw, "frameworkBindings", "evidencePack.supersede")
|
|
1262
|
+
: undefined;
|
|
1263
|
+
const hasConsumerHints = objectHasOwn(newPackRaw, "consumerHints");
|
|
1264
|
+
const consumerHintsRaw = hasConsumerHints
|
|
1265
|
+
? readInputField(newPackRaw, "consumerHints", "evidencePack.supersede")
|
|
1266
|
+
: undefined;
|
|
1267
|
+
const hasMetadata = objectHasOwn(newPackRaw, "metadata");
|
|
1268
|
+
const metadataRaw = hasMetadata
|
|
1269
|
+
? readInputField(newPackRaw, "metadata", "evidencePack.supersede")
|
|
1270
|
+
: undefined;
|
|
1271
|
+
// newPack.packType REQUIRED + closed-enum membership.
|
|
1272
|
+
if (!hasPackType || packTypeRaw === undefined) {
|
|
1273
|
+
throw new TypeError("evidencePack.supersede: `newPack.packType` is required");
|
|
1274
|
+
}
|
|
1275
|
+
if (typeof packTypeRaw !== "string") {
|
|
1276
|
+
throw new TypeError(`evidencePack.supersede: \`newPack.packType\` must be a string ` +
|
|
1277
|
+
`(got ${describeType(packTypeRaw)})`);
|
|
1278
|
+
}
|
|
1279
|
+
if (!PACK_TYPES.includes(packTypeRaw)) {
|
|
1280
|
+
throw new TypeError(`evidencePack.supersede: \`newPack.packType\` must be one of ` +
|
|
1281
|
+
`${JSON.stringify(PACK_TYPES)} (got ${JSON.stringify(packTypeRaw)})`);
|
|
1282
|
+
}
|
|
1283
|
+
const validatedPackType = packTypeRaw;
|
|
1284
|
+
// newPack.systemId optional UUID.
|
|
1285
|
+
let validatedSystemId;
|
|
1286
|
+
if (hasSystemId && systemIdRaw !== undefined) {
|
|
1287
|
+
if (typeof systemIdRaw !== "string") {
|
|
1288
|
+
throw new TypeError(`evidencePack.supersede: \`newPack.systemId\` must be a string when ` +
|
|
1289
|
+
`provided (got ${describeType(systemIdRaw)})`);
|
|
1290
|
+
}
|
|
1291
|
+
if (!UUID_REGEX.test(systemIdRaw)) {
|
|
1292
|
+
throw new TypeError("evidencePack.supersede: `newPack.systemId` must be an RFC 4122 " +
|
|
1293
|
+
"hyphenated UUID");
|
|
1294
|
+
}
|
|
1295
|
+
validatedSystemId = systemIdRaw;
|
|
1296
|
+
}
|
|
1297
|
+
// newPack.frameworkBindings optional array + length cap (Array.from
|
|
1298
|
+
// snapshot for TOCTOU defense; per-entry shape open-spec).
|
|
1299
|
+
let validatedFrameworkBindings;
|
|
1300
|
+
if (hasFrameworkBindings && frameworkBindingsRaw !== undefined) {
|
|
1301
|
+
if (!Array.isArray(frameworkBindingsRaw)) {
|
|
1302
|
+
throw new TypeError(`evidencePack.supersede: \`newPack.frameworkBindings\` must be an ` +
|
|
1303
|
+
`array when provided (got ${describeType(frameworkBindingsRaw)})`);
|
|
1304
|
+
}
|
|
1305
|
+
const snapshot = Array.from(frameworkBindingsRaw);
|
|
1306
|
+
if (snapshot.length > MAX_FRAMEWORK_BINDINGS_LENGTH) {
|
|
1307
|
+
throw new TypeError(`evidencePack.supersede: \`newPack.frameworkBindings\` array ` +
|
|
1308
|
+
`exceeds the kernel's max length of ` +
|
|
1309
|
+
`${MAX_FRAMEWORK_BINDINGS_LENGTH} (got ${snapshot.length})`);
|
|
1310
|
+
}
|
|
1311
|
+
validatedFrameworkBindings = snapshot;
|
|
1312
|
+
}
|
|
1313
|
+
// newPack.consumerHints optional non-null non-array object (DEV-74).
|
|
1314
|
+
let validatedConsumerHints;
|
|
1315
|
+
if (hasConsumerHints && consumerHintsRaw !== undefined) {
|
|
1316
|
+
if (consumerHintsRaw === null ||
|
|
1317
|
+
typeof consumerHintsRaw !== "object" ||
|
|
1318
|
+
Array.isArray(consumerHintsRaw)) {
|
|
1319
|
+
throw new TypeError(`evidencePack.supersede: \`newPack.consumerHints\` must be a ` +
|
|
1320
|
+
`non-null object when provided (got ` +
|
|
1321
|
+
`${describeType(consumerHintsRaw)})`);
|
|
1322
|
+
}
|
|
1323
|
+
validatedConsumerHints = consumerHintsRaw;
|
|
1324
|
+
}
|
|
1325
|
+
// newPack.metadata optional non-null non-array object.
|
|
1326
|
+
let validatedMetadata;
|
|
1327
|
+
if (hasMetadata && metadataRaw !== undefined) {
|
|
1328
|
+
if (metadataRaw === null ||
|
|
1329
|
+
typeof metadataRaw !== "object" ||
|
|
1330
|
+
Array.isArray(metadataRaw)) {
|
|
1331
|
+
throw new TypeError(`evidencePack.supersede: \`newPack.metadata\` must be a non-null ` +
|
|
1332
|
+
`object when provided (got ${describeType(metadataRaw)})`);
|
|
1333
|
+
}
|
|
1334
|
+
validatedMetadata = metadataRaw;
|
|
1335
|
+
}
|
|
1336
|
+
// Build newPack from validated locals only (F-CR-1). Omit the optional
|
|
1337
|
+
// fields the consumer omitted so the kernel applies its defaults.
|
|
1338
|
+
const newPack = { packType: validatedPackType };
|
|
1339
|
+
if (validatedSystemId !== undefined)
|
|
1340
|
+
newPack.systemId = validatedSystemId;
|
|
1341
|
+
if (validatedFrameworkBindings !== undefined) {
|
|
1342
|
+
newPack.frameworkBindings = validatedFrameworkBindings;
|
|
1343
|
+
}
|
|
1344
|
+
if (validatedConsumerHints !== undefined) {
|
|
1345
|
+
newPack.consumerHints = validatedConsumerHints;
|
|
1346
|
+
}
|
|
1347
|
+
if (validatedMetadata !== undefined)
|
|
1348
|
+
newPack.metadata = validatedMetadata;
|
|
1349
|
+
const path = `/api/v1/evidence-packs/${encodeURIComponent(packIdRaw)}/supersede`;
|
|
1350
|
+
return this.client
|
|
1351
|
+
._request({
|
|
1352
|
+
method: "POST",
|
|
1353
|
+
path,
|
|
1354
|
+
body: { newPack },
|
|
1355
|
+
options,
|
|
1356
|
+
})
|
|
1357
|
+
.then((result) => {
|
|
1358
|
+
if (result === null ||
|
|
1359
|
+
typeof result !== "object" ||
|
|
1360
|
+
Array.isArray(result)) {
|
|
1361
|
+
throw new AttestryError(`evidencePack.supersede: expected an object response from the ` +
|
|
1362
|
+
`kernel (got ${describeType(result)})`);
|
|
1363
|
+
}
|
|
1364
|
+
const obj = result;
|
|
1365
|
+
const newPackRawResp = objectHasOwn(obj, "newPack")
|
|
1366
|
+
? obj.newPack
|
|
1367
|
+
: undefined;
|
|
1368
|
+
validatePack(newPackRawResp, "evidencePack.supersede", "response.newPack");
|
|
1369
|
+
const oldPackRawResp = objectHasOwn(obj, "oldPack")
|
|
1370
|
+
? obj.oldPack
|
|
1371
|
+
: undefined;
|
|
1372
|
+
validatePack(oldPackRawResp, "evidencePack.supersede", "response.oldPack");
|
|
1373
|
+
return result;
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Revoke a signed evidence pack, transitioning it `signed → revoked`
|
|
1378
|
+
* and blocking future verification. Wraps
|
|
1379
|
+
* `POST /api/v1/evidence-packs/{id}/revoke`.
|
|
1380
|
+
*
|
|
1381
|
+
* **No cascade** — revoking a pack does NOT touch its children or the
|
|
1382
|
+
* supersession-chain neighbour. Revocation is intentionally NOT
|
|
1383
|
+
* idempotent: a second revoke 409s (auditors care about the difference
|
|
1384
|
+
* between "revoked once" and "revoked again"; the first is canonical).
|
|
1385
|
+
*
|
|
1386
|
+
* **Auth: ADMIN-only** — the kernel gates `sessionRoles:['admin']` +
|
|
1387
|
+
* `apiKeyPermissions:[ADMIN]`. A non-admin key → 403.
|
|
1388
|
+
*
|
|
1389
|
+
* Optional `reason` (≤500 chars) is recorded verbatim in the pack's
|
|
1390
|
+
* audit-log entry for compliance investigators.
|
|
1391
|
+
*
|
|
1392
|
+
* Errors — ordered by kernel firing precedence (path-uuid 400 BEFORE
|
|
1393
|
+
* auth):
|
|
1394
|
+
* - `AttestryAPIError` (status 429) — rate limit (auto-retried).
|
|
1395
|
+
* - `AttestryAPIError` (status 400 — path UUID) — malformed URL-path
|
|
1396
|
+
* packId. **Fires BEFORE auth.** Reachable only via SDK rule changes.
|
|
1397
|
+
* - `AttestryAPIError` (status 401 / 403) — auth missing / not ADMIN.
|
|
1398
|
+
* - `AttestryAPIError` (status 400 — JSON parse) — malformed body.
|
|
1399
|
+
* - `AttestryAPIError` (status 422) — Zod validation failed
|
|
1400
|
+
* (`details.code === "evidence_pack.validation_failed"`).
|
|
1401
|
+
* - `AttestryAPIError` (status 404) — pack missing OR cross-org.
|
|
1402
|
+
* - `AttestryAPIError` (status 409) — `InvalidStateError` (pack not in
|
|
1403
|
+
* `signed` state, e.g. already revoked / still draft / superseded;
|
|
1404
|
+
* `details.currentStatus` carries the state).
|
|
1405
|
+
* - `AttestryAPIError` (status 500) — internal kernel error.
|
|
1406
|
+
* - `AttestryError` ("request aborted by caller") — abort.
|
|
1407
|
+
* - `AttestryError` (P2 hardening) — response-shape violation.
|
|
1408
|
+
* - `AttestryAPIError` (P3 hardening) — non-JSON response.
|
|
1409
|
+
* - `TypeError` (synchronous, no fetch issued) — input failed
|
|
1410
|
+
* SDK-side validation.
|
|
1411
|
+
*
|
|
1412
|
+
* **SDK-side validation**:
|
|
1413
|
+
* - `input`: required; non-null, non-array object.
|
|
1414
|
+
* - `input.packId`: required own-property; non-empty UUID string.
|
|
1415
|
+
* - `input.reason` (when own-present): non-empty string; length ≤500.
|
|
1416
|
+
*
|
|
1417
|
+
* **Response-shape validation** (P2 hardening): the revoked `EvidencePack`.
|
|
1418
|
+
*
|
|
1419
|
+
* @example
|
|
1420
|
+
* ```ts
|
|
1421
|
+
* const revoked = await client.evidencePack.revoke({
|
|
1422
|
+
* packId: "11111111-1111-1111-1111-111111111111",
|
|
1423
|
+
* reason: "superseding control framework updated; pack no longer valid",
|
|
1424
|
+
* });
|
|
1425
|
+
* console.log(revoked.status); // "revoked"
|
|
1426
|
+
* ```
|
|
1427
|
+
*/
|
|
1428
|
+
revoke(input, options) {
|
|
1429
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
1430
|
+
throw new TypeError("evidencePack.revoke: `input` must be a non-null object with `packId`");
|
|
1431
|
+
}
|
|
1432
|
+
const hasPackId = objectHasOwn(input, "packId");
|
|
1433
|
+
const packIdRaw = hasPackId
|
|
1434
|
+
? readInputField(input, "packId", "evidencePack.revoke")
|
|
1435
|
+
: undefined;
|
|
1436
|
+
const hasReason = objectHasOwn(input, "reason");
|
|
1437
|
+
const reasonRaw = hasReason
|
|
1438
|
+
? readInputField(input, "reason", "evidencePack.revoke")
|
|
1439
|
+
: undefined;
|
|
1440
|
+
if (!hasPackId || packIdRaw === undefined) {
|
|
1441
|
+
throw new TypeError("evidencePack.revoke: `packId` is required");
|
|
1442
|
+
}
|
|
1443
|
+
if (typeof packIdRaw !== "string" || packIdRaw.length === 0) {
|
|
1444
|
+
throw new TypeError("evidencePack.revoke: `packId` must be a non-empty string");
|
|
1445
|
+
}
|
|
1446
|
+
if (!UUID_REGEX.test(packIdRaw)) {
|
|
1447
|
+
throw new TypeError("evidencePack.revoke: `packId` must be an RFC 4122 hyphenated UUID");
|
|
1448
|
+
}
|
|
1449
|
+
// reason optional non-empty string + length cap.
|
|
1450
|
+
let validatedReason;
|
|
1451
|
+
if (hasReason && reasonRaw !== undefined) {
|
|
1452
|
+
if (typeof reasonRaw !== "string") {
|
|
1453
|
+
throw new TypeError(`evidencePack.revoke: \`reason\` must be a string when provided ` +
|
|
1454
|
+
`(got ${describeType(reasonRaw)})`);
|
|
1455
|
+
}
|
|
1456
|
+
if (reasonRaw.length === 0) {
|
|
1457
|
+
throw new TypeError("evidencePack.revoke: `reason` must be a non-empty string when " +
|
|
1458
|
+
"provided");
|
|
1459
|
+
}
|
|
1460
|
+
if (reasonRaw.length > MAX_REASON_LENGTH) {
|
|
1461
|
+
throw new TypeError(`evidencePack.revoke: \`reason\` exceeds the maximum length of ` +
|
|
1462
|
+
`${MAX_REASON_LENGTH} characters (got ${reasonRaw.length})`);
|
|
1463
|
+
}
|
|
1464
|
+
validatedReason = reasonRaw;
|
|
1465
|
+
}
|
|
1466
|
+
// Body from validated locals only (F-CR-1). Always an object; `{}`
|
|
1467
|
+
// when no reason (mirrors the MCP tool; route `parseBody` gets valid
|
|
1468
|
+
// JSON).
|
|
1469
|
+
const body = {};
|
|
1470
|
+
if (validatedReason !== undefined)
|
|
1471
|
+
body.reason = validatedReason;
|
|
1472
|
+
const path = `/api/v1/evidence-packs/${encodeURIComponent(packIdRaw)}/revoke`;
|
|
1473
|
+
return this.client
|
|
1474
|
+
._request({
|
|
1475
|
+
method: "POST",
|
|
1476
|
+
path,
|
|
1477
|
+
body,
|
|
1478
|
+
options,
|
|
1479
|
+
})
|
|
1480
|
+
.then((result) => {
|
|
1481
|
+
validatePack(result, "evidencePack.revoke", "response");
|
|
1482
|
+
return result;
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Export an evidence pack as a downloadable artifact. Wraps
|
|
1487
|
+
* `GET /api/v1/evidence-packs/{id}/export?format={json|pdf|zip}`.
|
|
1488
|
+
*
|
|
1489
|
+
* **Returns a non-JSON artifact** (P1.8 DEV-73). Unlike every other
|
|
1490
|
+
* method, the kernel export route returns the RAW artifact on success
|
|
1491
|
+
* (NOT the `{success,data}` envelope) with a download
|
|
1492
|
+
* `Content-Disposition` header. This method therefore routes through the
|
|
1493
|
+
* streaming transport and returns an {@link EvidencePackExportResult}
|
|
1494
|
+
* wrapping the **un-consumed** `Response`:
|
|
1495
|
+
*
|
|
1496
|
+
* - `json` → `await result.response.json()` yields the artifact
|
|
1497
|
+
* `{export:{format,generatedAt,schemaVersion:"evidence-pack-export.v1"},
|
|
1498
|
+
* pack, bundles}`.
|
|
1499
|
+
* - `pdf` → `await result.response.arrayBuffer()` yields the PDF bytes.
|
|
1500
|
+
* - `zip` → `result.response.body` is a `ReadableStream<Uint8Array>`
|
|
1501
|
+
* (stream it to disk for large packs), or `await result.response.blob()`.
|
|
1502
|
+
*
|
|
1503
|
+
* The transport has already verified the HTTP status (a non-2xx threw
|
|
1504
|
+
* `AttestryAPIError` — NOT a stream/parse crash) and that the response's
|
|
1505
|
+
* `Content-Type` MIME matches the requested format. The SDK does NOT
|
|
1506
|
+
* consume or `validatePack` the artifact body — faithful courier (same
|
|
1507
|
+
* discipline as `decisions.export` / `auditLog.export`).
|
|
1508
|
+
*
|
|
1509
|
+
* **Auth**: READ_ASSESSMENTS. **Revoked packs are exportable** (the
|
|
1510
|
+
* artifact carries `status:'revoked'` verbatim — no filtering).
|
|
1511
|
+
*
|
|
1512
|
+
* **No internal timeout** — the streaming transport does not arm the
|
|
1513
|
+
* 30s default (a large zip can take longer). Pass `options.signal` from
|
|
1514
|
+
* your own `AbortController` to bound the duration.
|
|
1515
|
+
*
|
|
1516
|
+
* Errors — ordered by kernel firing precedence. **The query-schema parse
|
|
1517
|
+
* runs BEFORE auth** in this route, so an absent/unknown `format` 422s
|
|
1518
|
+
* BEFORE 401/403:
|
|
1519
|
+
* - `AttestryAPIError` (status 429) — rate limit (auto-retried).
|
|
1520
|
+
* - `AttestryAPIError` (status 400 — path UUID) — malformed URL-path
|
|
1521
|
+
* packId. **Fires BEFORE auth.** Reachable only via SDK rule changes.
|
|
1522
|
+
* - `AttestryAPIError` (status 422) — absent / unknown `format`
|
|
1523
|
+
* (`details.code === "evidence_pack.validation_failed"`). **Fires
|
|
1524
|
+
* BEFORE auth.** The SDK pre-validates `format`, so reachable only
|
|
1525
|
+
* via SDK rule changes.
|
|
1526
|
+
* - `AttestryAPIError` (status 401 / 403) — auth missing / lacks
|
|
1527
|
+
* READ_ASSESSMENTS.
|
|
1528
|
+
* - `AttestryAPIError` (status 404) — pack missing OR cross-org.
|
|
1529
|
+
* - `AttestryAPIError` (status 500) — internal kernel error.
|
|
1530
|
+
* - `AttestryError` ("request aborted by caller") — abort.
|
|
1531
|
+
* - `AttestryAPIError` (transport guard) — a 2xx with the wrong
|
|
1532
|
+
* `Content-Type` for the requested format.
|
|
1533
|
+
* - `TypeError` (synchronous, no fetch issued) — input failed
|
|
1534
|
+
* SDK-side validation.
|
|
1535
|
+
*
|
|
1536
|
+
* **SDK-side validation**:
|
|
1537
|
+
* - `input`: required; non-null, non-array object.
|
|
1538
|
+
* - `input.packId`: required own-property; non-empty UUID string.
|
|
1539
|
+
* - `input.format`: required own-property; member of `EXPORT_FORMATS`.
|
|
1540
|
+
*
|
|
1541
|
+
* @example Stream a zip export to disk (Node)
|
|
1542
|
+
* ```ts
|
|
1543
|
+
* import { Writable } from "node:stream";
|
|
1544
|
+
* const { response } = await client.evidencePack.export({
|
|
1545
|
+
* packId: "11111111-1111-1111-1111-111111111111",
|
|
1546
|
+
* format: "zip",
|
|
1547
|
+
* });
|
|
1548
|
+
* await response.body!.pipeTo(Writable.toWeb(fs.createWriteStream("pack.zip")));
|
|
1549
|
+
* ```
|
|
1550
|
+
*
|
|
1551
|
+
* @example Read the JSON artifact for offline content-hash re-verification
|
|
1552
|
+
* ```ts
|
|
1553
|
+
* const { response } = await client.evidencePack.export({
|
|
1554
|
+
* packId: "11111111-1111-1111-1111-111111111111",
|
|
1555
|
+
* format: "json",
|
|
1556
|
+
* });
|
|
1557
|
+
* const artifact = await response.json(); // {export, pack, bundles}
|
|
1558
|
+
* ```
|
|
1559
|
+
*/
|
|
1560
|
+
export(input, options) {
|
|
1561
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
1562
|
+
throw new TypeError("evidencePack.export: `input` must be a non-null object with " +
|
|
1563
|
+
"`packId` and `format`");
|
|
1564
|
+
}
|
|
1565
|
+
const hasPackId = objectHasOwn(input, "packId");
|
|
1566
|
+
const packIdRaw = hasPackId
|
|
1567
|
+
? readInputField(input, "packId", "evidencePack.export")
|
|
1568
|
+
: undefined;
|
|
1569
|
+
const hasFormat = objectHasOwn(input, "format");
|
|
1570
|
+
const formatRaw = hasFormat
|
|
1571
|
+
? readInputField(input, "format", "evidencePack.export")
|
|
1572
|
+
: undefined;
|
|
1573
|
+
if (!hasPackId || packIdRaw === undefined) {
|
|
1574
|
+
throw new TypeError("evidencePack.export: `packId` is required");
|
|
1575
|
+
}
|
|
1576
|
+
if (typeof packIdRaw !== "string" || packIdRaw.length === 0) {
|
|
1577
|
+
throw new TypeError("evidencePack.export: `packId` must be a non-empty string");
|
|
1578
|
+
}
|
|
1579
|
+
if (!UUID_REGEX.test(packIdRaw)) {
|
|
1580
|
+
throw new TypeError("evidencePack.export: `packId` must be an RFC 4122 hyphenated UUID");
|
|
1581
|
+
}
|
|
1582
|
+
// format REQUIRED + closed-enum membership (synchronous TypeError).
|
|
1583
|
+
if (!hasFormat || formatRaw === undefined) {
|
|
1584
|
+
throw new TypeError("evidencePack.export: `format` is required");
|
|
1585
|
+
}
|
|
1586
|
+
if (typeof formatRaw !== "string") {
|
|
1587
|
+
throw new TypeError(`evidencePack.export: \`format\` must be a string ` +
|
|
1588
|
+
`(got ${describeType(formatRaw)})`);
|
|
1589
|
+
}
|
|
1590
|
+
if (!EXPORT_FORMATS.includes(formatRaw)) {
|
|
1591
|
+
throw new TypeError(`evidencePack.export: \`format\` must be one of ` +
|
|
1592
|
+
`${JSON.stringify(EXPORT_FORMATS)} (got ${JSON.stringify(formatRaw)})`);
|
|
1593
|
+
}
|
|
1594
|
+
const validatedFormat = formatRaw;
|
|
1595
|
+
const path = `/api/v1/evidence-packs/${encodeURIComponent(packIdRaw)}/export`;
|
|
1596
|
+
const expectedContentType = EXPORT_CONTENT_TYPES[validatedFormat];
|
|
1597
|
+
// `export` returns a downloadable artifact, NOT the {success,data}
|
|
1598
|
+
// envelope — route through the streaming transport `_streamRequest`
|
|
1599
|
+
// (returns the un-consumed Response; sets Accept + the per-format
|
|
1600
|
+
// content-type guard from `expectedContentType`; on non-2xx drains the
|
|
1601
|
+
// body and throws AttestryAPIError BEFORE the guard runs). The SDK
|
|
1602
|
+
// does NOT consume/validate the body (faithful courier; DEV-73). The
|
|
1603
|
+
// canonical `contentType` is surfaced from EXPORT_CONTENT_TYPES — the
|
|
1604
|
+
// guard guarantees the response MIME equals it.
|
|
1605
|
+
return this.client
|
|
1606
|
+
._streamRequest({
|
|
1607
|
+
path,
|
|
1608
|
+
query: { format: validatedFormat },
|
|
1609
|
+
expectedContentType,
|
|
1610
|
+
options,
|
|
1611
|
+
})
|
|
1612
|
+
.then((response) => ({
|
|
1613
|
+
format: validatedFormat,
|
|
1614
|
+
contentType: expectedContentType,
|
|
1615
|
+
contentDisposition: response.headers.get("content-disposition"),
|
|
1616
|
+
response,
|
|
1617
|
+
}));
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
// ─── Shared validation helpers ──────────────────────────────────────────────
|
|
1621
|
+
/**
|
|
1622
|
+
* Validate an `EvidencePack` response shape (P2 hardening). Throws
|
|
1623
|
+
* `AttestryError` on any violation. Used by `create` (the response
|
|
1624
|
+
* IS a pack), `get` (the `pack` field), `list` (each item in `items`),
|
|
1625
|
+
* and `addBundle` (the `pack` field).
|
|
1626
|
+
*
|
|
1627
|
+
* Every field read goes through the module-load `objectHasOwn`
|
|
1628
|
+
* snapshot — a hostile npm dep that pollutes
|
|
1629
|
+
* `Object.prototype.<field>` cannot mask a kernel regression where
|
|
1630
|
+
* the field is missing (symmetric prototype-pollution defense, carry-
|
|
1631
|
+
* forward of session-16 second-hostile-review MEDIUM #3).
|
|
1632
|
+
*
|
|
1633
|
+
* Closed-enum fields (`packType`, `status`) are checked as `typeof
|
|
1634
|
+
* === "string"` only at runtime — the typed-closed / runtime-open
|
|
1635
|
+
* faithful-courier discipline (carry-forward from `vision.ts`
|
|
1636
|
+
* `packIntegration.status` / `gate.ts` `gate`). The drift pin is the
|
|
1637
|
+
* trip-wire for actual enum drift.
|
|
1638
|
+
*/
|
|
1639
|
+
function validatePack(value, methodName, location) {
|
|
1640
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1641
|
+
throw new AttestryError(`${methodName}: expected ${location} to be an object ` +
|
|
1642
|
+
`(got ${describeType(value)})`);
|
|
1643
|
+
}
|
|
1644
|
+
const obj = value;
|
|
1645
|
+
// Always-present string fields.
|
|
1646
|
+
for (const key of ["id", "packType", "orgId", "status", "createdAt"]) {
|
|
1647
|
+
const v = objectHasOwn(obj, key) ? obj[key] : undefined;
|
|
1648
|
+
if (typeof v !== "string") {
|
|
1649
|
+
throw new AttestryError(`${methodName}: expected ${location}.${key} to be a string ` +
|
|
1650
|
+
`(got ${describeType(v)})`);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
// Nullable string fields (string | null).
|
|
1654
|
+
for (const key of [
|
|
1655
|
+
"systemId",
|
|
1656
|
+
"parentPackId",
|
|
1657
|
+
"supersededById",
|
|
1658
|
+
"attestationCertificateId",
|
|
1659
|
+
"contentHash",
|
|
1660
|
+
"signedAt",
|
|
1661
|
+
"signedByUserId",
|
|
1662
|
+
]) {
|
|
1663
|
+
const v = objectHasOwn(obj, key) ? obj[key] : undefined;
|
|
1664
|
+
if (v !== null && typeof v !== "string") {
|
|
1665
|
+
throw new AttestryError(`${methodName}: expected ${location}.${key} to be a string or null ` +
|
|
1666
|
+
`(got ${describeType(v)})`);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
// frameworkBindings: array (kernel `notNull` jsonb default `[]`).
|
|
1670
|
+
const frameworkBindings = objectHasOwn(obj, "frameworkBindings")
|
|
1671
|
+
? obj.frameworkBindings
|
|
1672
|
+
: undefined;
|
|
1673
|
+
if (!Array.isArray(frameworkBindings)) {
|
|
1674
|
+
throw new AttestryError(`${methodName}: expected ${location}.frameworkBindings to be an array ` +
|
|
1675
|
+
`(got ${describeType(frameworkBindings)})`);
|
|
1676
|
+
}
|
|
1677
|
+
// consumerHints, metadata: non-null non-array object (kernel
|
|
1678
|
+
// `notNull` jsonb default `{}`).
|
|
1679
|
+
for (const key of ["consumerHints", "metadata"]) {
|
|
1680
|
+
const v = objectHasOwn(obj, key) ? obj[key] : undefined;
|
|
1681
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) {
|
|
1682
|
+
throw new AttestryError(`${methodName}: expected ${location}.${key} to be a non-null object ` +
|
|
1683
|
+
`(got ${describeType(v)})`);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Validate a `ReperformanceBundle` response shape (P2 hardening).
|
|
1689
|
+
* Used by `get` (`bundles[i]`) and `addBundle` (`bundle`).
|
|
1690
|
+
*/
|
|
1691
|
+
function validateBundle(value, methodName, location) {
|
|
1692
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1693
|
+
throw new AttestryError(`${methodName}: expected ${location} to be an object ` +
|
|
1694
|
+
`(got ${describeType(value)})`);
|
|
1695
|
+
}
|
|
1696
|
+
const obj = value;
|
|
1697
|
+
// Always-present string fields.
|
|
1698
|
+
for (const key of [
|
|
1699
|
+
"id",
|
|
1700
|
+
"evidencePackId",
|
|
1701
|
+
"inputsHash",
|
|
1702
|
+
"outputsHash",
|
|
1703
|
+
"createdAt",
|
|
1704
|
+
]) {
|
|
1705
|
+
const v = objectHasOwn(obj, key) ? obj[key] : undefined;
|
|
1706
|
+
if (typeof v !== "string") {
|
|
1707
|
+
throw new AttestryError(`${methodName}: expected ${location}.${key} to be a string ` +
|
|
1708
|
+
`(got ${describeType(v)})`);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
// storageUri: string | null.
|
|
1712
|
+
const storageUri = objectHasOwn(obj, "storageUri")
|
|
1713
|
+
? obj.storageUri
|
|
1714
|
+
: undefined;
|
|
1715
|
+
if (storageUri !== null && typeof storageUri !== "string") {
|
|
1716
|
+
throw new AttestryError(`${methodName}: expected ${location}.storageUri to be a string or null ` +
|
|
1717
|
+
`(got ${describeType(storageUri)})`);
|
|
1718
|
+
}
|
|
1719
|
+
// traceContent: array (kernel `notNull` jsonb; runtime is an array).
|
|
1720
|
+
const traceContent = objectHasOwn(obj, "traceContent")
|
|
1721
|
+
? obj.traceContent
|
|
1722
|
+
: undefined;
|
|
1723
|
+
if (!Array.isArray(traceContent)) {
|
|
1724
|
+
throw new AttestryError(`${methodName}: expected ${location}.traceContent to be an array ` +
|
|
1725
|
+
`(got ${describeType(traceContent)})`);
|
|
1726
|
+
}
|
|
1727
|
+
// modelBehaviorLog, corroborationResults: object | null (both nullable).
|
|
1728
|
+
for (const key of ["modelBehaviorLog", "corroborationResults"]) {
|
|
1729
|
+
const v = objectHasOwn(obj, key) ? obj[key] : undefined;
|
|
1730
|
+
if (v !== null &&
|
|
1731
|
+
(typeof v !== "object" || Array.isArray(v))) {
|
|
1732
|
+
throw new AttestryError(`${methodName}: expected ${location}.${key} to be an object or null ` +
|
|
1733
|
+
`(got ${describeType(v)})`);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
// metadata: non-null non-array object (kernel `notNull` default `{}`).
|
|
1737
|
+
const metadata = objectHasOwn(obj, "metadata") ? obj.metadata : undefined;
|
|
1738
|
+
if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
1739
|
+
throw new AttestryError(`${methodName}: expected ${location}.metadata to be a non-null object ` +
|
|
1740
|
+
`(got ${describeType(metadata)})`);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Validate the `hashCollision` block on the `addBundle` response.
|
|
1745
|
+
*/
|
|
1746
|
+
function validateHashCollision(value, methodName, location) {
|
|
1747
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1748
|
+
throw new AttestryError(`${methodName}: expected ${location} to be an object ` +
|
|
1749
|
+
`(got ${describeType(value)})`);
|
|
1750
|
+
}
|
|
1751
|
+
const obj = value;
|
|
1752
|
+
const detected = objectHasOwn(obj, "detected") ? obj.detected : undefined;
|
|
1753
|
+
if (typeof detected !== "boolean") {
|
|
1754
|
+
throw new AttestryError(`${methodName}: expected ${location}.detected to be a boolean ` +
|
|
1755
|
+
`(got ${describeType(detected)})`);
|
|
1756
|
+
}
|
|
1757
|
+
const count = objectHasOwn(obj, "count") ? obj.count : undefined;
|
|
1758
|
+
if (typeof count !== "number") {
|
|
1759
|
+
throw new AttestryError(`${methodName}: expected ${location}.count to be a number ` +
|
|
1760
|
+
`(got ${describeType(count)})`);
|
|
1761
|
+
}
|
|
1762
|
+
const collidingBundleIds = objectHasOwn(obj, "collidingBundleIds")
|
|
1763
|
+
? obj.collidingBundleIds
|
|
1764
|
+
: undefined;
|
|
1765
|
+
if (!Array.isArray(collidingBundleIds)) {
|
|
1766
|
+
throw new AttestryError(`${methodName}: expected ${location}.collidingBundleIds to be an ` +
|
|
1767
|
+
`array (got ${describeType(collidingBundleIds)})`);
|
|
1768
|
+
}
|
|
1769
|
+
for (let i = 0; i < collidingBundleIds.length; i++) {
|
|
1770
|
+
if (typeof collidingBundleIds[i] !== "string") {
|
|
1771
|
+
throw new AttestryError(`${methodName}: expected ${location}.collidingBundleIds[${i}] to ` +
|
|
1772
|
+
`be a string (got ${describeType(collidingBundleIds[i])})`);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Human-readable type description for error messages. Distinguishes
|
|
1778
|
+
* `null` and `array` from generic `object`. Duplicated across SDK
|
|
1779
|
+
* resource modules per project pattern (small helper, leaf-resource
|
|
1780
|
+
* modules, no shared module yet).
|
|
1781
|
+
*/
|
|
1782
|
+
function describeType(value) {
|
|
1783
|
+
if (value === null)
|
|
1784
|
+
return "null";
|
|
1785
|
+
if (Array.isArray(value))
|
|
1786
|
+
return "array";
|
|
1787
|
+
return typeof value;
|
|
1788
|
+
}
|
|
1789
|
+
//# sourceMappingURL=evidence-pack.js.map
|