@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.
Files changed (99) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +1269 -0
  3. package/dist/client.d.ts +58 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +74 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/constants.d.ts +7 -0
  8. package/dist/constants.d.ts.map +1 -0
  9. package/dist/constants.js +43 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/errors.d.ts +16 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +41 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/index.d.ts +17 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +20 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/lines-parser.d.ts +50 -0
  20. package/dist/lines-parser.d.ts.map +1 -0
  21. package/dist/lines-parser.js +211 -0
  22. package/dist/lines-parser.js.map +1 -0
  23. package/dist/ndjson-parser.d.ts +57 -0
  24. package/dist/ndjson-parser.d.ts.map +1 -0
  25. package/dist/ndjson-parser.js +245 -0
  26. package/dist/ndjson-parser.js.map +1 -0
  27. package/dist/resources/abac-policies.d.ts +1034 -0
  28. package/dist/resources/abac-policies.d.ts.map +1 -0
  29. package/dist/resources/abac-policies.js +1519 -0
  30. package/dist/resources/abac-policies.js.map +1 -0
  31. package/dist/resources/audit-log.d.ts +588 -0
  32. package/dist/resources/audit-log.d.ts.map +1 -0
  33. package/dist/resources/audit-log.js +629 -0
  34. package/dist/resources/audit-log.js.map +1 -0
  35. package/dist/resources/batch.d.ts +845 -0
  36. package/dist/resources/batch.d.ts.map +1 -0
  37. package/dist/resources/batch.js +1074 -0
  38. package/dist/resources/batch.js.map +1 -0
  39. package/dist/resources/chat.d.ts +151 -0
  40. package/dist/resources/chat.d.ts.map +1 -0
  41. package/dist/resources/chat.js +124 -0
  42. package/dist/resources/chat.js.map +1 -0
  43. package/dist/resources/check.d.ts +348 -0
  44. package/dist/resources/check.d.ts.map +1 -0
  45. package/dist/resources/check.js +543 -0
  46. package/dist/resources/check.js.map +1 -0
  47. package/dist/resources/compliance-check.d.ts +330 -0
  48. package/dist/resources/compliance-check.d.ts.map +1 -0
  49. package/dist/resources/compliance-check.js +402 -0
  50. package/dist/resources/compliance-check.js.map +1 -0
  51. package/dist/resources/decisions.d.ts +1208 -0
  52. package/dist/resources/decisions.d.ts.map +1 -0
  53. package/dist/resources/decisions.js +1362 -0
  54. package/dist/resources/decisions.js.map +1 -0
  55. package/dist/resources/evidence-pack.d.ts +1080 -0
  56. package/dist/resources/evidence-pack.d.ts.map +1 -0
  57. package/dist/resources/evidence-pack.js +1789 -0
  58. package/dist/resources/evidence-pack.js.map +1 -0
  59. package/dist/resources/gate.d.ts +613 -0
  60. package/dist/resources/gate.d.ts.map +1 -0
  61. package/dist/resources/gate.js +737 -0
  62. package/dist/resources/gate.js.map +1 -0
  63. package/dist/resources/incidents.d.ts +136 -0
  64. package/dist/resources/incidents.d.ts.map +1 -0
  65. package/dist/resources/incidents.js +229 -0
  66. package/dist/resources/incidents.js.map +1 -0
  67. package/dist/resources/regulatory-changes.d.ts +307 -0
  68. package/dist/resources/regulatory-changes.d.ts.map +1 -0
  69. package/dist/resources/regulatory-changes.js +365 -0
  70. package/dist/resources/regulatory-changes.js.map +1 -0
  71. package/dist/resources/safe-input-read.d.ts +21 -0
  72. package/dist/resources/safe-input-read.d.ts.map +1 -0
  73. package/dist/resources/safe-input-read.js +57 -0
  74. package/dist/resources/safe-input-read.js.map +1 -0
  75. package/dist/resources/ship-gate.d.ts +475 -0
  76. package/dist/resources/ship-gate.d.ts.map +1 -0
  77. package/dist/resources/ship-gate.js +727 -0
  78. package/dist/resources/ship-gate.js.map +1 -0
  79. package/dist/resources/vision.d.ts +540 -0
  80. package/dist/resources/vision.d.ts.map +1 -0
  81. package/dist/resources/vision.js +1036 -0
  82. package/dist/resources/vision.js.map +1 -0
  83. package/dist/retry.d.ts +103 -0
  84. package/dist/retry.d.ts.map +1 -0
  85. package/dist/retry.js +224 -0
  86. package/dist/retry.js.map +1 -0
  87. package/dist/sse-parser.d.ts +64 -0
  88. package/dist/sse-parser.d.ts.map +1 -0
  89. package/dist/sse-parser.js +271 -0
  90. package/dist/sse-parser.js.map +1 -0
  91. package/dist/transport.d.ts +142 -0
  92. package/dist/transport.d.ts.map +1 -0
  93. package/dist/transport.js +455 -0
  94. package/dist/transport.js.map +1 -0
  95. package/dist/types.d.ts +61 -0
  96. package/dist/types.d.ts.map +1 -0
  97. package/dist/types.js +3 -0
  98. package/dist/types.js.map +1 -0
  99. 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