verdifax 1.3.0 → 1.5.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 +133 -1
- package/dist/index.d.mts +479 -1
- package/dist/index.d.ts +479 -1
- package/dist/index.js +741 -10
- package/dist/index.mjs +737 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,6 +105,22 @@ if (v.verified) {
|
|
|
105
105
|
| `client.testAccountPolicy({ inputs, document?, documentHash? })` | `POST /account/policies/test` | owner key |
|
|
106
106
|
| `client.getAccountPolicyHistory()` | `GET /account/policies/history` | owner key |
|
|
107
107
|
| `client.activateAccountPolicy(documentHash)` | `POST /account/policies/{document_hash}/activate` | owner key |
|
|
108
|
+
| `client.registerApprover(name, approver)` | `POST /account/approvers` | owner key |
|
|
109
|
+
| `client.listApprovers()` | `GET /account/approvers` | owner key |
|
|
110
|
+
| `client.revokeApprover(id)` | `DELETE /account/approvers/{id}` | owner key |
|
|
111
|
+
| `client.getSignOffHistory()` | `GET /account/signoffs/history` | owner key |
|
|
112
|
+
| `client.getNewestSignOff({ asOwner? })` | `GET /signoffs/newest`, or `GET /account/signoffs/newest` with `asOwner` | required, or owner key |
|
|
113
|
+
| `client.signOff(target, decision, approverKey, { note?, asOwner? })` | `POST /signoffs`, or `POST /account/signoffs` with `asOwner` | required, or owner key |
|
|
114
|
+
| `client.getSignOffs(runId, { asOwner? })` | `GET /runs/{id}/signoffs`, or `GET /account/runs/{id}/signoffs` with `asOwner` | required, or owner key |
|
|
115
|
+
| `client.getSignals({ after?, limit?, wait?, asOwner? })` | `GET /signals`, or `GET /account/signals` with `asOwner` | feed key, or owner key |
|
|
116
|
+
| `client.followSignals({ after?, wait?, asOwner? })` | `GET /signals` page after page, or `GET /account/signals` with `asOwner` | feed key, or owner key |
|
|
117
|
+
| `client.createFeedKey(name, { expiresAt? })` | `POST /account/feed-keys` | owner key |
|
|
118
|
+
| `client.revokeFeedKey(id)` | `DELETE /account/feed-keys/{id}` | owner key |
|
|
119
|
+
| `client.createWebhook(url, { types? })` | `POST /account/webhooks` | owner key |
|
|
120
|
+
| `client.listWebhooks()` | `GET /account/webhooks` | owner key |
|
|
121
|
+
| `client.deleteWebhook(id)` | `DELETE /account/webhooks/{id}` | owner key |
|
|
122
|
+
| `client.listWebhookDeliveries(id, { limit? })` | `GET /account/webhooks/{id}/deliveries` | owner key |
|
|
123
|
+
| `client.getFailureReceipt(id)` | `GET /runs/{id}/failure-receipt` | required |
|
|
108
124
|
| `client.adminCreateKey(name, { accountId? })` | `POST /admin/keys` | admin |
|
|
109
125
|
| `client.adminListKeys()` | `GET /admin/keys` | admin |
|
|
110
126
|
| `client.adminRevokeKey(id)` | `DELETE /admin/keys/{id}` | admin |
|
|
@@ -115,7 +131,7 @@ if (v.verified) {
|
|
|
115
131
|
|
|
116
132
|
`client.job(jobId)` returns a [job helper](#jobs) and makes no request until a step runs.
|
|
117
133
|
|
|
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.
|
|
134
|
+
These functions make no request: `captureEnvironment`, `formatProviderPayload`, `signRequest`, `signRequestV2`, `requestKeyRegistrationBody`, `requestKeyChangeApproval`, `requestKeyChangePreimage`, `policyActivationApproval`, `policyActivationPreimage`, `signOffPreimage`, `approverKeyProof`, `verifyWebhook`, `signalEventHash`, `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.
|
|
119
135
|
|
|
120
136
|
`getRunArtifacts`, `getPublicBundle` and `getPublicBundleByToken` return the exact JSON text the server sent, so you can save it and check it with `verdifax-verify`.
|
|
121
137
|
|
|
@@ -293,6 +309,122 @@ The account's view covers the runs its keys made through Verdifax. It does not c
|
|
|
293
309
|
|
|
294
310
|
`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
311
|
|
|
312
|
+
## Sign-off
|
|
313
|
+
|
|
314
|
+
A sign-off is an approver's decision, approve or reject, on one exact result: a completed run's record, a refused or halted run's receipt, or a whole job by its final step. The approver signs it on their own machine with an approver key, and Verdifax links it to the account's previous sign-off and logs it on the public log.
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
import { writeFileSync } from "node:fs";
|
|
318
|
+
import { VerdifaxClient, approverKeyProof, generateRequestKeyPair } from "verdifax";
|
|
319
|
+
|
|
320
|
+
// The approver creates a key on their own machine and keeps the private key.
|
|
321
|
+
const approverKey = generateRequestKeyPair().privateKey;
|
|
322
|
+
const proof = approverKeyProof(approverKey); // { public_key, proof }, safe to hand to the owner
|
|
323
|
+
|
|
324
|
+
// The account's owner registers the approver key.
|
|
325
|
+
const owner = new VerdifaxClient({ apiKey: process.env.VERDIFAX_OWNER_KEY! });
|
|
326
|
+
await owner.registerApprover("credit review", proof);
|
|
327
|
+
|
|
328
|
+
// Any key in the account submits the approver's signed decision on one exact result.
|
|
329
|
+
const member = new VerdifaxClient({ apiKey: process.env.VERDIFAX_API_KEY! });
|
|
330
|
+
const bundle = JSON.parse(await member.getRunArtifacts(1042));
|
|
331
|
+
const signOff = await member.signOff(
|
|
332
|
+
{ kind: "record", run_id: 1042, digest: bundle.bundle_anchor.anchored_digest },
|
|
333
|
+
"approve",
|
|
334
|
+
approverKey,
|
|
335
|
+
{ note: "Checked against the loan file." }, // only its SHA-256 is sent
|
|
336
|
+
);
|
|
337
|
+
console.log(signOff.sequence, signOff.signoff_hash);
|
|
338
|
+
|
|
339
|
+
// Save the run's sign-offs and check them with verdifax-verify --signoff signoffs.json.
|
|
340
|
+
writeFileSync("signoffs.json", JSON.stringify(await member.getSignOffs(1042)));
|
|
341
|
+
|
|
342
|
+
// The owner saves the account's whole chain for verdifax-verify --signoff-history.
|
|
343
|
+
writeFileSync("signoff-history.json", JSON.stringify(await owner.getSignOffHistory()));
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
An approver key is registered by the account's owner, and it is never a request key in the same account. The server refuses to register a key that is, so the key that requested a run cannot sign it off. `registerApprover` takes the approver's signing key, from which it makes the proof on this machine, or the `{ public_key, proof }` that `approverKeyProof` made on the approver's own machine, so the owner need not hold the approver's private key. It refuses a proof that does not verify before sending. `revokeApprover` revokes a key, and its earlier sign-offs stay on the record. `listApprovers` lists revoked keys too.
|
|
347
|
+
|
|
348
|
+
`signOff` reads `getNewestSignOff` first for the account's chain, `account-N`, and its newest sign-off, then signs over them, the target, the decision, the note's hash, the time and a fresh nonce. The approver's private key is never sent. The target's `digest` is what the result's public-log entry covers: for a record, the `bundle_anchor.anchored_digest` of the bundle `getRunArtifacts` returns; for a receipt, its hash; for a job, the final step's record digest, read the same way as a record's, with `job_id`. The note is hashed on your machine and only its SHA-256 is sent and signed: the note's text never reaches Verdifax, so keep it yourself. `signOff` refuses a bad target, decision, key or note before sending anything. It also checks that the record the server returns carries the signature, `approver_key`, `decision`, `previous_signoff_hash` and `target` that were sent, and throws `VerdifaxError` if not. An HTTP 409 means another sign-off came first or the target is not this account's result: it throws the usual `APIError` and is not retried.
|
|
349
|
+
|
|
350
|
+
With `asOwner: true`, `signOff`, `getNewestSignOff` and `getSignOffs` use the owner key's `/account` routes. `getSignOffs` returns a run's sign-offs oldest first, in the form `verdifax-verify --signoff FILE` reads: for a member key, a run it made; with `asOwner`, any run of the account. `getSignOffHistory` returns the account's whole chain, oldest first, in the form `verdifax-verify --signoff-history FILE` reads, and `exportAccountRuns` carries the same chain as `signoffs`. The SDK returns `signoffs` as the server sent it, without checking it: check it with `verdifax-verify --signoff-history`.
|
|
351
|
+
|
|
352
|
+
A sign-off shows that the holder of a registered approver key signed that decision on that exact result, and that Verdifax logged it on the public log. It does not show that the review was careful, that the result is right, or who the person is. A key is not an identity.
|
|
353
|
+
|
|
354
|
+
A sign-off gates nothing on its own.
|
|
355
|
+
|
|
356
|
+
A later sign-off on the same result supersedes the earlier one, and nothing is deleted.
|
|
357
|
+
|
|
358
|
+
Verdifax did not review the result.
|
|
359
|
+
|
|
360
|
+
Since v0.22.0, the verifier reads sign-offs with `--signoff` and `--signoff-history`.
|
|
361
|
+
|
|
362
|
+
## Signal feed
|
|
363
|
+
|
|
364
|
+
The signal feed is every control signal Verdifax recorded for the account, in order: refused, halted and failed runs, policy changes, sign-offs, approver keys registered and revoked, and keys created and revoked. Each event is a CloudEvent that carries its sealed entry, its place in the feed and the hash of the event before it. The feed cannot show a signal Verdifax never recorded.
|
|
365
|
+
|
|
366
|
+
A feed key reads the feed and nothing else. The account's owner creates one with `createFeedKey` and revokes it with `revokeFeedKey`, and the owner key reads the same feed with `asOwner: true`.
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
import { writeFileSync } from "node:fs";
|
|
370
|
+
import { SignalFeedError, VerdifaxClient } from "verdifax";
|
|
371
|
+
|
|
372
|
+
const owner = new VerdifaxClient({ apiKey: process.env.VERDIFAX_OWNER_KEY! });
|
|
373
|
+
const { secret } = await owner.createFeedKey("grc reader"); // shown this once
|
|
374
|
+
|
|
375
|
+
const feed = new VerdifaxClient({ apiKey: secret });
|
|
376
|
+
let after = 0; // the last sequence your system stored
|
|
377
|
+
try {
|
|
378
|
+
for await (const event of feed.followSignals({ after })) {
|
|
379
|
+
console.log(event.data.sequence, event.type, event.data.reason ?? "");
|
|
380
|
+
after = event.data.sequence;
|
|
381
|
+
}
|
|
382
|
+
} catch (err) {
|
|
383
|
+
if (!(err instanceof SignalFeedError)) throw err;
|
|
384
|
+
after = err.lastSequence; // follow again from here and nothing is lost
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Save a page and check it with verdifax-verify --signals signals.json.
|
|
388
|
+
writeFileSync("signals.json", JSON.stringify(await feed.getSignals({ limit: 500 })));
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
`followSignals` reads page after page and starts each read at the previous page's `next_after`, so it never skips an event, and it refuses a page that would skip or go back. When there is no new event, each read waits up to `wait` seconds (default 25) on the server. A failed read is retried like any GET. If it still fails, `followSignals` throws `SignalFeedError`, whose `lastSequence` is the last sequence it handed out and whose `cause` is the error the read threw. It runs until you stop it with `break` or `return`. `getSignals` reads one page: `after` is the last sequence read (default 0), `limit` is 1 to 500 and `wait` is 0 to 25 seconds. The timeout of a signals call is at least its wait plus 30 seconds, whatever the client's `timeoutMs`. Bad options are refused before anything is sent.
|
|
392
|
+
|
|
393
|
+
The owner key subscribes an https address with `createWebhook(url, { types })`, at most ten per account. `types` are the kinds to deliver, each once; empty or left out takes every kind. The secret, `whsec_` and base64, is in the result this once. Each delivery is signed under the Standard Webhooks scheme. Check it with `verifyWebhook` before you use it, passing the body exactly as received:
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
import http from "node:http";
|
|
397
|
+
import { verifyWebhook } from "verdifax";
|
|
398
|
+
|
|
399
|
+
http
|
|
400
|
+
.createServer((req, res) => {
|
|
401
|
+
const chunks: Buffer[] = [];
|
|
402
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
403
|
+
req.on("end", () => {
|
|
404
|
+
try {
|
|
405
|
+
const event = verifyWebhook(req.headers, Buffer.concat(chunks), process.env.VERDIFAX_WEBHOOK_SECRET!);
|
|
406
|
+
console.log(event.data.sequence, event.type);
|
|
407
|
+
res.statusCode = 204;
|
|
408
|
+
} catch (err) {
|
|
409
|
+
console.error((err as Error).message); // names the check that failed, never the secret
|
|
410
|
+
res.statusCode = 400;
|
|
411
|
+
}
|
|
412
|
+
res.end();
|
|
413
|
+
});
|
|
414
|
+
})
|
|
415
|
+
.listen(8080);
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`verifyWebhook` reads the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers without regard to case. It refuses a timestamp more than `toleranceSeconds` (default 300) from `now` in either direction, accepts the delivery when any one `v1` signature matches, compares in constant time and returns the parsed CloudEvent. It throws `VerdifaxError` naming the check that failed, and never puts the secret in a message.
|
|
419
|
+
|
|
420
|
+
A webhook delivery is a copy: the pull feed and the public log are the record. A delivery can fail or arrive more than once, so store `data.sequence` and read the feed from the last one you stored. `listWebhookDeliveries(id, { limit })` shows each delivery's status and attempts, newest first. `deleteWebhook` ends a subscription, and its events stay in the feed.
|
|
421
|
+
|
|
422
|
+
`getFailureReceipt(runId)` returns a failed run's sealed failure receipt, in the form `verdifax-verify` reads. A run that failed before failure receipts were sealed has none, and the call throws `NotFoundError`.
|
|
423
|
+
|
|
424
|
+
`signalEventHash(kind, entryHash, { account, sequence, previousSignalHash })` recomputes an event's `event_hash`, the hash the next event names. It makes no request.
|
|
425
|
+
|
|
426
|
+
`verdifax-verify --signals FILE` checks a saved feed: each entry as its own kind, that the place each entry seals is the event's place, and that the numbers and links run without a gap.
|
|
427
|
+
|
|
296
428
|
## Evidence export
|
|
297
429
|
|
|
298
430
|
`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.
|