verdifax 0.3.0 → 1.1.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/README.md CHANGED
@@ -49,17 +49,177 @@ if (v.verified) {
49
49
  | --- | --- | --- |
50
50
  | `client.health()` | `GET /health` | none |
51
51
  | `client.version()` | `GET /version` | none |
52
+ | `client.getOpenAPI()` | `GET /openapi.json` | none |
53
+ | `client.getStats()` | `GET /stats` | required |
52
54
  | `client.execute(req)` | `POST /execute` | required |
55
+ | `client.attest(req)` | `POST /execute` | required |
56
+ | `client.attestClaudeResponse(args)` | `POST /execute` | required |
57
+ | `client.attestOpenAIResponse(args)` | `POST /execute` | required |
58
+ | `client.executeBatch(runs)` | `POST /execute/batch` | required |
59
+ | `client.verifyDeterminism(req)` | `POST /execute/verify-determinism` | required |
53
60
  | `client.getRun(id)` | `GET /runs/{id}` | required |
54
61
  | `client.verifyRun(id)` | `GET /runs/{id}/verify` | required |
62
+ | `client.listRuns(params)` | `GET /runs` | required |
63
+ | `client.searchRuns(params)` | `GET /runs/search` | required |
64
+ | `client.exportRuns(params)` | `GET /runs/export` | required |
65
+ | `client.exportRunsCSV(params)` | `GET /runs/export?format=csv` | required |
66
+ | `client.getRunArtifacts(id)` | `GET /runs/{id}/artifacts` | required |
67
+ | `client.getRunReport(id)` | `GET /runs/{id}/report` | required |
68
+ | `client.downloadAuditPDF(id, format)` | `GET /runs/{id}/report.pdf` | required |
69
+ | `client.getAllowToken(id)` | `GET /runs/{id}/allow-token` | required |
70
+ | `client.getDenyReceipt(id)` | `GET /runs/{id}/deny-receipt` | required |
71
+ | `client.getCCVHaltReceipt(id)` | `GET /runs/{id}/ccv-halt-receipt` | required |
72
+ | `client.getMACCHaltReceipt(id)` | `GET /runs/{id}/macc-halt-receipt` | required |
73
+ | `client.getAivpT4HaltReceipt(id)` | `GET /runs/{id}/aivp-t4-halt-receipt` | required |
74
+ | `client.getAttestationGapReceipt(id)` | `GET /runs/{id}/attestation-gap-receipt` | required |
75
+ | `client.getCESAuditRecord(id)` | `GET /runs/{id}/ces-audit-record` | required |
76
+ | `client.listDeletionReceipts(id)` | `GET /runs/{id}/deletion-receipts` | required |
77
+ | `client.listAttestations(params)` | `GET /attestations` | required |
78
+ | `client.getAttestation(id)` | `GET /attestations/{id}` | required |
79
+ | `client.getPublicBundle(id)` | `GET /runs/{id}/bundle.json` | none |
80
+ | `client.getPublicBundleByToken(token)` | `GET /public/runs/{token}/bundle.json` | none |
81
+ | `client.dcaeVerify(bundle)` | `POST /dcae/verify` | none |
55
82
  | `client.registerRequestKey()` | `POST /keys/request-key` | required |
83
+ | `client.rotateRequestKey(newSigningKey)` | `POST /keys/request-key` | required |
56
84
  | `client.clearRequestKey()` | `DELETE /keys/request-key` | required |
85
+ | `client.adminCreateKey(name)` | `POST /admin/keys` | admin |
86
+ | `client.adminListKeys()` | `GET /admin/keys` | admin |
87
+ | `client.adminRevokeKey(id)` | `DELETE /admin/keys/{id}` | admin |
88
+ | `client.adminClearRequestKey(id)` | `DELETE /admin/keys/{id}/request-key` | admin |
89
+ | `client.adminErase(req)` | `POST /admin/erase` | admin |
57
90
 
58
- The full HTTP surface (every endpoint, deletion-receipt list,
59
- DCAE verify, admin endpoints) is documented in the
60
- [OpenAPI spec](https://github.com/Verdifax/verdifax-orchestrator/blob/main/openapi/verdifax.yaml).
61
- This SDK ships the canonical operations; broader coverage lands
62
- as integrators ask.
91
+ `client.job(jobId)` returns a [job helper](#jobs) and makes no request until a step runs.
92
+
93
+ These functions make no request: `captureEnvironment`, `formatProviderPayload`, `signRequest`, `signRequestV2`, `requestKeyRegistrationBody`, `requestKeyChangeApproval`, `requestKeyChangePreimage`, `requestSignaturePreimage`, `requestSignaturePreimageV2`, `generateRequestKeyPair`, `validateHex64`, `validateJobName`, `validateRouteId`, `dseDispatchHash`, `dcaeClosureHash` and `cresReceiptHash`. `RUN_STATUSES` lists every run status the server uses.
94
+
95
+ `getRunArtifacts`, `getPublicBundle` and `getPublicBundleByToken` return the exact JSON text the server sent, so you can save it and check it with `verdifax-verify`.
96
+
97
+ ## Versioning
98
+
99
+ From 1.0, a breaking change needs a new major version.
100
+
101
+ ## Settings
102
+
103
+ `new VerdifaxClient(config)` takes these options. `fromEnv()` reads the same settings from the environment.
104
+
105
+ | Setting | Option | Environment variable | Default |
106
+ | --- | --- | --- | --- |
107
+ | Base URL | `baseURL` | `VERDIFAX_BASE_URL`, then `VERDIFAX_API_URL` | `https://api.verdifax.com` |
108
+ | API key | `apiKey` | `VERDIFAX_API_KEY` | none |
109
+ | Timeout | `timeoutMs` | `VERDIFAX_TIMEOUT_MS` | 90 seconds |
110
+ | User agent | `userAgent` | `VERDIFAX_USER_AGENT` | `verdifax-sdk-node/<version>` |
111
+ | Retries | `maxRetries` | `VERDIFAX_MAX_RETRIES` | 3 |
112
+ | First retry delay | `retryBaseDelayMs` | none | 200 ms |
113
+
114
+ GET, HEAD and OPTIONS requests are retried after a network failure or an HTTP 429, 502, 503 or 504, with exponential backoff and full jitter. POST is never retried.
115
+
116
+ ## Batch runs
117
+
118
+ `executeBatch(runs)` sends up to 1,000 runs in one request and refuses more before sending anything. A row that fails comes back in `results` with `ok: false`, so the call does not throw for it.
119
+
120
+ ```typescript
121
+ const batch = await client.executeBatch([
122
+ { program_id, route_id, registry_record_hash, payload_text: "first" },
123
+ { program_id, route_id, registry_record_hash, payload_text: "second" },
124
+ ]);
125
+
126
+ for (const row of batch.results) {
127
+ console.log(row.index, row.ok, row.ok ? row.run_id : row.error);
128
+ }
129
+ ```
130
+
131
+ The client's timeout covers the whole batch, so raise `timeoutMs` for a large one. A batch is one POST and it is never retried. After a timeout the server may have run some or all of the rows, so list your runs before you send them again.
132
+
133
+ ## Jobs
134
+
135
+ A job links several runs into one map: each step names the records it directly follows, so work can split into branches and join again. `client.job(jobId)` returns a helper that keeps the map for you.
136
+
137
+ ```typescript
138
+ const job = client.job("loan-1042");
139
+ const run = { program_id, route_id, registry_record_hash };
140
+
141
+ await job.step("intake", { ...run, payload_text: "application received" });
142
+
143
+ // Two branches after one step.
144
+ await job.step("credit", { ...run, payload_text: "credit check", after: ["intake"] });
145
+ await job.step("income", { ...run, payload_text: "income check", after: ["intake"] });
146
+
147
+ // A final step joining them.
148
+ await job.step("decide", {
149
+ ...run,
150
+ payload_text: "decision",
151
+ after: ["credit", "income"],
152
+ final: true,
153
+ });
154
+
155
+ console.log(job.recordHashes); // step id to record hash
156
+ console.log(job.signedRequests); // step id to signed text, when the client signs
157
+ ```
158
+
159
+ `job.step(stepId, options)` takes everything `attest` takes, plus `after`, `external_previous` and `final`, and returns what `attest` returns. `after` lists step ids in this job. Without it, a step follows the one run just before it through this helper, and the first step follows nothing. An `after` that names an unknown step throws a `VerdifaxError` before anything is sent.
160
+
161
+ A refused or halted step stays in the map, so a later step can follow it. `job.step` records its hash and signed text, then throws the same `APIError` that `attest` throws, with the hash in `err.recordHash`. A step that sealed nothing, such as a timeout or a 409, is not in the map and does not move the default `after`, so it can run again under the same id. To run branches at the same time, give each one its `after`. An empty `after` starts another root step.
162
+
163
+ A successful step records `record_hash`, or `manifest_hash` if the response has no `record_hash`. If it has neither, `job.step` throws a `VerdifaxError`, so the map never silently gets a gap. A mistyped option such as `afer` is refused before sending, so a typo cannot silently drop a link.
164
+
165
+ `execute`, `attest` and each row of `executeBatch` also take a `job` block directly: `{ job_id, step_id, previous_record_hashes, external_previous, final }`. Results carry `record_hash`. `job_id` and `step_id` are 1 to 128 letters, digits, `.`, `_`, `:` or `-`, and any other name is refused before sending. The SDK sorts both lists and removes repeats, and refuses unknown fields, a `final` that is not true or false, and hash lists that are not lists. A determinism check runs twice, so `verifyDeterminism` refuses a job.
166
+
167
+ The server refuses a step in a closed job, a repeated step id, and a final step that leaves a branch with no step after it. To check a whole job, save each record in one folder and run `verdifax-verify --job FOLDER`.
168
+
169
+ A job map covers what was sent. It cannot show a step that was never sent.
170
+
171
+ ## Evidence export
172
+
173
+ `exportRuns` lists the runs the server holds for your key in a period, failures included, with up to 10,000 rows per export. `exportRunsCSV` returns the same rows as raw CSV text.
174
+
175
+ ```typescript
176
+ const period = { from: "2026-11-01T00:00:00Z", to: "2026-12-01T00:00:00Z" };
177
+
178
+ const evidence = await client.exportRuns(period);
179
+ console.log(evidence.total, evidence.truncated, evidence.by_status);
180
+
181
+ const { csv, total, truncated } = await client.exportRunsCSV(period);
182
+ if (truncated) {
183
+ console.log(`The CSV holds part of ${total} runs. Use a narrower period.`);
184
+ }
185
+ ```
186
+
187
+ Pass `job_id` in the period to export one job. JSON rows also carry `anchor_source` (`bundle`, `receipt` or `none`), and `job_id` and `step_id` when the run is a job step.
188
+
189
+ The export states what the server stored, not a verdict. A call that never reached Verdifax cannot appear in it. Both forms say when a period holds more rows than the export returned: check `truncated`. `exportRunsCSV` reads `total` and `truncated` from the `X-Verdifax-Total` and `X-Verdifax-Truncated` response headers, and throws if the server does not send them. In the CSV, a `route_id` that a spreadsheet could read as a formula gets a leading `'`.
190
+
191
+ ## Provider helpers
192
+
193
+ `attestClaudeResponse` and `attestOpenAIResponse` attest a prompt and response as one payload: canonical JSON, exactly `{"prompt":...,"provider":...,"response":...}` with sorted keys and no extra whitespace, UTF-8, with `provider` set to `anthropic` or `openai`. `formatProviderPayload` builds that text. All four Verdifax SDKs produce the same bytes. A helper call from an earlier version used a different payload, so it gave a different hash for the same text; records already stored are not touched.
194
+
195
+ ```typescript
196
+ await client.attestClaudeResponse({
197
+ program_id,
198
+ route_id,
199
+ registry_record_hash,
200
+ prompt: "What is the boiling point of water?",
201
+ response: "100 C at sea level.",
202
+ govern: true,
203
+ });
204
+ ```
205
+
206
+ Both helpers take the same optional `job` block as `attest` and return `record_hash` and `signed_request` the same way.
207
+
208
+ `govern` is false by default, which changes earlier behavior. Set `govern: true` to also send the response text as `ai_output_text` for the AIVP review, which runs on Anthropic in production. Without it, the AI output text is not sent for review.
209
+
210
+ ## Optional execute fields
211
+
212
+ `execute`, `attest`, `executeBatch` and `verifyDeterminism` accept `attested_context`, `reproducibility_context`, `request_substance`, `authorization_chain`, `regulatory_scaffolding` and `causal_graph`. Each is recorded as supplied and Verdifax does not check it. The client derives `attested` and `declared` from the fields you provide. Leave out `request_substance.origin.source_ip_hash` and `user_agent_hash`: the server computes them. `execute`, `attest` and `verifyDeterminism` refuse them before sending, and a batch row that sets them fails in `results`.
213
+
214
+ `execute`, `attest` and `verifyDeterminism` check that `program_id` and `registry_record_hash` are 64 lowercase hex characters and that `route_id` is 1 to 256 printable ASCII characters.
215
+
216
+ ## Environment capture
217
+
218
+ `captureEnvironment()` records the Node version, the platform, the packages under `node_modules`, the git commit and any seeds you declare. Pass the result as `reproducibility_context` on `execute`, `attest`, `executeBatch` or `verifyDeterminism`. Detection is best effort, and a value it cannot read is left out. The server records these values as given and does not check them.
219
+
220
+ ## Determinism check
221
+
222
+ `verifyDeterminism(req)` runs one payload twice on the server and compares the manifest hashes. `deterministic` is true when both runs ended `ok` and the two hashes match. Both runs are recorded, with the status `probe`.
63
223
 
64
224
  ## Authentication
65
225
 
@@ -105,6 +265,7 @@ Request signing is optional. A client with no signing key sends the same request
105
265
  You can register an Ed25519 public key on your API key, so that each run must also be signed with a private key that only you hold. The key pair is created on your machine, and Verdifax receives only the public key.
106
266
 
107
267
  ```typescript
268
+ import { writeFileSync } from "node:fs";
108
269
  import { VerdifaxClient, generateRequestKeyPair } from "verdifax";
109
270
 
110
271
  // Run once, then store privateKey as a secret.
@@ -117,20 +278,31 @@ const client = new VerdifaxClient({
117
278
 
118
279
  await client.registerRequestKey(); // sends the public key with its proof
119
280
 
120
- // execute, attest, attestClaudeResponse and attestOpenAIResponse now sign.
121
- await client.execute({
281
+ // execute, attest, attestClaudeResponse, attestOpenAIResponse, executeBatch
282
+ // and verifyDeterminism now sign.
283
+ const run = await client.execute({
122
284
  program_id: "1111111111111111111111111111111111111111111111111111111111111111",
123
285
  route_id: "demo-attestation",
124
286
  registry_record_hash: "2222222222222222222222222222222222222222222222222222222222222222",
125
287
  payload_text: "first signed attestation",
126
288
  });
127
289
 
128
- await client.clearRequestKey(); // stops requiring signatures on this API key
290
+ // The exact text that was signed. Keep it for verdifax-verify --request FILE.
291
+ writeFileSync("signed-request.json", run.signed_request!);
292
+
293
+ // The current key approves the change, and this client then signs with the new key.
294
+ await client.rotateRequestKey(generateRequestKeyPair().privateKey);
295
+
296
+ await client.clearRequestKey(); // stops requiring signatures, and this client stops signing
129
297
  ```
130
298
 
131
- Registering a key changes that API key for every client that uses it: the server then refuses a run without a valid signature, so every caller on the key needs the private key. Each request carries the time and a fresh random nonce. The server refuses a repeated nonce and a time more than five minutes from its own clock.
299
+ A signed call serializes the request once and signs that exact text, using signature version 2, so the signature covers the whole request. It sends `signed_request` and `request_signature` and nothing else, and a batch row has the same shape. The result carries the text as `signed_request`, on each row for a batch. A refused or halted run throws the usual `APIError`, with the text in `err.signedRequest`. It holds your payload, so it stays out of the error message and logging the error does not print it: read it by name.
300
+
301
+ Registering a key changes that API key for every client that uses it: the server then refuses a run without a valid signature, so every caller on the key needs the private key. An API key alone cannot replace or clear a registered key: `rotateRequestKey` and `clearRequestKey` send the current key's approval. After a successful `clearRequestKey` the client drops its signing key and sends unsigned requests, because the server refuses a signed run on an API key with no registered key. To sign again, create a client with a `signingKey` and register it. Each request carries the time and a fresh random nonce. The server refuses a repeated nonce and a time more than five minutes from its own clock.
302
+
303
+ In a batch each row carries its own signature and nonce, and the server checks them as it reaches that row. A row refused for its signature comes back in `results`, so a repeated batch returns HTTP 200 with failed rows, and a row reached more than five minutes after it was signed fails the same way. A determinism check uses one signature for the request, and repeating it returns HTTP 403.
132
304
 
133
- The signature shows the holder of the key requested the run. It does not show the key holder accepted the outcome.
305
+ A signature shows who requested the run, not that they accepted the outcome.
134
306
 
135
307
  ## Trust model
136
308