@attestry/sdk 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +1269 -0
- package/dist/client.d.ts +58 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +74 -0
- package/dist/client.js.map +1 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +43 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +41 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/lines-parser.d.ts +50 -0
- package/dist/lines-parser.d.ts.map +1 -0
- package/dist/lines-parser.js +211 -0
- package/dist/lines-parser.js.map +1 -0
- package/dist/ndjson-parser.d.ts +57 -0
- package/dist/ndjson-parser.d.ts.map +1 -0
- package/dist/ndjson-parser.js +245 -0
- package/dist/ndjson-parser.js.map +1 -0
- package/dist/resources/abac-policies.d.ts +1034 -0
- package/dist/resources/abac-policies.d.ts.map +1 -0
- package/dist/resources/abac-policies.js +1519 -0
- package/dist/resources/abac-policies.js.map +1 -0
- package/dist/resources/audit-log.d.ts +588 -0
- package/dist/resources/audit-log.d.ts.map +1 -0
- package/dist/resources/audit-log.js +629 -0
- package/dist/resources/audit-log.js.map +1 -0
- package/dist/resources/batch.d.ts +845 -0
- package/dist/resources/batch.d.ts.map +1 -0
- package/dist/resources/batch.js +1074 -0
- package/dist/resources/batch.js.map +1 -0
- package/dist/resources/chat.d.ts +151 -0
- package/dist/resources/chat.d.ts.map +1 -0
- package/dist/resources/chat.js +124 -0
- package/dist/resources/chat.js.map +1 -0
- package/dist/resources/check.d.ts +348 -0
- package/dist/resources/check.d.ts.map +1 -0
- package/dist/resources/check.js +543 -0
- package/dist/resources/check.js.map +1 -0
- package/dist/resources/compliance-check.d.ts +330 -0
- package/dist/resources/compliance-check.d.ts.map +1 -0
- package/dist/resources/compliance-check.js +402 -0
- package/dist/resources/compliance-check.js.map +1 -0
- package/dist/resources/decisions.d.ts +1208 -0
- package/dist/resources/decisions.d.ts.map +1 -0
- package/dist/resources/decisions.js +1362 -0
- package/dist/resources/decisions.js.map +1 -0
- package/dist/resources/evidence-pack.d.ts +1080 -0
- package/dist/resources/evidence-pack.d.ts.map +1 -0
- package/dist/resources/evidence-pack.js +1789 -0
- package/dist/resources/evidence-pack.js.map +1 -0
- package/dist/resources/gate.d.ts +613 -0
- package/dist/resources/gate.d.ts.map +1 -0
- package/dist/resources/gate.js +737 -0
- package/dist/resources/gate.js.map +1 -0
- package/dist/resources/incidents.d.ts +136 -0
- package/dist/resources/incidents.d.ts.map +1 -0
- package/dist/resources/incidents.js +229 -0
- package/dist/resources/incidents.js.map +1 -0
- package/dist/resources/regulatory-changes.d.ts +307 -0
- package/dist/resources/regulatory-changes.d.ts.map +1 -0
- package/dist/resources/regulatory-changes.js +365 -0
- package/dist/resources/regulatory-changes.js.map +1 -0
- package/dist/resources/safe-input-read.d.ts +21 -0
- package/dist/resources/safe-input-read.d.ts.map +1 -0
- package/dist/resources/safe-input-read.js +57 -0
- package/dist/resources/safe-input-read.js.map +1 -0
- package/dist/resources/ship-gate.d.ts +475 -0
- package/dist/resources/ship-gate.d.ts.map +1 -0
- package/dist/resources/ship-gate.js +727 -0
- package/dist/resources/ship-gate.js.map +1 -0
- package/dist/resources/vision.d.ts +540 -0
- package/dist/resources/vision.d.ts.map +1 -0
- package/dist/resources/vision.js +1036 -0
- package/dist/resources/vision.js.map +1 -0
- package/dist/retry.d.ts +103 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +224 -0
- package/dist/retry.js.map +1 -0
- package/dist/sse-parser.d.ts +64 -0
- package/dist/sse-parser.d.ts.map +1 -0
- package/dist/sse-parser.js +271 -0
- package/dist/sse-parser.js.map +1 -0
- package/dist/transport.d.ts +142 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +455 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,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
|