@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,1208 @@
1
+ import type { AttestryClient } from "../client.js";
2
+ import type { RequestOptions } from "../types.js";
3
+ /**
4
+ * Public wire shape for a decision record returned by
5
+ * `GET /api/v1/decisions/:id`. Mirrors the kernel's `successResponse`
6
+ * column projection (canonicalPayload deliberately excluded).
7
+ *
8
+ * jsonb arrays (`frameworkClaims`, `toolInvocations`, `delegationChain`)
9
+ * are typed as `unknown[]` — the SDK does not enforce inner shape; the
10
+ * server already validated them at write time and the SDK is meant to
11
+ * be forward-compatible with kernel-side schema growth.
12
+ */
13
+ export interface DecisionRecord {
14
+ id: string;
15
+ orgId: string;
16
+ systemId: string;
17
+ manifestVersionId: string;
18
+ /** Null when no attestation is bound to this record. */
19
+ attestationId: string | null;
20
+ /** Per-system monotonic sequence (notNull on the kernel side). */
21
+ sequenceNumber: number;
22
+ /** sha256:[a-f0-9]{64} format — server CHECK constraint. */
23
+ inputDigest: string;
24
+ /** sha256:[a-f0-9]{64} OR null when output isn't recorded. */
25
+ outputDigest: string | null;
26
+ frameworkClaims: unknown[];
27
+ toolInvocations: unknown[];
28
+ delegationChain: unknown[];
29
+ humanOversightState: string | null;
30
+ policyOutcome: string | null;
31
+ /** Null on the first record in the chain; otherwise the previous record's hash. */
32
+ prevRecordHash: string | null;
33
+ /** Hash of (prev_record_hash || canonical_payload). */
34
+ recordHash: string;
35
+ clientSignature: string | null;
36
+ clientKeyId: string | null;
37
+ idempotencyKey: string | null;
38
+ zkProof: Record<string, unknown> | null;
39
+ tombstoned: boolean;
40
+ /** ISO-8601 timestamp; non-null iff `tombstoned === true`. */
41
+ tombstonedAt: string | null;
42
+ tombstonedReason: string | null;
43
+ /** ISO-8601 timestamp. */
44
+ createdAt: string;
45
+ }
46
+ /**
47
+ * One framework-compliance claim asserted by the decision (e.g.
48
+ * `{framework: "eu_ai_act", article: "Art.13", claim: "human oversight provided"}`).
49
+ * Up to 50 entries per record. The server validates each item's shape
50
+ * via `.strict()`; the SDK forwards faithfully without per-item
51
+ * validation.
52
+ */
53
+ export interface FrameworkClaim {
54
+ /** Framework code (1-100 chars). E.g., `"eu_ai_act"`, `"nist_ai_rmf"`. */
55
+ framework: string;
56
+ /** Article / section identifier (1-100 chars). E.g., `"Art.13"`, `"GV-1.1"`. */
57
+ article: string;
58
+ /** Claim text (1-2000 chars). */
59
+ claim: string;
60
+ }
61
+ /**
62
+ * One tool / model / external-API invocation that participated in the
63
+ * decision (e.g. `{name: "vector-store-query", inputHash: "sha256:..."}`).
64
+ * Up to 50 entries per record. Hash fields are optional; when present,
65
+ * must match `sha256:[a-f0-9]{64}` server-side.
66
+ */
67
+ export interface ToolInvocation {
68
+ /** Tool / model identifier (1-200 chars). */
69
+ name: string;
70
+ /** Hash of the tool's input payload (sha256:[a-f0-9]{64}). */
71
+ inputHash?: string;
72
+ /** Hash of the tool's output payload (sha256:[a-f0-9]{64}). */
73
+ outputHash?: string;
74
+ }
75
+ /**
76
+ * One step in the agent-delegation chain that produced this decision.
77
+ * Up to 20 entries per record. `agentId` identifies the agent
78
+ * (caller-defined; could be a UUID, a slug, or an external system's
79
+ * identifier); `delegationToken` is an optional opaque proof-of-
80
+ * delegation (max 2000 chars).
81
+ */
82
+ export interface DelegationEntry {
83
+ /** Agent identifier (1-500 chars). Caller-defined format. */
84
+ agentId: string;
85
+ /** Opaque delegation token (max 2000 chars). Optional. */
86
+ delegationToken?: string;
87
+ }
88
+ /**
89
+ * Optional zero-knowledge proof attached to the decision.
90
+ *
91
+ * Field caps (kernel-side):
92
+ * - `type`: 1-100 chars (e.g., `"groth16"`, `"plonk"`, `"stark"`)
93
+ * - `proof`: 1-100_000 chars — generous for real ZK schemes
94
+ * (Groth16 ~200 bytes, PLONK ~5KB, STARKs can exceed 100KB)
95
+ * - `publicSignals`: array of strings, each ≤500 chars, max 100 entries
96
+ *
97
+ * SDK forwards the object faithfully; server validates the inner shape.
98
+ */
99
+ export interface ZkProof {
100
+ /** ZK scheme identifier (1-100 chars). */
101
+ type: string;
102
+ /** Proof data — opaque to the SDK (1-100_000 chars). */
103
+ proof: string;
104
+ /** Public signals — opaque strings (each ≤500 chars, max 100 entries). */
105
+ publicSignals: string[];
106
+ }
107
+ /**
108
+ * Input shape for `decisions.ingest()`. Mirrors the kernel's
109
+ * `decisionCreateSchema` (`src/lib/validation/decision-schemas.ts:17-93`)
110
+ * field-for-field. Strict at the server side (`.strict()`) — extra keys
111
+ * cause a 422 response, which is load-bearing for hash-chain
112
+ * non-malleability (every field that participates in the canonical hash
113
+ * must come through this shape; silent extras would weaken the chain).
114
+ *
115
+ * SDK validates field TYPES synchronously (throws `TypeError` BEFORE
116
+ * issuing any request). Format checks (UUID, hash regex, base64, enum
117
+ * membership, length caps, refine clause) are deferred to the server —
118
+ * decision D5 in the build-round audit.
119
+ *
120
+ * **Idempotency**: when `idempotencyKey` is provided AND a prior record
121
+ * with the same `(orgId, idempotencyKey)` exists, the server compares
122
+ * canonical bytes. Match → returns the persisted record (HTTP 200,
123
+ * `decision.idempotency_replay`). Mismatch → 409
124
+ * `IdempotencyConflictError` ("Idempotency key already used with
125
+ * different payload"). The SDK does NOT surface the 200/201 distinction
126
+ * (both resolve as `Promise<DecisionRecord>`); consumers can check
127
+ * `record.idempotencyKey === input.idempotencyKey` if they need to know.
128
+ *
129
+ * **Pairing constraint**: `clientSignature` and `clientKeyId` must
130
+ * EITHER both be provided OR both be absent. The server's `.refine()`
131
+ * rejects asymmetric input with a 422; the SDK forwards faithfully (D4).
132
+ */
133
+ export interface DecisionIngestInput {
134
+ /** Required — UUID. The system this decision belongs to. */
135
+ systemId: string;
136
+ /** Required — `sha256:[a-f0-9]{64}`. Hash of the decision input. */
137
+ inputDigest: string;
138
+ /** Optional — `sha256:[a-f0-9]{64}`. Hash of the decision output. */
139
+ outputDigest?: string;
140
+ /** Optional — UUID. Bind this record to an existing attestation. */
141
+ attestationId?: string;
142
+ /** Optional — up to 50 framework-compliance claims. Defaults `[]`. */
143
+ frameworkClaims?: FrameworkClaim[];
144
+ /** Optional — up to 50 tool invocations. Defaults `[]`. */
145
+ toolInvocations?: ToolInvocation[];
146
+ /** Optional — up to 20 delegation steps. Defaults `[]`. */
147
+ delegationChain?: DelegationEntry[];
148
+ /** Optional — human-oversight gate state for this decision. */
149
+ humanOversightState?: "approved" | "bypassed" | "not_required";
150
+ /** Optional — final policy verdict for this decision. */
151
+ policyOutcome?: "permitted" | "denied" | "escalated";
152
+ /**
153
+ * Optional — base64-encoded signature over the canonical payload.
154
+ * Must be paired with `clientKeyId` (server `.refine()` rejects one
155
+ * without the other).
156
+ */
157
+ clientSignature?: string;
158
+ /**
159
+ * Optional — identifier for the key used to produce `clientSignature`.
160
+ * Must be paired with `clientSignature`.
161
+ */
162
+ clientKeyId?: string;
163
+ /**
164
+ * Optional — caller-supplied dedupe key (1-200 chars). Same
165
+ * `(orgId, idempotencyKey)` + same canonical payload → idempotent
166
+ * replay (returns the prior record). Different payload → 409.
167
+ *
168
+ * **For at-least-once delivery semantics across network failures,
169
+ * pass an `idempotencyKey` so retries dedupe server-side.** Without
170
+ * one, a 429-retry that succeeds-but-loses-the-response could create
171
+ * a duplicate record.
172
+ */
173
+ idempotencyKey?: string;
174
+ /** Optional — zero-knowledge proof attached to this decision. */
175
+ zkProof?: ZkProof;
176
+ }
177
+ /**
178
+ * Input shape for `decisions.bulk()`. Mirrors the kernel's
179
+ * `decisionBulkCreateSchema` (`src/lib/validation/decision-schemas.ts:105-114`)
180
+ * — a single-field wrapper around an items array. The wrapper (rather
181
+ * than a bare array) matches the kernel wire literal `{items}` and
182
+ * leaves room for future top-level additions (e.g.,
183
+ * `mode: "atomic" | "best-effort"`) without a breaking change.
184
+ *
185
+ * The SDK validates that `input` is an object and `input.items` is an
186
+ * array (synchronously, before any fetch). It does NOT pre-cap the
187
+ * length client-side — the kernel's `.min(1).max(500)` is the schema
188
+ * authority — and it does NOT recursively validate `items[i]` shapes
189
+ * (symmetric to ingest's `frameworkClaims` / `toolInvocations` policy
190
+ * — `.strict()` Zod is the kernel-side authority).
191
+ */
192
+ export interface DecisionBulkInput {
193
+ /**
194
+ * 1-500 entries. Each item is the same shape as `decisions.ingest`
195
+ * input. Bulk does NOT recover from idempotency races — failed items
196
+ * with `code === "idempotency_unique_violation"` should be retried
197
+ * individually via `decisions.ingest` to invoke the per-record race
198
+ * recovery path.
199
+ */
200
+ items: DecisionIngestInput[];
201
+ }
202
+ /**
203
+ * One inserted-record summary in `BulkIngestResult.inserted[]`. Slim by
204
+ * design — matches the kernel's bulk-ingest summary (no heavy fields
205
+ * like `frameworkClaims` / `canonicalPayload`). Call
206
+ * `decisions.retrieve(id)` if the caller needs the full record.
207
+ *
208
+ * Sorted by original input `index` server-side so the caller can map
209
+ * each entry back to the position in the submitted `items[]` array.
210
+ */
211
+ export interface BulkInsertedSummary {
212
+ /** Position in the submitted `items[]` array (zero-based). */
213
+ index: number;
214
+ /** UUID of the persisted decision record. */
215
+ id: string;
216
+ systemId: string;
217
+ /** Per-system monotonic sequence assigned at insert time. */
218
+ sequenceNumber: number;
219
+ /** sha256:[a-f0-9]{64} format. */
220
+ recordHash: string;
221
+ /**
222
+ * ISO-8601 timestamp string. (Kernel emits a `Date`; `JSON.stringify`
223
+ * converts it to the wire string.)
224
+ */
225
+ createdAt: string;
226
+ }
227
+ /**
228
+ * One failed-record summary in `BulkIngestResult.failed[]`. The whole
229
+ * chunk a record belonged to fails together (kernel groups records into
230
+ * fixed-size chunks under one `chain_heads` lock; a chunk is all-or-
231
+ * nothing), but other chunks for the same system OR other systems
232
+ * continue. Sorted by original input `index` server-side.
233
+ */
234
+ export interface BulkFailedSummary {
235
+ /** Position in the submitted `items[]` array (zero-based). */
236
+ index: number;
237
+ systemId: string;
238
+ /** Human-readable per-record error message. */
239
+ error: string;
240
+ /**
241
+ * Machine-readable per-record code. Source of truth: kernel
242
+ * `classifyChunkError` in `src/lib/decisions/bulk-ingest.ts:114-156`.
243
+ * Today's possible values:
244
+ *
245
+ * - `"idempotency_conflict"` — same idempotencyKey, different
246
+ * canonical bytes (within a chunk).
247
+ * - `"payload_too_large"` — one record's canonical bytes exceed
248
+ * the 256KB per-record cap.
249
+ * - `"chain_head_missing"` — internal invariant violation (should
250
+ * never fire in practice).
251
+ * - `"system_not_found"` — cross-org system OR cross-system
252
+ * `attestationId`. Collapsed deliberately for enumeration safety
253
+ * (matches single-record ingest behavior).
254
+ * - `"ijson_validation_failed"` — per-record canonicalize tripped
255
+ * on NaN/Infinity/BigInt/undefined/Symbol.
256
+ * - `"idempotency_unique_violation"` — race condition. Bulk does
257
+ * NOT recover; retry via `decisions.ingest` (single-record
258
+ * endpoint) to invoke the per-record race-recovery path.
259
+ * - `"chunk_failed"` — catch-all for unclassified chunk-tx
260
+ * failures.
261
+ *
262
+ * Typed as `string` (NOT a literal-union) for forward-compat — same
263
+ * convention as `DecisionRecord.humanOversightState` and
264
+ * `DecisionStreamEvent.eventType`. Future kernel additions slot in
265
+ * cleanly without an SDK bump.
266
+ */
267
+ code: string;
268
+ }
269
+ /**
270
+ * Result envelope returned by `decisions.bulk()`. The transport
271
+ * unwraps the kernel's `{success:true, data}` JSON envelope — the
272
+ * caller receives this shape directly.
273
+ *
274
+ * **Critical contract**: `decisions.bulk()` resolves successfully (no
275
+ * throw) even when every record failed. Inspect `totalFailed` and
276
+ * `failed[]` to detect per-record errors. Top-level failures (auth,
277
+ * rate limit, plan limit, oversize batch) DO throw `AttestryAPIError`
278
+ * with the corresponding HTTP status.
279
+ */
280
+ export interface BulkIngestResult {
281
+ /** Number of items in the submitted `items[]` array. */
282
+ totalSubmitted: number;
283
+ /** Number of records persisted to the chain. Equals `inserted.length`. */
284
+ totalInserted: number;
285
+ /** Number of records that failed. Equals `failed.length`. */
286
+ totalFailed: number;
287
+ /** Sorted by original input `index`. */
288
+ inserted: BulkInsertedSummary[];
289
+ /** Sorted by original input `index`. */
290
+ failed: BulkFailedSummary[];
291
+ }
292
+ /**
293
+ * Slim row returned by the list endpoint. Mirrors the kernel's
294
+ * `DecisionListItem` (in `src/lib/decisions/list-query.ts`) — a subset
295
+ * of the full `DecisionRecord`, deliberately excluding heavy fields
296
+ * (`canonicalPayload` BYTEA, `manifestVersionId`, `attestationId`,
297
+ * `clientSignature`, `clientKeyId`, `idempotencyKey`, `zkProof`,
298
+ * `tombstonedAt`, `tombstonedReason`, `orgId`).
299
+ *
300
+ * jsonb arrays are typed as `unknown[]` for forward-compat — same
301
+ * convention as `DecisionRecord`. `createdAt` is the wire-shape ISO
302
+ * string (kernel `Date` → JSON.stringify → string).
303
+ *
304
+ * Use `decisions.retrieve(id)` if you need the full record (e.g.,
305
+ * verifying a signature with `clientSignature` / `clientKeyId`).
306
+ */
307
+ export interface DecisionListItem {
308
+ id: string;
309
+ systemId: string;
310
+ /** Per-system monotonic sequence (notNull on the kernel side). */
311
+ sequenceNumber: number;
312
+ /** sha256:[a-f0-9]{64} format. */
313
+ inputDigest: string;
314
+ /** sha256:[a-f0-9]{64} OR null when output isn't recorded. */
315
+ outputDigest: string | null;
316
+ frameworkClaims: unknown[];
317
+ toolInvocations: unknown[];
318
+ delegationChain: unknown[];
319
+ humanOversightState: string | null;
320
+ policyOutcome: string | null;
321
+ /** Hash of (prev_record_hash || canonical_payload). */
322
+ recordHash: string;
323
+ /** Null on the first record in the chain. */
324
+ prevRecordHash: string | null;
325
+ /** ISO-8601 timestamp string. */
326
+ createdAt: string;
327
+ tombstoned: boolean;
328
+ }
329
+ /**
330
+ * Filter / pagination inputs for `decisions.list()`. All fields optional;
331
+ * a bare `decisions.list()` call returns the most-recent page (default
332
+ * 50) of the org's records.
333
+ *
334
+ * Pagination is keyset-based: pass back `response.nextCursor` as
335
+ * `input.cursor` to fetch the next page. The cursor format is opaque
336
+ * to the SDK — kernel encodes/decodes it.
337
+ *
338
+ * Filters:
339
+ * - `systemId`: limit to one system's records
340
+ * - `from` / `to`: ISO datetime range filters on `createdAt`
341
+ * - `framework` / `article`: jsonb-contains filters on `frameworkClaims`
342
+ * - `tool`: jsonb-contains filter on `toolInvocations`
343
+ * - `includeTombstoned`: include soft-deleted records (default false)
344
+ * - `limit`: page size, 1-200, default 50
345
+ */
346
+ export interface DecisionsListInput {
347
+ systemId?: string;
348
+ /** ISO datetime — `createdAt >= from`. */
349
+ from?: string;
350
+ /** ISO datetime — `createdAt <= to`. */
351
+ to?: string;
352
+ /** Filter by `frameworkClaims[].framework`. 1-100 chars. */
353
+ framework?: string;
354
+ /** Filter by `frameworkClaims[].article`. 1-100 chars. */
355
+ article?: string;
356
+ /** Filter by `toolInvocations[].name`. 1-200 chars. */
357
+ tool?: string;
358
+ /** Opaque cursor from a prior response's `nextCursor`. */
359
+ cursor?: string;
360
+ /** Page size, 1-200, default 50. */
361
+ limit?: number;
362
+ /** Include soft-deleted records. Default false. */
363
+ includeTombstoned?: boolean;
364
+ }
365
+ export interface DecisionsListResponse {
366
+ items: DecisionListItem[];
367
+ /**
368
+ * Cursor for the next page. `null` (NOT undefined) when no more pages
369
+ * exist — matches the kernel wire shape exactly. Pass as
370
+ * `input.cursor` on the next call to fetch the following page.
371
+ */
372
+ nextCursor: string | null;
373
+ }
374
+ /**
375
+ * Public stream event types. Today the kernel emits exactly one
376
+ * (`decision.appended`) — extracted as `as const` so consumers can
377
+ * iterate, narrow, and so the drift-detection pin in
378
+ * `src/lib/incidents/__tests__/sdk-drift.test.ts` can compare
379
+ * structurally against the kernel's `formatSSEFrame` default. When the
380
+ * kernel adds a new event type (e.g. `decision.tombstoned`), update
381
+ * BOTH this array AND the kernel emitter, and bump the SDK minor.
382
+ */
383
+ export declare const DECISION_STREAM_EVENT_TYPES: readonly ["decision.appended"];
384
+ export type DecisionStreamEventType = (typeof DECISION_STREAM_EVENT_TYPES)[number];
385
+ /**
386
+ * One event yielded by `decisions.stream()`. Mirrors the kernel's
387
+ * `DecisionStreamEvent` (in `src/lib/decisions/stream-cursor.ts`) but
388
+ * `createdAt` is an ISO-8601 string (the wire shape — kernel emits
389
+ * `event.createdAt.toISOString()` in `formatSSEFrame`), and two
390
+ * SSE-level fields are surfaced for reconnection:
391
+ *
392
+ * - `eventId`: the value from the SSE `id:` line. Pass this back as
393
+ * `input.lastEventId` on a subsequent `stream()` call to resume.
394
+ * The kernel's cursor format is opaque to the SDK (today it's a
395
+ * base64url-encoded `{c, i}` JSON, but the SDK does not parse it).
396
+ * - `eventType`: the value from the SSE `event:` line. Currently
397
+ * always `"decision.appended"`; surfaced for forward-compat with
398
+ * future event types (e.g. `"decision.tombstoned"`).
399
+ *
400
+ * Slim by design — clients call `decisions.retrieve(id)` for the full
401
+ * record (including `frameworkClaims`, `delegationChain`, etc., which
402
+ * the stream endpoint omits to keep frames small).
403
+ */
404
+ export interface DecisionStreamEvent {
405
+ /** Decision record id (UUID). */
406
+ id: string;
407
+ /** System the decision was made for (UUID). */
408
+ systemId: string;
409
+ /** Per-system monotonic sequence number. */
410
+ sequenceNumber: number;
411
+ /** sha256:[a-f0-9]{64} format. */
412
+ recordHash: string;
413
+ /** Null on the first record in the chain. */
414
+ prevRecordHash: string | null;
415
+ /** True if the record has been tombstoned. */
416
+ tombstoned: boolean;
417
+ /** ISO-8601 timestamp string. */
418
+ createdAt: string;
419
+ /**
420
+ * SSE `id:` field — pass back as `input.lastEventId` to resume.
421
+ * Always a non-empty string (the SDK validates the frame had a
422
+ * non-empty `id:` line; throws `AttestryError` if not).
423
+ */
424
+ eventId: string;
425
+ /**
426
+ * SSE `event:` field. Today always `"decision.appended"` (the only
427
+ * type emitted by the kernel — see `DECISION_STREAM_EVENT_TYPES`).
428
+ * Typed as `string` rather than the literal-union for forward-compat:
429
+ * a future kernel patch can add a new event type and consumer code
430
+ * that does `if (event.eventType === 'decision.tombstoned')` keeps
431
+ * compiling without an SDK bump (the consumer just needs to know
432
+ * about the new type).
433
+ */
434
+ eventType: string;
435
+ }
436
+ /**
437
+ * Filters / resume cursor for `decisions.stream()`. All fields optional:
438
+ * a fresh `stream()` call with no args subscribes to the entire org's
439
+ * stream from "now" forward.
440
+ *
441
+ * Lifecycle:
442
+ * 1. First call with `lastEventId` undefined → start at "now", no
443
+ * historical replay. Server only emits events created AFTER the
444
+ * connection opens.
445
+ * 2. Server emits events. Each event carries `eventId` (the resume
446
+ * cursor) — store the latest.
447
+ * 3. On disconnect (network drop, server timeout, deliberate abort),
448
+ * call `stream({ lastEventId: lastSeen.eventId, ... })` to resume —
449
+ * server backfills every event after the cursor before resuming
450
+ * live polling.
451
+ *
452
+ * Validation: the SDK validates that `systemId` and `lastEventId` are
453
+ * non-empty strings if provided (catches `null`/empty programming
454
+ * errors). Format validation (UUID for systemId, base64url cursor for
455
+ * lastEventId) is the server's job — the server returns 400 on
456
+ * malformed input.
457
+ */
458
+ export interface DecisionsStreamInput {
459
+ /** Filter to a single system's events. UUID format. */
460
+ systemId?: string;
461
+ /** Resume cursor — typically the `eventId` of the last seen event. */
462
+ lastEventId?: string;
463
+ }
464
+ /**
465
+ * Filter inputs for `decisions.export()`. `systemId` is REQUIRED — the
466
+ * kernel scopes export to a single system's chain (no cross-system
467
+ * exports). Date filters optional. No pagination — the response streams.
468
+ *
469
+ * Validation: SDK validates field TYPES synchronously (throws
470
+ * `TypeError` BEFORE issuing any request). Format checks (UUID, ISO
471
+ * datetime) deferred to the server's Zod schema (`decisionExportQuerySchema`).
472
+ */
473
+ export interface DecisionsExportInput {
474
+ /** REQUIRED — UUID. The system whose chain to export. */
475
+ systemId: string;
476
+ /** Optional — ISO datetime, `createdAt >= from`. */
477
+ from?: string;
478
+ /** Optional — ISO datetime, `createdAt <= to`. */
479
+ to?: string;
480
+ /** Optional — include soft-deleted records. Default false. */
481
+ includeTombstoned?: boolean;
482
+ }
483
+ /**
484
+ * Per-record frame yielded by `decisions.export()`. Structurally
485
+ * IDENTICAL to `DecisionListItem` — same field names, same types, same
486
+ * null-vs-undefined semantics. Reusing the existing exported type
487
+ * (rather than redefining) signals to consumers that `decisions.list`
488
+ * and `decisions.export` emit interchangeable rows. Kernel-side, the
489
+ * `ExportRecord` shape was deliberately aligned with the list-row
490
+ * projection to enable this type identity.
491
+ *
492
+ * Build-round D2.
493
+ */
494
+ export type DecisionExportRecord = DecisionListItem;
495
+ /**
496
+ * Final frame in a `decisions.export()` stream. Distinguishes itself
497
+ * from records via the `type: "ExportTrailer"` discriminator. The
498
+ * kernel emits exactly one trailer at the end of every successful
499
+ * stream — including empty exports (recordCount === 0).
500
+ *
501
+ * The trailer commits the export to a single Merkle root over the
502
+ * per-record `recordHash` leaves (Bitcoin-style binary Merkle, with
503
+ * empty-export sentinel `sha256("ATTESTRY-EMPTY-EXPORT")`). Verifying
504
+ * the commitment is the consumer's responsibility — the SDK exposes
505
+ * the trailer raw without recomputation.
506
+ */
507
+ export interface DecisionExportTrailer {
508
+ /** Discriminator — distinguishes the trailer from records. */
509
+ type: "ExportTrailer";
510
+ /** UUID — the systemId from the export filter. */
511
+ systemId: string;
512
+ /** Number of records that streamed before the trailer. >= 0. */
513
+ recordCount: number;
514
+ /** First record's `sequenceNumber`. `null` on empty export. */
515
+ sequenceFrom: number | null;
516
+ /** Last record's `sequenceNumber`. `null` on empty export. */
517
+ sequenceTo: number | null;
518
+ /**
519
+ * `sha256:[a-f0-9]{64}` format. Bitcoin-style binary Merkle root over
520
+ * the per-record `recordHash` leaves (in `sequenceNumber` ascending
521
+ * order). Empty-export sentinel: `sha256:` + hex of
522
+ * `sha256("ATTESTRY-EMPTY-EXPORT")`. SDK does NOT recompute — the
523
+ * caller (post-Prompt-1) verifies independently.
524
+ */
525
+ merkleRoot: string;
526
+ /**
527
+ * Today: literal string `"unsigned-prompt-1-blocked"`. Once Prompt 1
528
+ * (Ed25519 signing) ships, the kernel will replace this field with
529
+ * a structured `proof` value carrying an `eddsa-jcs-2022` signature
530
+ * over the canonical trailer bytes. Typing as `string` (rather than
531
+ * a literal-union) accommodates that transition without an SDK bump
532
+ * — same forward-compat convention as `BulkFailedSummary.code`,
533
+ * `humanOversightState`, and `eventType`. Build-round D1.
534
+ *
535
+ * SDK does NOT attempt signature verification — caller is
536
+ * responsible (post-Prompt-1).
537
+ */
538
+ signing: string;
539
+ /** ISO-8601 timestamp string. */
540
+ generatedAt: string;
541
+ }
542
+ /**
543
+ * Discriminated union of frames in the export stream. Records arrive
544
+ * first (in `sequenceNumber` ascending order), then exactly one trailer.
545
+ * Caller branches on the trailer:
546
+ *
547
+ * @example
548
+ * ```ts
549
+ * for await (const frame of client.decisions.export({ systemId })) {
550
+ * if ("type" in frame && frame.type === "ExportTrailer") {
551
+ * // Final frame — record count + Merkle root commit + signing field.
552
+ * console.log(`exported ${frame.recordCount} records, root=${frame.merkleRoot}`);
553
+ * } else {
554
+ * // Per-record frame — DecisionListItem shape.
555
+ * console.log(frame.id, frame.sequenceNumber, frame.recordHash);
556
+ * }
557
+ * }
558
+ * ```
559
+ *
560
+ * Build-round D3 (Option A: yield typed frames, vs Option B: two-phase
561
+ * records + trailer-Promise API). Mirrors wire shape; symmetric to
562
+ * `decisions.stream`.
563
+ */
564
+ export type DecisionExportFrame = DecisionExportRecord | DecisionExportTrailer;
565
+ /**
566
+ * Result of `client.decisions.verifyChain(systemId)`. Source-of-truth
567
+ * lives kernel-side at `src/lib/decisions/chain-verification.ts:20-49`.
568
+ *
569
+ * **Critical contract**: this shape is returned for BOTH valid and
570
+ * invalid chains. `chainValid: false` is NOT an error — the kernel
571
+ * answered the customer's question (is this chain tampered?) and the
572
+ * SDK resolves the Promise with the verdict body. Top-level structural
573
+ * failures (auth, rate limit, system-not-found, ChainTooLong) throw
574
+ * `AttestryAPIError`. Carry-forward invariant #12.
575
+ *
576
+ * The two ID arrays distinguish two failure modes:
577
+ * - `tamperedRecordIds`: stored `recordHash` doesn't match the
578
+ * recomputed hash of `canonicalPayload` — direct content tampering
579
+ * (security signal, fires `chain.tampered` webhook).
580
+ * - `brokenRecordIds`: `prevRecordHash` doesn't match the running
581
+ * watermark — gap in the sequence (ops signal: record deleted /
582
+ * missing; fires `chain.broken` webhook). Both can be non-empty
583
+ * simultaneously; `chain.tampered` takes precedence at webhook
584
+ * dispatch but BOTH arrays appear in this response.
585
+ */
586
+ export interface ChainVerificationResult {
587
+ /** UUID of the system whose chain was verified. */
588
+ systemId: string;
589
+ /** Total rows replayed (active + tombstoned). */
590
+ recordCount: number;
591
+ /** Records with `tombstoned: false`. */
592
+ activeRecordCount: number;
593
+ /** Records with `tombstoned: true`. */
594
+ tombstonedRecordCount: number;
595
+ /**
596
+ * `true` iff every record's `recordHash` matches the recomputed hash
597
+ * AND every record's `prevRecordHash` matches the running watermark.
598
+ * Empty chains verify as `true` (vacuous truth).
599
+ */
600
+ chainValid: boolean;
601
+ /**
602
+ * Sequence number of the last record before tampering was first
603
+ * detected. Equals the highest sequence on a valid chain; one less
604
+ * than the first tampered/broken record's sequence on an invalid
605
+ * chain (so callers can show "verified up to sequence N"). `0` on
606
+ * empty chains AND when the very first record fails verification.
607
+ */
608
+ lastVerifiedSequence: number;
609
+ /**
610
+ * ISO-8601 string captured by the kernel at the end of verification
611
+ * (`new Date().toISOString()`). Wire is a STRING, not a `Date`
612
+ * instance. Parse via `new Date(value)` if needed.
613
+ */
614
+ lastVerifiedAt: string;
615
+ /**
616
+ * Record IDs whose stored `recordHash` doesn't match the recomputed
617
+ * hash of their `canonicalPayload` — direct content tampering
618
+ * (security signal). Empty array on a valid chain. Triggers the
619
+ * `chain.tampered` webhook server-side.
620
+ */
621
+ tamperedRecordIds: string[];
622
+ /**
623
+ * Record IDs whose `prevRecordHash` doesn't match the running
624
+ * watermark — gap in the sequence (ops signal: record deleted /
625
+ * missing). Empty array on a valid chain. Triggers the
626
+ * `chain.broken` webhook server-side. Distinct from
627
+ * `tamperedRecordIds` — both can be non-empty (tampered takes
628
+ * precedence in webhook event selection but both arrays appear in
629
+ * the webhook payload AND in this response).
630
+ */
631
+ brokenRecordIds: string[];
632
+ /**
633
+ * Server-side observability counters. Authoritative — the SDK does
634
+ * NOT add its own timer.
635
+ */
636
+ performanceMetrics: {
637
+ /** Wall-clock duration of the replay loop, milliseconds. */
638
+ verificationDurationMs: number;
639
+ /**
640
+ * Rounded throughput. `0` on empty chains AND on sub-millisecond
641
+ * verifications (kernel guards divide-by-zero). The SDK preserves
642
+ * the kernel's value verbatim — does NOT recompute.
643
+ */
644
+ recordsPerSecond: number;
645
+ };
646
+ }
647
+ export declare class DecisionsResource {
648
+ private readonly client;
649
+ constructor(client: AttestryClient);
650
+ /**
651
+ * Retrieve one decision record by id.
652
+ *
653
+ * Server returns 400 for malformed UUIDs and 404 for not-found OR
654
+ * cross-org records (deliberate conflation — see route docstring).
655
+ * Both surface as `AttestryAPIError` with the corresponding status.
656
+ *
657
+ * Throws `TypeError` synchronously for invalid `id` BEFORE issuing
658
+ * a request — empty string, non-string, lone-surrogate UTF-16, or
659
+ * path-traversal segments (`"."` / `".."` / strings containing
660
+ * `\0`) all reject. The path-traversal guard exists because
661
+ * `encodeURIComponent` does NOT encode `.` or `..`, and `fetch`'s
662
+ * URL normalization would collapse `retrieve("..")` to the LIST
663
+ * endpoint at `/api/v1/decisions/` — silently redirecting to a
664
+ * different resource. Hostile-review F1 (cross-resource fix
665
+ * symmetric to `decisions.verifyChain`); validation centralized in
666
+ * the shared `encodePathSegment` helper.
667
+ *
668
+ * Rejects with `AttestryError` (P2 hardening) if the kernel emits
669
+ * a non-object response shape (`null`, scalar, or array). Rejects
670
+ * with `AttestryAPIError` (P3 hardening) if the kernel responds
671
+ * with a non-`application/json` Content-Type — protects against
672
+ * proxy-injected HTML 200 pages parsing into junk consumer state.
673
+ */
674
+ retrieve(id: string, options?: RequestOptions): Promise<DecisionRecord>;
675
+ /**
676
+ * Append a decision record to the org's append-only hash chain.
677
+ *
678
+ * Wraps `POST /api/v1/decisions`. Returns the persisted record (with
679
+ * `canonicalPayload` BYTEA omitted — the kernel's `toResponseShape()`
680
+ * helper drops it from the wire response since the client already has
681
+ * the input digest for verification).
682
+ *
683
+ * **Idempotency replay**: subsequent calls with the same
684
+ * `idempotencyKey` AND identical canonical payload return the SAME
685
+ * record (server returns HTTP 200 `decision.idempotency_replay`; SDK
686
+ * resolves the same `Promise<DecisionRecord>` as a fresh insert,
687
+ * which returns 201). Different payload with the same key throws
688
+ * `AttestryAPIError` with `status === 409`
689
+ * `decision.idempotency_conflict`. Status-code distinction is NOT
690
+ * surfaced in the SDK return type — both 2xx resolve identically.
691
+ *
692
+ * **At-least-once delivery**: pass an `idempotencyKey` to make
693
+ * 429-retries safe under network failure. Without one, a retry that
694
+ * succeeds-but-loses-the-response could create a duplicate record.
695
+ * Body is re-stringified per attempt (carry-forward invariant #4).
696
+ *
697
+ * **Plan limits (402)**: when the org has exhausted its
698
+ * `decisionsPerMonth` quota, the kernel throws `PlanLimitError`
699
+ * (mapped to HTTP 402). The SDK surfaces it as `AttestryAPIError`
700
+ * with `status === 402` and `details: {feature, currentPlan,
701
+ * upgradeRequired}` — the structured body lets dashboards route the
702
+ * user straight to the upgrade flow (B.1 carry-forward).
703
+ *
704
+ * Errors:
705
+ * - `AttestryAPIError` (status 401) — auth required
706
+ * - `AttestryAPIError` (status 402) — plan limit (with
707
+ * `details.feature` / `details.currentPlan` /
708
+ * `details.upgradeRequired`)
709
+ * - `AttestryAPIError` (status 404) — system not found OR cross-org
710
+ * attestation (collapsed deliberately to prevent enumeration)
711
+ * - `AttestryAPIError` (status 409) — idempotency conflict (same
712
+ * key, different payload)
713
+ * - `AttestryAPIError` (status 413) — canonical payload exceeds 256KB
714
+ * - `AttestryAPIError` (status 422) — Zod validation failed (field
715
+ * errors in `details`) OR I-JSON validation failed (NaN /
716
+ * Infinity / BigInt / undefined / Symbol — `details.path` names
717
+ * the offending field) OR refine-clause failed
718
+ * (clientSignature/clientKeyId pairing)
719
+ * - `AttestryAPIError` (status 429) — rate limit (auto-retried by
720
+ * default — invariant #18)
721
+ * - `AttestryAPIError` (status 500) — internal invariant violation
722
+ * (chain head missing — should never fire in practice)
723
+ * - `AttestryError` ("invalid request body: ...") — body
724
+ * serialization failed (BigInt, circular reference) BEFORE fetch
725
+ * (carry-forward invariant #4)
726
+ * - `AttestryError` ("request aborted by caller") — caller-supplied
727
+ * `options.signal` fired (pre-aborted or mid-flight)
728
+ * - `TypeError` (synchronous, no fetch issued) — input failed SDK-
729
+ * side type validation (see below)
730
+ *
731
+ * SDK-side validation (synchronous `TypeError`, no fetch issued):
732
+ * - `input` itself: must be a non-null, non-array object
733
+ * - `systemId` / `inputDigest`: required non-empty strings
734
+ * - Optional string fields: when provided, must be non-empty strings
735
+ * (outputDigest, attestationId, clientSignature, clientKeyId,
736
+ * idempotencyKey, humanOversightState, policyOutcome)
737
+ * - Optional array fields: when provided, must be `Array.isArray`
738
+ * (frameworkClaims, toolInvocations, delegationChain). Empty
739
+ * arrays pass through.
740
+ * - Optional `zkProof`: when provided, must be a non-null,
741
+ * non-array object.
742
+ *
743
+ * **Format validation deferred to server** (UUID, hash regex,
744
+ * base64, enum membership, length caps, refine pairing,
745
+ * inner-array shape, inner-zkProof shape). Decision D5 in the
746
+ * build-round audit.
747
+ *
748
+ * @example
749
+ * ```ts
750
+ * const record = await client.decisions.ingest({
751
+ * systemId: "550e8400-e29b-41d4-a716-446655440000",
752
+ * inputDigest: "sha256:abc123...",
753
+ * frameworkClaims: [
754
+ * { framework: "eu_ai_act", article: "Art.13", claim: "human oversight provided" },
755
+ * ],
756
+ * humanOversightState: "approved",
757
+ * policyOutcome: "permitted",
758
+ * idempotencyKey: "ingest-2026-05-06-trace-789", // safe retries
759
+ * });
760
+ * console.log(record.id, record.sequenceNumber, record.recordHash);
761
+ * ```
762
+ */
763
+ ingest(input: DecisionIngestInput, options?: RequestOptions): Promise<DecisionRecord>;
764
+ /**
765
+ * Append up to 500 decision records in a single request, with a
766
+ * partial-success envelope.
767
+ *
768
+ * Wraps `POST /api/v1/decisions/bulk`. Returns a `BulkIngestResult`
769
+ * describing which records persisted and which failed — the call
770
+ * **resolves successfully even when every record failed**. Partial
771
+ * success is the entire point of the endpoint; the caller branches on
772
+ * `result.totalFailed` (or `result.failed.length`) if they care about
773
+ * per-record errors. Top-level failures (auth, rate limit, plan limit,
774
+ * oversize batch) DO throw `AttestryAPIError` with the corresponding
775
+ * HTTP status.
776
+ *
777
+ * **Per-record codes** — see `BulkFailedSummary.code` JSDoc for the
778
+ * full list. Most relevant for retries:
779
+ * - `"idempotency_unique_violation"`: race condition. Bulk does NOT
780
+ * auto-recover — retry the failed record individually via
781
+ * `decisions.ingest()` to invoke per-record race recovery.
782
+ * - `"system_not_found"`: cross-org system OR cross-system
783
+ * attestationId. Collapsed for enumeration safety.
784
+ *
785
+ * **At-least-once delivery**: pass an `idempotencyKey` on every item.
786
+ * A 429-retry of the same batch then returns duplicates as
787
+ * `failed[i].code === "idempotency_unique_violation"`; other items
788
+ * insert normally. Without per-item keys, a retry that succeeds-but-
789
+ * loses-the-response can create duplicate records.
790
+ *
791
+ * **Plan limits (402)**: the kernel checks the FULL batch size against
792
+ * the org's `decisionsPerMonth` quota. A 100-record batch with 50
793
+ * quota remaining is rejected wholesale (none persisted) — partial
794
+ * quota fills are a reconciliation hazard. The 402 carries the same
795
+ * `details: {feature, currentPlan, upgradeRequired}` shape as
796
+ * `decisions.ingest` (B.1 carry-forward).
797
+ *
798
+ * Errors:
799
+ * - `AttestryAPIError` (status 401) — auth required
800
+ * - `AttestryAPIError` (status 402) — plan limit (with
801
+ * `details.feature` / `details.currentPlan` /
802
+ * `details.upgradeRequired`)
803
+ * - `AttestryAPIError` (status 413) — defensive top-level batch
804
+ * size guard (>500 items). Verbatim message: `"Bulk ingest
805
+ * limited to 500 records per request"`. In practice the kernel's
806
+ * Zod `.max(500)` fires first with a 422; this 413 only surfaces
807
+ * if the schema is bypassed.
808
+ * - `AttestryAPIError` (status 422) — Zod validation failed (one or
809
+ * more `items` malformed; OR top-level Zod fails for >500 items
810
+ * OR empty array — server's `.min(1).max(500)`).
811
+ * - `AttestryAPIError` (status 429) — rate limit (auto-retried by
812
+ * default — invariant #18). Body re-stringified per attempt.
813
+ * - `AttestryError` ("invalid request body: ...") — body
814
+ * serialization failed (BigInt, circular reference) BEFORE fetch
815
+ * (carry-forward invariant #4)
816
+ * - `AttestryError` ("request aborted by caller") — caller-supplied
817
+ * `options.signal` fired (pre-aborted or mid-flight)
818
+ * - `TypeError` (synchronous, no fetch issued) — input failed SDK-
819
+ * side type validation (see below)
820
+ *
821
+ * Notably ABSENT from the top-level error chain (vs `decisions.ingest`):
822
+ * - 404 (system not found) → per-record `failed[i].code === "system_not_found"`
823
+ * - 409 (idempotency conflict) → per-record `code === "idempotency_conflict"`
824
+ * - 500 (chain head missing) → per-record `code === "chain_head_missing"`
825
+ *
826
+ * SDK-side validation (synchronous `TypeError`, no fetch issued):
827
+ * - `input` itself: must be a non-null, non-array object
828
+ * - `input.items`: required, must be `Array.isArray`
829
+ *
830
+ * **Format and per-item validation deferred to server**. The SDK does
831
+ * NOT pre-cap `items.length` at 500 (kernel's `.max(500)` is the
832
+ * authority — a future cap raise would otherwise require an SDK
833
+ * change). It does NOT recurse into `items[i]` to validate per-record
834
+ * shape (symmetric to ingest's `frameworkClaims` / `toolInvocations`
835
+ * policy — server's `.strict()` Zod is the schema authority).
836
+ *
837
+ * @example
838
+ * ```ts
839
+ * const result = await client.decisions.bulk({
840
+ * items: [
841
+ * { systemId, inputDigest, idempotencyKey: "trace-001" },
842
+ * { systemId, inputDigest, idempotencyKey: "trace-002" },
843
+ * ],
844
+ * });
845
+ * console.log(`${result.totalInserted}/${result.totalSubmitted} succeeded`);
846
+ * for (const failure of result.failed) {
847
+ * if (failure.code === "idempotency_unique_violation") {
848
+ * // retry via single-record endpoint to invoke race recovery
849
+ * await client.decisions.ingest(originalItems[failure.index]);
850
+ * }
851
+ * }
852
+ * ```
853
+ */
854
+ bulk(input: DecisionBulkInput, options?: RequestOptions): Promise<BulkIngestResult>;
855
+ /**
856
+ * List decision records the caller can see, cursor-paginated.
857
+ *
858
+ * Pagination is keyset-based over `(createdAt DESC, id DESC)` —
859
+ * identical-microsecond timestamps don't cause skipped rows. Pass
860
+ * back `response.nextCursor` as `input.cursor` to fetch the next page.
861
+ *
862
+ * Returns a slim per-row shape (`DecisionListItem`) — subset of
863
+ * `DecisionRecord`, deliberately omitting heavy fields. Call
864
+ * `decisions.retrieve(id)` for the full record.
865
+ *
866
+ * Errors:
867
+ * - `AttestryAPIError` (status 400) — malformed cursor (server-side)
868
+ * - `AttestryAPIError` (status 401) — auth required
869
+ * - `AttestryAPIError` (status 403) — api-key missing `read:assessments`
870
+ * - `AttestryAPIError` (status 422) — invalid query parameters
871
+ * - `AttestryAPIError` (status 429) — rate limit (auto-retried by default)
872
+ *
873
+ * SDK-side validation (throws `TypeError` synchronously):
874
+ * - Each optional string field (systemId, from, to, framework,
875
+ * article, tool, cursor) must be a non-empty string when provided.
876
+ * Format validation (UUID, ISO date) is deferred to the server.
877
+ * - `limit` must be a number when provided.
878
+ * - `includeTombstoned` must be a boolean when provided.
879
+ *
880
+ * Response-shape validation (P2 hardening):
881
+ * - Rejects with `AttestryError` if the kernel response isn't a
882
+ * non-null object, lacks an `items` array, or has a `nextCursor`
883
+ * that isn't a string-or-null. Per-row shape is faithful-courier
884
+ * (NOT validated — P4 candidate).
885
+ *
886
+ * Transport-shape validation (P3 hardening):
887
+ * - Rejects with `AttestryAPIError` if the kernel responds with a
888
+ * non-`application/json` Content-Type — protects against
889
+ * proxy-injected HTML 200 pages parsing into junk consumer state.
890
+ *
891
+ * @example
892
+ * ```ts
893
+ * let cursor: string | undefined;
894
+ * for (let page = 0; page < 100; page++) {
895
+ * const { items, nextCursor } = await client.decisions.list({
896
+ * systemId,
897
+ * limit: 50,
898
+ * cursor,
899
+ * });
900
+ * for (const item of items) console.log(item.id, item.sequenceNumber);
901
+ * if (nextCursor === null) break;
902
+ * cursor = nextCursor;
903
+ * }
904
+ * ```
905
+ */
906
+ list(input?: DecisionsListInput, options?: RequestOptions): Promise<DecisionsListResponse>;
907
+ /**
908
+ * Subscribe to decision-record events as they're appended.
909
+ *
910
+ * Returns an `AsyncIterable<DecisionStreamEvent>` — consume with
911
+ * `for await (const event of stream)`. Errors THROW (the iterator
912
+ * surfaces them via the for-await loop's natural error path), in
913
+ * contrast to `chat.stream()` which yields error chunks. Reason:
914
+ *
915
+ * - `chat.stream()` is a request/response (one POST → one iterator).
916
+ * Yielding errors inline lets consumers render them in the same UI
917
+ * stream as the assistant's text.
918
+ * - `decisions.stream()` is a long-lived subscription. An error
919
+ * means the connection is gone — yielding inline would force every
920
+ * consumer to write `if (chunk.type === 'error') break;`. Throwing
921
+ * gives clean `try/catch` semantics with typed error classes
922
+ * (`AttestryAPIError` for 4xx/5xx, `AttestryError` for network /
923
+ * abort).
924
+ *
925
+ * Errors surface as:
926
+ * - `AttestryAPIError` (status 401) — auth failed
927
+ * - `AttestryAPIError` (status 403) — insufficient permissions
928
+ * (api keys need `read:assessments` scope)
929
+ * - `AttestryAPIError` (status 400) — malformed `systemId` or
930
+ * `lastEventId` (server-side validation)
931
+ * - `AttestryAPIError` (status 429) — rate limited
932
+ * - `AttestryError` ("request aborted by caller") — caller-provided
933
+ * `options.signal` fired (pre-abort or mid-iteration)
934
+ * - `AttestryError` ("network error: ...") — fetch-level failure
935
+ * before any frame; OR mid-stream connection drop (surfaces from
936
+ * the underlying reader, wrapped during iteration)
937
+ * - `AttestryError` ("SSE frame data was not valid JSON: ...") —
938
+ * defensive; the kernel always emits valid JSON in `data:` lines.
939
+ *
940
+ * Reconnection: the iterator does NOT auto-reconnect. On any error or
941
+ * clean termination (server-side 5min timeout closes the connection),
942
+ * the for-await loop ends. The caller then decides whether to call
943
+ * `stream({lastEventId: lastSeen.eventId})` to resume.
944
+ *
945
+ * Lazy: the request is NOT issued until the first iteration. Pass
946
+ * `options.signal` for cancellation — pre-aborted causes the first
947
+ * iteration to throw `AttestryError` with no fetch issued; mid-flight
948
+ * abort surfaces as `AttestryError` from the iterator.
949
+ *
950
+ * Heartbeat frames (`: heartbeat\n\n`) are silently consumed by the
951
+ * SSE parser and never yielded to the consumer.
952
+ *
953
+ * @example
954
+ * ```ts
955
+ * try {
956
+ * for await (const event of client.decisions.stream({ systemId })) {
957
+ * console.log(event.id, event.sequenceNumber);
958
+ * lastEventId = event.eventId; // for reconnection
959
+ * }
960
+ * } catch (err) {
961
+ * if (err instanceof AttestryAPIError && err.status === 401) {
962
+ * // re-auth
963
+ * } else if (err instanceof AttestryError) {
964
+ * // network drop — wait + retry with lastEventId
965
+ * }
966
+ * }
967
+ * ```
968
+ */
969
+ stream(input?: DecisionsStreamInput, options?: RequestOptions): AsyncIterable<DecisionStreamEvent>;
970
+ /**
971
+ * Export a system's decision chain as a streaming NDJSON response.
972
+ *
973
+ * Wraps `GET /api/v1/decisions/export`. Returns an
974
+ * `AsyncIterable<DecisionExportFrame>` — records arrive first (in
975
+ * `sequenceNumber` ascending order), then exactly one trailer that
976
+ * commits the batch to a Merkle root over per-record `recordHash`
977
+ * leaves.
978
+ *
979
+ * Errors **throw** from the iterator (long-lived stream semantics —
980
+ * symmetric with `decisions.stream`). Use `try / catch` around the
981
+ * for-await loop:
982
+ *
983
+ * @example
984
+ * ```ts
985
+ * try {
986
+ * for await (const frame of client.decisions.export({ systemId })) {
987
+ * if ("type" in frame && frame.type === "ExportTrailer") {
988
+ * // Final commit — verify Merkle root client-side post-Prompt-1.
989
+ * console.log(`${frame.recordCount} records, root=${frame.merkleRoot}`);
990
+ * } else {
991
+ * // Per-record line — DecisionListItem shape.
992
+ * process(frame);
993
+ * }
994
+ * }
995
+ * } catch (err) {
996
+ * if (err instanceof AttestryAPIError && err.status === 422) {
997
+ * // bad systemId / unknown query / malformed datetime
998
+ * } else if (err instanceof AttestryError) {
999
+ * // network drop, parser error, missing trailer
1000
+ * }
1001
+ * }
1002
+ * ```
1003
+ *
1004
+ * **Empty export** — when the systemId has zero records (or doesn't
1005
+ * exist / belongs to another org), the iterator yields a SINGLE
1006
+ * frame: a trailer with `recordCount: 0`, `sequenceFrom: null`,
1007
+ * `sequenceTo: null`, and the deterministic empty-export merkleRoot
1008
+ * (`sha256:` + hex of `sha256("ATTESTRY-EMPTY-EXPORT")`). The SDK
1009
+ * does NOT throw — the empty trailer is the kernel's success signal
1010
+ * for "no data".
1011
+ *
1012
+ * **Missing trailer** — every successful 200 stream ends with a
1013
+ * trailer. If the iterator exhausts without seeing one (mid-stream
1014
+ * connection drop, kernel error after headers committed), the SDK
1015
+ * throws `AttestryError("decisions.export: stream ended without
1016
+ * trailer — connection dropped or server failed mid-stream")`. This
1017
+ * surfaces a class of failures that the kernel can't return as 4xx
1018
+ * (the response was already 200 by the time the error arose).
1019
+ *
1020
+ * **Trailer signing field** — today the trailer's `signing` field is
1021
+ * the literal string `"unsigned-prompt-1-blocked"`. Once Prompt 1
1022
+ * ships Ed25519 signing, the field is replaced by a structured proof.
1023
+ * The SDK does NOT verify the signature — caller is responsible
1024
+ * (post-Prompt-1).
1025
+ *
1026
+ * Errors:
1027
+ * - `AttestryAPIError` (status 401) — auth required
1028
+ * - `AttestryAPIError` (status 422) — invalid query (missing
1029
+ * systemId / unknown key / non-UUID systemId / malformed datetime)
1030
+ * - `AttestryAPIError` (status 429) — rate limit (auto-retried by
1031
+ * default — invariant #18; initial fetch only — invariant #20)
1032
+ * - `AttestryAPIError` — wrong content-type at 200 (proxy / LB
1033
+ * error page wrapped at 200)
1034
+ * - `AttestryError` ("decisions.export: stream ended without
1035
+ * trailer ...") — mid-stream failure detected at iterator end
1036
+ * - `AttestryError` ("network error during stream: ...") — TCP
1037
+ * drop / proxy hang-up mid-stream
1038
+ * - `AttestryError` ("request aborted by caller") — caller-supplied
1039
+ * `options.signal` fired (pre-aborted or mid-flight)
1040
+ * - `AttestryError` ("NDJSON line was not valid JSON: ...") —
1041
+ * defensive; the kernel always emits valid JSON
1042
+ * - `AttestryError` ("NDJSON line exceeded maximum buffer size ...") —
1043
+ * defensive; the kernel's per-record line is well below 1 MiB
1044
+ * - `TypeError` (synchronous, no fetch issued) — input failed
1045
+ * SDK-side type validation (see below)
1046
+ *
1047
+ * Notably ABSENT from the error surface:
1048
+ * - **No 402 plan-limit** — export is a READ, doesn't count against
1049
+ * the org's `decisionsPerMonth` quota.
1050
+ * - **No 404 system-not-found** — a non-existent or cross-org
1051
+ * systemId returns 200 with zero records and a trailer with
1052
+ * `recordCount: 0`. Consumers detect via the trailer.
1053
+ *
1054
+ * SDK-side validation (synchronous `TypeError`, no fetch issued):
1055
+ * - `input` itself: must be a non-null, non-array object
1056
+ * - `input.systemId`: required, non-empty string
1057
+ * - `input.from` / `input.to`: optional; non-empty string when provided
1058
+ * - `input.includeTombstoned`: optional; boolean when provided
1059
+ *
1060
+ * Format validation deferred to server (UUID, ISO datetime). The
1061
+ * `includeTombstoned: false` boolean is forwarded LITERALLY (no
1062
+ * workaround) — the kernel session-6 fix to `stringBoolean` accepts
1063
+ * `"false"` correctly. Asymmetry from `decisions.list` (which still
1064
+ * omits `false` as defense-in-depth) is deliberate — build-round D7.
1065
+ *
1066
+ * Lazy: the request is NOT issued until the first iteration. Pass
1067
+ * `options.signal` for cancellation — pre-aborted causes the first
1068
+ * iteration to throw `AttestryError` with no fetch issued; mid-flight
1069
+ * abort surfaces as `AttestryError` from the iterator.
1070
+ */
1071
+ export(input: DecisionsExportInput, options?: RequestOptions): AsyncIterable<DecisionExportFrame>;
1072
+ /**
1073
+ * Replay a system's hash chain and report integrity verdict.
1074
+ *
1075
+ * Wraps `GET /api/v1/decisions/verify-chain/{systemId}`. Returns a
1076
+ * `ChainVerificationResult` describing whether tampering was detected
1077
+ * and which records (if any) failed which check.
1078
+ *
1079
+ * **Critical contract — partial-success envelope**: the kernel returns
1080
+ * **HTTP 200 with `chainValid: false`** when tampering is detected.
1081
+ * The SDK resolves the Promise with the verdict body — it does **NOT**
1082
+ * throw on `chainValid: false`. The customer asked the chain-integrity
1083
+ * question and the kernel answered; the SDK is a faithful courier.
1084
+ * Top-level structural failures (auth, rate limit, system-not-found,
1085
+ * `ChainTooLong`) DO throw `AttestryAPIError`. Carry-forward invariant
1086
+ * #12; same family as `decisions.bulk` (200 with `totalFailed > 0`
1087
+ * resolves rather than throws).
1088
+ *
1089
+ * **Failure-mode discrimination**: the two ID arrays are surfaced
1090
+ * separately so consumers can route on the SECURITY-vs-OPS distinction
1091
+ * at the call site (the kernel uses the same distinction to fire the
1092
+ * `chain.tampered` vs `chain.broken` webhook):
1093
+ * - `tamperedRecordIds`: direct content tampering (security signal).
1094
+ * - `brokenRecordIds`: gap in the chain (ops signal — missing record).
1095
+ * Both arrays can be non-empty simultaneously.
1096
+ *
1097
+ * **Side effect (out-of-band)**: the kernel dispatches one of three
1098
+ * fire-and-forget webhooks AFTER the response body is built but BEFORE
1099
+ * returning — `chain.verified` (when valid), `chain.tampered` (when
1100
+ * `tamperedRecordIds.length > 0`), or `chain.broken` (when only
1101
+ * `brokenRecordIds.length > 0`). The SDK does NOT see / verify these;
1102
+ * they're surfaced through the webhooks resource (a different SDK
1103
+ * surface). Consumers who want webhook-based observability subscribe
1104
+ * via the kernel's webhook endpoints.
1105
+ *
1106
+ * **413 with export hint**: when the chain length exceeds
1107
+ * `MAX_SYNC_CHAIN_LENGTH` (50,000 records), the kernel returns 413
1108
+ * with the export-endpoint hint. The transport stores the entire
1109
+ * parsed error body under `AttestryAPIError.details`, and the kernel's
1110
+ * own structured `details` object nests inside — so the consumer-side
1111
+ * access path is `err.details?.details?.hint` (double-`details`).
1112
+ * Consumers detect this case via
1113
+ * `error.details?.details?.hint?.includes("decisions/export")` and
1114
+ * fall back to streaming the chain through `decisions.export()` for
1115
+ * offline verification. The `ChainTooLongError` kernel class is
1116
+ * internal; its 413 surface is what the SDK exposes.
1117
+ *
1118
+ * Errors:
1119
+ * - `AttestryAPIError` (status 400) — `systemId` failed server-side
1120
+ * UUID format check (`isValidUuid` rejects, NOT a Zod 422).
1121
+ * - `AttestryAPIError` (status 401) — auth required (no session
1122
+ * and no api-key).
1123
+ * - `AttestryAPIError` (status 403) — propagates if upstream
1124
+ * `AuthError` was thrown with a custom 403 statusCode (rare; the
1125
+ * route's default for cross-org systems is 404).
1126
+ * - `AttestryAPIError` (status 404) — system not found OR cross-org
1127
+ * system (deliberate enumeration-safety collapse, same shape as
1128
+ * retrieve / ingest).
1129
+ * - `AttestryAPIError` (status 413) — `ChainTooLong` (>50,000 records)
1130
+ * with `err.details?.details?.hint` referencing `/api/v1/decisions/export`.
1131
+ * (Double-`details`: transport stores the whole parsed body under
1132
+ * `.details`; the kernel's own `details` object nests inside.)
1133
+ * - `AttestryAPIError` (status 429) — rate limit (auto-retried by
1134
+ * default — invariant #18; per-IP `assessmentLimiter`).
1135
+ * - `AttestryAPIError` (status 500) — internal error with a SCRUBBED
1136
+ * message (no leak of the underlying kernel error). Surfaces e.g.
1137
+ * when the DB connection drops mid-verification.
1138
+ * - `AttestryError` ("request aborted by caller") — caller-supplied
1139
+ * `options.signal` fired (pre-aborted or mid-flight).
1140
+ * - `TypeError` (synchronous, no fetch issued) — `systemId` failed
1141
+ * SDK-side validation (empty / non-string / lone-surrogate /
1142
+ * path-traversal segment).
1143
+ *
1144
+ * Notably ABSENT from the error chain:
1145
+ * - **No 402 plan-limit** — verifyChain is a READ; doesn't count
1146
+ * against `decisionsPerMonth` quota.
1147
+ * - **No 422** — the only input is a path segment, validated as a
1148
+ * UUID via `isValidUuid` (which returns 400, not 422). No query
1149
+ * schema, no body schema, no Zod.
1150
+ *
1151
+ * SDK-side validation (synchronous `TypeError`, no fetch issued):
1152
+ * - `systemId`: must be a non-empty string.
1153
+ * - `systemId`: must NOT be the exact string `"."` or `".."` — these
1154
+ * survive `encodeURIComponent` but get collapsed by `fetch`'s
1155
+ * URL normalization into the parent endpoint, silently redirecting
1156
+ * the request to a different resource. NUL bytes (`\0`) also
1157
+ * rejected. Hostile-review F1.
1158
+ * - `systemId`: must be encodable via `encodeURIComponent` — lone
1159
+ * surrogates throw a synchronous `TypeError` with `cause: err`
1160
+ * wrapping the original `URIError`. Mirror of `decisions.retrieve`'s
1161
+ * L1 pattern (carry-forward invariant #32).
1162
+ *
1163
+ * **Format validation deferred to server** (UUID format check happens
1164
+ * server-side via `isValidUuid`, returns 400).
1165
+ *
1166
+ * Response-shape validation (P2 hardening):
1167
+ * - Rejects with `AttestryError` if the kernel response isn't a
1168
+ * non-null object (`null`, scalar, or array). Per-field shape
1169
+ * (e.g. `chainValid: boolean`) is faithful-courier — NOT
1170
+ * validated.
1171
+ *
1172
+ * Transport-shape validation (P3 hardening):
1173
+ * - Rejects with `AttestryAPIError` if the kernel responds with a
1174
+ * non-`application/json` Content-Type. NOTE: `chainValid: false`
1175
+ * is a normal 200 response and resolves the promise (carry-forward
1176
+ * invariant #12); only structural failures throw.
1177
+ *
1178
+ * @example
1179
+ * ```ts
1180
+ * const verdict = await client.decisions.verifyChain(systemId);
1181
+ * if (!verdict.chainValid) {
1182
+ * if (verdict.tamperedRecordIds.length > 0) {
1183
+ * // SECURITY signal: someone edited stored bytes / hashes.
1184
+ * await notifySecurity({ systemId, ids: verdict.tamperedRecordIds });
1185
+ * } else if (verdict.brokenRecordIds.length > 0) {
1186
+ * // OPS signal: a record went missing.
1187
+ * await notifyOps({ systemId, ids: verdict.brokenRecordIds });
1188
+ * }
1189
+ * }
1190
+ * console.log(`verified up to sequence ${verdict.lastVerifiedSequence}`);
1191
+ *
1192
+ * // 413 → fall back to export + offline verification:
1193
+ * try {
1194
+ * await client.decisions.verifyChain(largeSystemId);
1195
+ * } catch (err) {
1196
+ * if (err instanceof AttestryAPIError && err.status === 413) {
1197
+ * // err.details?.details?.hint references /api/v1/decisions/export
1198
+ * // (double-details: transport's wrap + kernel's structured detail)
1199
+ * for await (const frame of client.decisions.export({ systemId: largeSystemId })) {
1200
+ * // verify chain offline ...
1201
+ * }
1202
+ * }
1203
+ * }
1204
+ * ```
1205
+ */
1206
+ verifyChain(systemId: string, options?: RequestOptions): Promise<ChainVerificationResult>;
1207
+ }
1208
+ //# sourceMappingURL=decisions.d.ts.map