verdifax 1.1.0 → 1.3.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
@@ -18,6 +18,8 @@ Requires Node 22 or newer.
18
18
 
19
19
  ## Quickstart
20
20
 
21
+ On a trial key, register a request key first and give the client that key: see [Request signing](#request-signing). Without it, a trial key refuses the run.
22
+
21
23
  ```typescript
22
24
  import { VerdifaxClient } from "verdifax";
23
25
 
@@ -82,21 +84,44 @@ if (v.verified) {
82
84
  | `client.registerRequestKey()` | `POST /keys/request-key` | required |
83
85
  | `client.rotateRequestKey(newSigningKey)` | `POST /keys/request-key` | required |
84
86
  | `client.clearRequestKey()` | `DELETE /keys/request-key` | required |
85
- | `client.adminCreateKey(name)` | `POST /admin/keys` | admin |
87
+ | `client.uploadPolicy(document)` | `POST /policies` | required |
88
+ | `client.listPolicies()` | `GET /policies` | required |
89
+ | `client.getPolicy(documentHash)` | `GET /policies/{document_hash}` | required |
90
+ | `client.testPolicy({ inputs, document?, documentHash? })` | `POST /policies/test` | required |
91
+ | `client.getPolicyHistory()` | `GET /policies/history` | required |
92
+ | `client.activatePolicy(documentHash)` | `POST /policies/{document_hash}/activate` | required |
93
+ | `client.getAccount()` | `GET /account` | owner key |
94
+ | `client.createAccountKey(name, { expiresAt? })` | `POST /account/keys` | owner key |
95
+ | `client.listAccountKeys()` | `GET /account/keys` | owner key |
96
+ | `client.revokeAccountKey(id)` | `DELETE /account/keys/{id}` | owner key |
97
+ | `client.registerAccountRequestKey()` | `POST /account/request-key` | owner key |
98
+ | `client.clearAccountRequestKey()` | `DELETE /account/request-key` | owner key |
99
+ | `client.listAccountRuns(params)` | `GET /account/runs` | owner key |
100
+ | `client.exportAccountRuns(params)` | `GET /account/runs/export` | owner key |
101
+ | `client.exportAccountRunsCSV(params)` | `GET /account/runs/export?format=csv` | owner key |
102
+ | `client.uploadAccountPolicy(document)` | `POST /account/policies` | owner key |
103
+ | `client.listAccountPolicies()` | `GET /account/policies` | owner key |
104
+ | `client.getAccountPolicy(documentHash)` | `GET /account/policies/{document_hash}` | owner key |
105
+ | `client.testAccountPolicy({ inputs, document?, documentHash? })` | `POST /account/policies/test` | owner key |
106
+ | `client.getAccountPolicyHistory()` | `GET /account/policies/history` | owner key |
107
+ | `client.activateAccountPolicy(documentHash)` | `POST /account/policies/{document_hash}/activate` | owner key |
108
+ | `client.adminCreateKey(name, { accountId? })` | `POST /admin/keys` | admin |
86
109
  | `client.adminListKeys()` | `GET /admin/keys` | admin |
87
110
  | `client.adminRevokeKey(id)` | `DELETE /admin/keys/{id}` | admin |
88
111
  | `client.adminClearRequestKey(id)` | `DELETE /admin/keys/{id}/request-key` | admin |
112
+ | `client.adminCreateAccount(name, { tier?, expiresAt? })` | `POST /admin/accounts` | admin |
113
+ | `client.adminListAccounts()` | `GET /admin/accounts` | admin |
89
114
  | `client.adminErase(req)` | `POST /admin/erase` | admin |
90
115
 
91
116
  `client.job(jobId)` returns a [job helper](#jobs) and makes no request until a step runs.
92
117
 
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.
118
+ These functions make no request: `captureEnvironment`, `formatProviderPayload`, `signRequest`, `signRequestV2`, `requestKeyRegistrationBody`, `requestKeyChangeApproval`, `requestKeyChangePreimage`, `policyActivationApproval`, `policyActivationPreimage`, `requestSignaturePreimage`, `requestSignaturePreimageV2`, `generateRequestKeyPair`, `validateHex64`, `validateJobName`, `validateRouteId`, `dseDispatchHash`, `dcaeClosureHash` and `cresReceiptHash`. `RUN_STATUSES` lists every run status the server uses, and `EXECUTE_FIELDS` lists every top-level key a run request may hold.
94
119
 
95
120
  `getRunArtifacts`, `getPublicBundle` and `getPublicBundleByToken` return the exact JSON text the server sent, so you can save it and check it with `verdifax-verify`.
96
121
 
97
122
  ## Versioning
98
123
 
99
- From 1.0, a breaking change needs a new major version.
124
+ From 1.0, a breaking change needs a new major version. See [CHANGELOG.md](CHANGELOG.md) for what each release added.
100
125
 
101
126
  ## Settings
102
127
 
@@ -168,6 +193,106 @@ The server refuses a step in a closed job, a repeated step id, and a final step
168
193
 
169
194
  A job map covers what was sent. It cannot show a step that was never sent.
170
195
 
196
+ ## Policies
197
+
198
+ A policy is your own set of rules that decides whether a run is allowed or refused, from the facts the run declares. You upload its exact text, test it on sample inputs, then activate it. Each change is one more link in a chain of changes on the public log.
199
+
200
+ ```typescript
201
+ import { readFileSync, writeFileSync } from "node:fs";
202
+
203
+ // Upload the exact text. Nothing changes until it is activated.
204
+ const stored = await client.uploadPolicy(readFileSync("policy.json", "utf8"));
205
+
206
+ // See which rule would fire on sample inputs. This seals nothing.
207
+ const trial = await client.testPolicy({
208
+ documentHash: stored.document_hash,
209
+ inputs: { subject_type: "agent", verb: "approve-loan", object_type: "transaction", classification: "CONFIDENTIAL" },
210
+ });
211
+ console.log(trial.outcome, trial.fired_rule_id, trial.reason_code);
212
+
213
+ // Make it the policy in force. With a signingKey the request key signs the change.
214
+ const change = await client.activatePolicy(stored.document_hash);
215
+
216
+ // A run declares the facts the rules read, and can name the policy it expects.
217
+ await client.attest({
218
+ program_id,
219
+ route_id,
220
+ registry_record_hash,
221
+ payload_text: "approve loan 1042",
222
+ attested_context: { actor_id: "agent:loan-bot" },
223
+ policy_context: {
224
+ actor_type: "agent",
225
+ mfa_satisfied: true,
226
+ object_type: "transaction",
227
+ object_id: "loan-1042",
228
+ classification: "CONFIDENTIAL",
229
+ },
230
+ expected_policy_hash: change.policy_hash,
231
+ });
232
+
233
+ // Roll back by activating an earlier document.
234
+ await client.activatePolicy(earlierDocumentHash);
235
+
236
+ // Save the chain of changes and check it with verdifax-verify --policy-history policy-history.json.
237
+ writeFileSync("policy-history.json", JSON.stringify(await client.getPolicyHistory()));
238
+ ```
239
+
240
+ `uploadPolicy` sends the string it is given unchanged, because the server hashes the exact text, and it refuses anything that is not a string. `getPolicy` returns a stored document with its exact text. `testPolicy` takes exactly one of `document` and `documentHash`. `listPolicies` returns the documents, `chain_id`, the newest change as `newest_activation_hash` (64 zeros when there is none yet) and the document in force as `active_document_hash` (empty under the built-in policy).
241
+
242
+ `activatePolicy` reads `listPolicies` first and names the newest change as the one it follows. With the `signingKey` option it sends the request key's signature over the chain, the document and that change, which the server requires while a request key is registered. Rolling back is activating an earlier document: it is one more change, so the change it undoes stays on the record. An HTTP 409 means another change came first or the document is not yet effective. It throws the usual `APIError` and is not retried. `getPolicyHistory` returns the changes oldest first, in the form `verdifax-verify --policy-history FILE` reads.
243
+
244
+ `execute`, `attest`, each row of `executeBatch`, `verifyDeterminism`, `job.step` and the provider helpers take `policy_context` and `expected_policy_hash`. `policy_context` holds only `actor_type` (`agent`, `user`, `service` or `system`), `mfa_satisfied` (true or false), `session_age_seconds` (a whole number, not negative), `object_type` (`text`, `transaction`, `code`, `data`, `model` or `file`), `object_id` (at most 256 bytes) and `classification` (`PUBLIC`, `INTERNAL`, `CONFIDENTIAL`, `RESTRICTED`, `TOP_SECRET` or `TS_SCI`). Any other key or value is refused before sending, because a misspelt key would be dropped and a rule on it would read its zero value. `actor_type`, `mfa_satisfied` and `session_age_seconds` describe the attested actor, so a run that sets one of them must also send `attested_context` with the actor, such as its `actor_id`. `expected_policy_hash` is 64 lowercase hex characters: when another policy is active the server refuses the run with HTTP 409. Both fields are inside `signed_request` on a signed client.
245
+
246
+ Verdifax did not write, review or approve your policy. A pass means the rules ran, not that they are good rules.
247
+
248
+ A policy judges what you declared. It cannot show the declared facts are true.
249
+
250
+ A refusal stops the action only if your system waits for the answer.
251
+
252
+ ## Accounts
253
+
254
+ An account is a company on Verdifax. Every API key belongs to one account for life. An account has an owner key, which manages the account: it creates and revokes the member keys, sets the account policy and reads the account's runs. The member keys are the keys that run things.
255
+
256
+ ```typescript
257
+ import { readFileSync, writeFileSync } from "node:fs";
258
+ import { VerdifaxClient, generateRequestKeyPair } from "verdifax";
259
+
260
+ // Verdifax creates the account and its first owner key.
261
+ const admin = new VerdifaxClient({ apiKey: process.env.VERDIFAX_ADMIN_KEY! });
262
+ const { account, owner_key } = await admin.adminCreateAccount("Acme", { tier: "production" });
263
+ console.log(account.account_id, owner_key.secret); // the secret is shown once
264
+
265
+ // The owner registers a request key, which must sign every account policy change.
266
+ const { privateKey } = generateRequestKeyPair();
267
+ const owner = new VerdifaxClient({ apiKey: owner_key.secret, signingKey: privateKey });
268
+ await owner.registerAccountRequestKey();
269
+
270
+ // The owner creates member keys. Each one stays in the account for life.
271
+ const member = await owner.createAccountKey("loan-bot");
272
+ console.log(member.id, member.secret); // the secret is shown once
273
+
274
+ // The owner loads, tests and activates the account policy, signed by its request key.
275
+ const stored = await owner.uploadAccountPolicy(readFileSync("account-policy.json", "utf8"));
276
+ await owner.testAccountPolicy({ documentHash: stored.document_hash, inputs: { verb: "approve-loan" } });
277
+ await owner.activateAccountPolicy(stored.document_hash);
278
+
279
+ // The owner exports the runs of every key in the account, with the account policy and its changes.
280
+ const evidence = await owner.exportAccountRuns({ from: "2026-11-01T00:00:00Z", to: "2026-12-01T00:00:00Z" });
281
+ writeFileSync("account-runs.json", JSON.stringify(evidence));
282
+ ```
283
+
284
+ An owner key cannot run anything. Every route that needs a key refuses it, apart from the `/account` routes, so give the member keys to the systems that run things.
285
+
286
+ The account number, such as `account-7`, is a label Verdifax assigned. It is not a verified company identity.
287
+
288
+ An account policy is the customer's own. Verdifax did not write, review or approve it. Every key in the account must pass the account policy on top of its own policy, and a run is admitted only when both allow it.
289
+
290
+ The account's view covers the runs its keys made through Verdifax. It does not cover runs made elsewhere.
291
+
292
+ `activateAccountPolicy` reads `listAccountPolicies` first, names the newest change as the one it follows and signs the change over the account's chain, `account-N`, with the same signature `activatePolicy` uses. The server refuses an unsigned account policy change, so `activateAccountPolicy` needs the `signingKey` option, set to the owner key's registered request key, and refuses before sending anything without it. It also refuses a `chain_id` that is not of the form `account-N`. The other account policy methods take and return what the matching key policy methods do. `getAccountPolicyHistory` returns the account's changes oldest first, in the form `verdifax-verify --policy-history FILE` reads.
293
+
294
+ `exportAccountRuns` and `exportAccountRunsCSV` take the same period and `job_id` as `exportRuns` and check the CSV counts the same way. The JSON form also carries `account_id`, and `account_policy_documents` and `account_policy_history` once the account has loaded a policy. `listAccountRuns` pages like `listRuns`. `revokeAccountKey` revokes a member key, and a key in another account, or an owner key, throws `NotFoundError`. `adminCreateKey(name, { accountId })` puts a new customer key in an existing account, by its number.
295
+
171
296
  ## Evidence export
172
297
 
173
298
  `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.
@@ -211,6 +336,8 @@ Both helpers take the same optional `job` block as `attest` and return `record_h
211
336
 
212
337
  `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
338
 
339
+ The server ignores a key it does not know, so a misspelt field would be dropped with no error. The client refuses it instead, before sending: a run request for `execute`, `attest`, `verifyDeterminism`, `job.step` or a row of `executeBatch` with a top-level key outside `EXECUTE_FIELDS`, or an `attested_context` key outside `attested`, `actor_id`, `actor_role`, `authorization_policy`, `actor_signature`, `model_provider`, `model_name`, `model_version`, `model_temperature`, `prompt_hash`, `decision_kind`, `decision_result` and `decision_note`, throws a `VerdifaxError` naming the key. `attest` also refuses `request_signature` and `signed_request`, which it does not send. `attestClaudeResponse` and `attestOpenAIResponse` accept their own argument names and every name in `EXECUTE_FIELDS`, so a run request spread into the arguments keeps working, and refuse any other key the same way. Responses are not checked this way: a field a newer server adds is kept and does not throw.
340
+
214
341
  `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
342
 
216
343
  ## Environment capture
@@ -260,7 +387,7 @@ try {
260
387
 
261
388
  ## Request signing
262
389
 
263
- Request signing is optional. A client with no signing key sends the same requests as before.
390
+ A trial key, which is what a pilot receives, needs a request key: it seals nothing until one is registered, and the key can then be replaced but not cleared. On a production key, request signing is optional, and a client with no signing key sends the same requests as before.
264
391
 
265
392
  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.
266
393