provared 0.2.0 → 0.4.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/docs/guide.md ADDED
@@ -0,0 +1,745 @@
1
+ # The Provared guide
2
+
3
+ Everything about the format, the library, the checker, the stub writer beside an agent and what a record does and does not prove, in one document. The [README](../README.md) is the short version: install, a first record, and where things are.
4
+
5
+ *Evidence, not a verdict.* A record shows what was signed and by which keys. People decide what it means.
6
+
7
+ ## Status
8
+
9
+ **Version 0.4.0, a draft.** Published as it is, by one author in their
10
+ own time; fixes as time allows. The format may still change, so do not
11
+ rely on a draft record staying checkable.
12
+
13
+ The core of the format is also written as an individual Internet-Draft
14
+ at the IETF:
15
+ [draft-izmaylov-agent-permission-receipts](https://datatracker.ietf.org/doc/draft-izmaylov-agent-permission-receipts/).
16
+ An Internet-Draft is a working document, not a standard.
17
+
18
+ ## The four parts
19
+
20
+ | Part | What it is | In this draft |
21
+ |---|---|---|
22
+ | **Slip** | A short permission a person signs with a passkey: which agent, which actions, what limits and conditions, with whom, from when until when, and why | Built |
23
+ | **Stub** | A signed receipt for each action, pointing back to its slip, countersigned by the other side where possible, and approved by the person where the slip asks for it | Built |
24
+ | **Seal** | The stubs chained together and gathered under one top fingerprint, signed by whoever keeps the book and time-stamped by an outside service | Built. A public place where seals are kept is not |
25
+ | **Show** | One page shown to a checker with proof that it is in the book, and when the book was sealed, with the names it does not need kept covered | Built. Only a slip's names and purpose can be covered so far |
26
+
27
+ ## What is here
28
+
29
+ | Folder | What it holds |
30
+ |---|---|
31
+ | `spec/provared-format.md` | The format, described exactly |
32
+ | `docs/threat-model.md` | Who might attack a record, what each part protects, and what it does not |
33
+ | `src/` | The library: the checker, the writers for every kind of record, the stub writer beside an agent and the connector for an agent's tools. No outside code |
34
+ | `bin/provared-check.mjs` | The checker for the command line |
35
+ | `bin/provared-headers.mjs`, `net/` | A command that fetches the chain of Bitcoin block headers from nodes you name, so that a block time-stamp can be checked with no node of your own. The only code here that makes a network request. Experimental |
36
+ | `page/` | The checking page and the demonstration page |
37
+ | `demo/` | The demonstration: an invented office agent works under a permission and goes outside it four times |
38
+ | `samples/` | A sample record, written once, that the tests keep checking |
39
+ | `tools/` | Three small tools: one wrote the sample record, one writes the checking page as a single file, one writes the shared test files |
40
+ | `test/` | The tests |
41
+ | `test-vectors/` | Shared test files: records and values, each with the answer this library gives. Another implementation of the format can be tested against them |
42
+ | `python/` | The Python version, `pip install provared`, which gives the same answers. See its own [README](../python/README.md), and its examples with three agent frameworks in `python/examples/`, and small packages for two of them in `python/integrations/` |
43
+ | `site/` | The pages at [prova.red](https://prova.red/): what a record proves, with the checker's answer for the sample record, and a worked example. Published with GitHub Pages by `.github/workflows/pages.yml`, which also serves the checking page at `prova.red/check/` and as one file. The pages load nothing from anywhere and keep nothing about a reader |
44
+
45
+ The npm package holds `src/`, `bin/`, `net/`, `spec/`, `docs/`,
46
+ `samples/` and the checking page. The tests, the shared test files, the
47
+ demonstration, the tools and the Python version are in the repository
48
+ only.
49
+
50
+ ## Install
51
+
52
+ ```
53
+ npm install provared
54
+ ```
55
+
56
+ You need Node.js 24.7 or later. There are no dependencies. Node.js marks
57
+ its ML-DSA and SLH-DSA as experimental and may print a warning about
58
+ them; the command-line checker does not repeat it.
59
+
60
+ ```js
61
+ import { checkBook, openRecorder } from 'provared'; // the whole library
62
+ import { checkBook } from 'provared/check'; // the checker alone, as a page uses it
63
+ import { createPasskey } from 'provared/passkey-browser'; // a passkey, in a browser
64
+ import { developmentRecorder } from 'provared/dev'; // a stand-in for a passkey, for development only
65
+ ```
66
+
67
+ The checker for the command line is `provared-check`:
68
+
69
+ ```
70
+ npx provared-check record.jsonl --issuer <the thumbprint of the passkey you trust>
71
+ npx provared-check --sample # the sample record, naming the stand-ins it was made with as trusted
72
+ ```
73
+
74
+ The examples below import from the package by name. In a copy of the
75
+ repository, import from `./src/index.js`, `./src/check.js`,
76
+ `./src/passkey-browser.js` and `./src/dev.js` instead.
77
+
78
+ ## A stand-in for a passkey, for development
79
+
80
+ A slip is signed by a person with a passkey, in a browser. To try the
81
+ library without one, `provared/dev` gives a **development stand-in**: an
82
+ ordinary key made in this process's memory that returns the same three
83
+ values a passkey returns, so that every record it signs checks exactly
84
+ as a real one would. It is not a passkey, and no person confirmed
85
+ anything. So that such a record is never mistaken for a person's, the
86
+ issuer's name in every slip it signs ends with " (development)",
87
+ whatever name you give, and its page address is `http://localhost`,
88
+ which the format allows for development only. The checker prints that
89
+ name beside the slip.
90
+
91
+ ```js
92
+ import { developmentPasskey, developmentRecorder, developmentSlip } from 'provared/dev';
93
+
94
+ const { recorder, issuerKeys } = await developmentRecorder({ actions: ['provared.message.send'] });
95
+ // the stub writer, opened under a slip signed by the stand-in; issuerKeys is its thumbprint, for a checker
96
+
97
+ const made = await developmentSlip({ actions: ['supplies.order'], limits: [{ action: 'supplies.order', max: 200, unit: 'GBP' }] });
98
+ // made.slip, made.slipFingerprint, made.book (the slip alone), made.issuerKeys, made.agent (keys and privateKeys), made.passkey
99
+
100
+ const passkey = await developmentPasskey();
101
+ // passkey.key, and passkey.sign(challenge) for a prepared slip, approval or cancellation
102
+ ```
103
+
104
+ `developmentSlip` takes the members of a slip as `prepareSlip` does, and
105
+ fills in the issuer (its key, page address and marked name) and the
106
+ agent's keys; by default the slip runs from a minute ago for one day,
107
+ holds no limits and names no other party, and its purpose says that it
108
+ is for development. [examples/first-record.mjs](../examples/first-record.mjs)
109
+ is a whole first record in one file. In Python the same is
110
+ `provared.dev` (`development_passkey`, `development_slip`,
111
+ `development_recorder`), with the writer as `.writer`.
112
+
113
+ ## Try it
114
+
115
+ These need a copy of the repository. You need Node.js 24.7 or later.
116
+ Nothing is installed: there are no dependencies.
117
+
118
+ **Run the tests.**
119
+
120
+ ```
121
+ npm test
122
+ ```
123
+
124
+ **Check the sample record from the command line.**
125
+
126
+ ```
127
+ npm run check -- samples/office-supplies.jsonl
128
+ ```
129
+
130
+ It prints each entry, then its answers: is the record intact, was
131
+ everything checked on this device, did the agent stay within its slip, is
132
+ it time-stamped in a way you trust, and was it checked against keys you
133
+ named. Here you named none, so it is not a pass, whatever else it shows;
134
+ the fingerprints that come with the sample are given below.
135
+ In the sample the agent goes outside its slip four times: an order the
136
+ supplier never confirmed, an order above 60 pounds that the person was
137
+ not asked to approve, a total of 230 against a limit of 200, and a
138
+ deletion that the slip forbids. The checker says where.
139
+
140
+ **Make the checking page as one file.**
141
+
142
+ ```
143
+ node tools/make-single-page.mjs
144
+ ```
145
+
146
+ It writes `provared-check.html`: the checking page, its style, the
147
+ checker and the sample record in one file, copied in unchanged. Open it
148
+ in a browser straight from the disk; it needs no server. The file allows
149
+ itself no request of any kind: its content security policy lets nothing
150
+ be loaded from anywhere. The tool prints the file's SHA-256 fingerprint,
151
+ so that a copy can be compared with it. (Tried on 8 October 2026 in the
152
+ three browser builds this repository is tested with, and on 13 October
153
+ 2026 in Chrome 155, Edge 155 and Brave 155.) The file for
154
+ version 0.4.0 is attached to the release on GitHub. Its SHA-256
155
+ fingerprint is
156
+ `49da6617ff493ddda2b2b65bfb6bedebdf8cf07ea6faa32b039f065eda207307`.
157
+
158
+ **Run the demonstration in a browser.**
159
+
160
+ ```
161
+ npm run demo
162
+ ```
163
+
164
+ Then open `http://localhost:8787`. You sign a permission with a passkey,
165
+ the agent works and stops once to ask your approval of one order, and the
166
+ page shows where the agent went outside the permission. Everything in it
167
+ is invented: nothing is bought, sent or deleted. The checking
168
+ page on its own is at `http://localhost:8787/check`. It takes a record
169
+ file, the fingerprints you already trust (keys, time-stamp services,
170
+ blocks), and what you were handed with the record: the person's own
171
+ copies of cancellations, and disclosures for covered fields. All of it is
172
+ read in the browser and sent nowhere.
173
+
174
+ ## What a slip can say
175
+
176
+ - **What the agent may do**: a list of action names. Use names of your
177
+ own, or names from the shared list in the format description, section
178
+ 16, such as `provared.order.place`, so that another organisation's
179
+ checker knows what is meant.
180
+ - **Limits**: a total, a limit on each single action, a number of
181
+ actions, and a total or a number within any period.
182
+ - **Conditions**: that the other side must countersign an action, and
183
+ that the person must approve an action with their passkey, always or
184
+ above an amount.
185
+ - **Stated rules**: kinds of action the agent must never do, such as
186
+ `destroys`, and rules of conduct, such as "never claim to be a human
187
+ being". A checker compares the first with the stubs. The second is the
188
+ person's signed instruction: no check can show that the agent kept it.
189
+ - **With whom**: the services the agent may deal with, and their keys.
190
+
191
+ The other side has a voice of its own. A service can sign a **refusal**
192
+ ("this agent asked for something outside its slip, and we refused") and
193
+ its **terms for agents** (which actions it accepts). Both are shown as
194
+ what the service said.
195
+
196
+ ## The seal and the outside time-stamp
197
+
198
+ Whoever keeps the book seals it from time to time: they sign its top
199
+ fingerprint and its number of entries, with three signatures (Ed25519,
200
+ ML-DSA-87 and SLH-DSA-SHA2-256s). An outside time-stamp service then
201
+ states when that seal existed, in the standard form of RFC 3161.
202
+
203
+ - After a seal by a recorder you named (`--sealer`, or `sealKeys`),
204
+ nothing before it can be changed, added or removed without the seal no
205
+ longer fitting. A seal by keys you did not name shows only that someone
206
+ sealed the book as it now stands.
207
+ - With a time-stamp from a service you trust, the entries before the seal
208
+ existed by the time the service states. An entry dated later than that
209
+ is reported.
210
+ - **You say whom you trust.** This format names no time-stamp service.
211
+ You give the checker the fingerprint of the certificate of each service
212
+ you trust; a time-stamp from any other service is shown, and not
213
+ counted. In the same way you can give the fingerprint of the key set of
214
+ the recorder you expect.
215
+
216
+ - **A second clock that rests on no signature.** Beside a time-stamp
217
+ from a service, a seal can carry a **block time-stamp**: proof that
218
+ its fingerprint was written into a block of a public blockchain (the
219
+ open proof format OpenTimestamps, and the Bitcoin blockchain). The
220
+ chain is used purely as a clock: no coin, no wallet, no account and no
221
+ payment is involved. It stays good even if every signing method of
222
+ today is broken. You name the blocks you trust by their fingerprints
223
+ (`--block`, or `blocks`). A block's time is right only to within about
224
+ two hours, and the checker says so. The tests use made-up blocks. Two
225
+ complete proofs that others had published, one made by hand and one
226
+ made by public calendar services, were checked once with this reader
227
+ against the real blocks they name (5 October 2026): each led to its
228
+ blocks, and each was refused with another block or with its file
229
+ changed by one bit.
230
+
231
+ ```
232
+ npm run check -- samples/office-supplies.jsonl --stamp-service <fingerprint> --sealer <fingerprint>
233
+ ```
234
+
235
+ The fingerprints for the sample are in
236
+ `samples/office-supplies.expected.json`. The sample's time-stamp service
237
+ is made up, as its passkey is.
238
+
239
+ ### Knowing that a block is part of the chain
240
+
241
+ **Experimental.** `provared-headers` is new in version 0.1.0 and may
242
+ change. It is the only part of Provared that connects to the internet,
243
+ and it does so only when you run it.
244
+
245
+ A block time-stamp counts only for a block you trust to be part of the
246
+ Bitcoin blockchain. You need not run a Bitcoin node of your own, or trust
247
+ anyone who lists blocks, to know that:
248
+
249
+ ```
250
+ provared-headers --node <host:port> --node <host:port>
251
+ provared-check record.jsonl --headers block-headers.bin --issuer <thumbprint> --sealer <fingerprint>
252
+ ```
253
+
254
+ `provared-headers` fetches the chain of block headers (about 78 MB, a
255
+ few minutes the first time) from the Bitcoin nodes you name, and from no
256
+ one else, and keeps it in a file. It checks every header on your device,
257
+ by the rules every Bitcoin node applies, from the chain's first block:
258
+ each names the block before it, carries the work its target asks for,
259
+ follows the rule for the difficulty, and states a time that the rules
260
+ allow. The chain must also hold, at their places, ten blocks known to be
261
+ part of it, which are written into the library, and after the last of
262
+ them no target may be more than four times easier than that block's. A
263
+ second node, on another network, must hand back the latest six blocks
264
+ itself. A node can refuse to answer, but cannot have a false header
265
+ kept. A chain file someone else hands you is only as good as they are:
266
+ fetch your own. Run it again later and it
267
+ fetches only the newer headers. Name your own node if you have one
268
+ (`127.0.0.1:8333`); a file of nodes, one `host:port` to a line, can be
269
+ given with `--nodes-file`. The nodes you name see your device's address,
270
+ and a node named by its name is looked up by your device's own name
271
+ service.
272
+
273
+ `provared-check --headers` checks that file again, in full, on your
274
+ device, and makes no request. A block time-stamp then counts if its block
275
+ is in the chain with at least six blocks after it. The checker says, for
276
+ each block time-stamp, what the chain shows. In the library:
277
+ `checkHeaderChain` and `blocksInChain`.
278
+
279
+ ## Cancelling a slip
280
+
281
+ The person who signed a slip can end it early with the same passkey, and
282
+ have the cancellation time-stamped at once. Every stub after it is then
283
+ reported as outside the slip. With a time-stamp on the cancellation, a
284
+ stub counts as made before it only if an outside time-stamp shows that
285
+ it existed by then: an agent cannot escape a cancellation by writing an
286
+ earlier time into a stub. If the person cancelled more than once, every
287
+ cancellation counts, in whichever order they stand in the book.
288
+
289
+ The person should keep their own copy of a cancellation, with its
290
+ time-stamp. Whoever keeps the book could leave the cancellation out, or
291
+ put a later time-stamp in its place. Handed to the checker beside the
292
+ book (`--cancellation <file>`, or `cancellations`), the person's copy
293
+ counts all the same: the checker says whether the book holds it, and
294
+ reports every stub that no outside time-stamp shows to have existed
295
+ before it.
296
+
297
+ A cancellation shows when the person signed it, not that the agent's
298
+ side was told. So the agent's side can **acknowledge** it: a short
299
+ record, signed with the agent's own keys, that says "this cancellation
300
+ was handed to us at this time". The stub writer signs one whenever the
301
+ person's cancellation of its slip is handed to its `add`, once for each
302
+ cancellation, writes it into the book after the cancellation, and hands
303
+ it back for the person to keep. Handed to the checker with the person's
304
+ copy (as `acknowledgements` in the same file), it shows which stubs the
305
+ agent made after it was told, which passes it handed on after it was
306
+ told, and whether a cancellation that the book leaves out had been
307
+ handed over. Both dates in that comparison are the agent's own word. An
308
+ acknowledgement that does not check is reported, and the cancellation
309
+ itself is still used.
310
+
311
+ ## Who stands behind a name
312
+
313
+ A name in a record is a label. An organisation can stand behind one with
314
+ a **vouching record**: "this key belongs to this name". It can vouch for
315
+ a person's passkey, an agent's keys, a service's keys and a recorder's
316
+ keys, and can later **withdraw** the statement. You tell the checker
317
+ which organisations you trust (`--voucher`, or `vouchers`), and it then
318
+ shows a name as "vouched for by" that organisation instead of "a label,
319
+ not checked". The vouching record must be in force for the whole of the
320
+ slip's time. Provared does not decide who anyone is: it records who
321
+ vouched.
322
+
323
+ A slip can also state the fingerprints of the agent's program and
324
+ settings, as the person approved them. That is a statement, not proof of
325
+ what ran.
326
+
327
+ ## Covered fields
328
+
329
+ A slip can be written with its names and its purpose covered: the person's
330
+ name, the agent's name, the services' names and the purpose. What is
331
+ signed then holds a fingerprint in place of each. To let a reader see a
332
+ field, hand over its **disclosure**; to keep it hidden, do not. The
333
+ signature, the chain and the tree check either way, and a disclosure
334
+ cannot be made for a value that was not signed. A disclosure that does
335
+ not fit is refused and said to be, and the slip is checked as it would
336
+ be without it: what a slip is does not depend on what is handed over
337
+ with it. Nothing that a checker
338
+ compares can be covered: not an action, a limit, a condition, a service
339
+ or a key. So every reader checks the same slip.
340
+
341
+ ```js
342
+ const prepared = await prepareSlip(fields, { cover: ['issuer.name', 'purpose'] });
343
+ prepared.disclosures; // keep these beside the slip
344
+
345
+ await checkBook(book, { disclosures: { [fingerprintOfTheSlip]: theDisclosuresYouWereHanded } });
346
+ await makeShow(book, [0, 3], { disclosures: { 0: theOnesThisReaderMaySee } });
347
+ ```
348
+
349
+ The method is the standard "Selective Disclosure for JSON Web Tokens"
350
+ (RFC 9901), which digital identity wallets also use. The standard's own
351
+ worked example is one of the tests. A slip uses less than the standard
352
+ allows: no covered item of a list, no decoy fingerprints, and
353
+ disclosures written in one canonical form.
354
+
355
+ ## Passing a slip on
356
+
357
+ A slip can say that its agent may hand part of its permission to a helper
358
+ agent, and how many times in a row (`passes`: from 1 to 10). The agent then
359
+ signs a **pass** that names the helper's keys, what the helper may do,
360
+ its limits and its times. A helper's stubs name the pass, are signed with
361
+ the helper's keys, and are compared with the pass, with every pass above
362
+ it, and with the slip the person signed. The totals of the slip, and of
363
+ each pass, take in everything done under them, so passing on cannot
364
+ widen a permission. A pass the slip does
365
+ not allow is reported, with every stub under it.
366
+
367
+ ## The check before acting
368
+
369
+ `checkBefore` answers, before an action: would this be outside the slip?
370
+ It uses the same comparison as the checker, so it refuses everything the
371
+ checker would report afterwards. In one case it refuses more: an action
372
+ that would add to a period which a stub dated after it already
373
+ overfills; the checker reports that against the later stub. The software
374
+ around an agent can call it and stop the action.
375
+
376
+ ```js
377
+ import { checkBefore } from 'provared';
378
+
379
+ const answer = await checkBefore(
380
+ textOfTheBookSoFar,
381
+ {
382
+ slip: fingerprintOfTheSlip,
383
+ action: 'provared.order.place',
384
+ amount: { unit: 'GBP', value: 80 },
385
+ with: 'supplier',
386
+ approval: theSignedApprovalIfTheSlipAsksForOne,
387
+ },
388
+ { issuerKeys: ['the thumbprint of the passkey you trust'] },
389
+ );
390
+
391
+ answer.allowed; // true only if the record so far is sound, fully checked (but for a seal's third signature, below), and the action is inside the slip
392
+ answer.notChecked; // present only where the device went on without checking a seal's third signature (SLH-DSA)
393
+ answer.breaches; // why not, in the checker's own codes
394
+ answer.problems; // why the question could not be answered with a yes
395
+ answer.needs; // what the slip asks to go with it: 'approval', 'countersignature'
396
+ ```
397
+
398
+ It must be told whose passkey it trusts: a slip signed by any other key
399
+ allows nothing. Where the slip asks for the person's approval, the action
400
+ is allowed only if the signed approval is handed over. Where the slip
401
+ asks for a countersignature, `needs` says so: the action is inside the
402
+ slip only if the other side then countersigns it.
403
+
404
+ It is a help, not evidence: nothing in a record shows that it was called.
405
+
406
+ ## Beside an agent
407
+
408
+ `openRecorder` puts the two things an agent's software must do into one
409
+ call: ask first, and write the receipt after.
410
+
411
+ ```js
412
+ import { openRecorder } from 'provared';
413
+
414
+ const recorder = await openRecorder({ book, slip, privateKeys, issuerKeys });
415
+
416
+ const outcome = await recorder.act(
417
+ { action: 'provared.order.place', amount: { unit: 'GBP', value: 45 }, with: 'supplier' },
418
+ async () => placeTheOrder(), // taken only if the answer is yes
419
+ { countersign: askTheSupplierToCountersign },
420
+ );
421
+
422
+ outcome.done; // false: the action was outside the slip, was not taken, and nothing was written
423
+ recorder.book(); // the book, with the new stub in the agent's chain
424
+ await recorder.check(); // what the record shows as it stands, as checkBook would answer
425
+ ```
426
+
427
+ If taking the action fails with an error, no stub is written; the caller
428
+ must deal with whatever part of the action did happen. `record` writes a
429
+ stub without asking, for an action that was taken some other way: the
430
+ record is of what happened, inside the slip or not.
431
+
432
+ The writer's calls wait for one another, so two actions asked for at the
433
+ same moment are taken one after the other, each against the book as the
434
+ one before left it. A countersignature that the other side hands back is
435
+ checked before it goes into the book; one that does not check is left
436
+ out, the stub stands one-sided, and `stub.countersignature.problem` says
437
+ why. `add` puts an entry that someone else made (a seal, a cancellation,
438
+ a refusal) into the book only if the book still passes its check with
439
+ it, and it is not dated ahead of the clock. The other side is given 30
440
+ seconds to countersign (`countersignWithin`); after that the stub is
441
+ written one-sided. Where the slip asks for a countersignature, `act`
442
+ takes the action only if it is given a way to ask for one. Before an
443
+ action is taken, its stub is written and checked with the whole book, so
444
+ that the receipt is known to fit. `act` takes an action now: a date
445
+ handed to it must be within 300 seconds of the writer's clock (`now`).
446
+ Nothing is written that is dated more than 300 seconds ahead of that
447
+ clock. What the writer is handed (the request, an approval, what the
448
+ other side hands back) is copied when it is handed over, and the copy is
449
+ what is checked and written. `record` hands a stub to the other side
450
+ only once it has passed the check of the whole book. The function
451
+ that takes the action must not call the writer. Use one writer for one
452
+ chain: two writers on two copies of a book know nothing of each other.
453
+
454
+ The writer reads its book once, when it is opened, and then carries it
455
+ on: each new line is read by itself, and no signature is checked twice.
456
+ So the time an action takes grows only slowly with the length of the
457
+ book. Measured on one computer on 4 October 2026, with countersigned
458
+ stubs: 9, 14 and 23 thousandths of a second for one action after 400,
459
+ 1,600 and 6,400 stubs. (Before the book was carried on: 2.0 seconds
460
+ after 400 stubs and 7.7 after 1,600.) Opening the writer still reads
461
+ the whole book: about one second for every 450 stubs. Every line
462
+ is tried on a reader split off the book's own before it joins the book,
463
+ so an entry that does not fit, or an error part of the way through a
464
+ call, leaves the writer's view of its book exact. `recorder.check()`
465
+ gives the checker's answer for the book as it stands, without reading
466
+ it again. `before` waits its turn with the other calls. What the writer
467
+ is told to trust (`options`) is read once, when it is opened, and
468
+ fixed.
469
+
470
+ A cancellation that the person signed is never lost in `add`. If it
471
+ cannot be added as it was handed over (a faulty time-stamp beside it, a
472
+ member that cannot be written as a line, such as a list with a gap in
473
+ it), the writer adds the cancellation with its time-stamps, or with
474
+ those of them that pass their check, or by itself. If it cannot be added even so (it is dated ahead
475
+ of the clock), the writer keeps it, allows nothing more under the slip,
476
+ and writes it into the book, at its next call, once the clock has
477
+ reached its date. In both cases `add` fails, and the error says what was
478
+ done. (Where the clock fails, the error is the clock's own; the
479
+ cancellation is held all the same, as `cancellations()` shows.) A
480
+ cancellation that the writer keeps lives only in that writer:
481
+ `recorder.cancellations()` hands back what it holds, each with the
482
+ acknowledgement the writer signed for it, and so does an acknowledgement
483
+ that still waits to follow its cancellation into the book. Keep that
484
+ with the book, and hand it in again (`options.cancellations`, an empty
485
+ list included) when you open a writer again; a writer opened from the
486
+ book alone knows nothing of it. Read `recorder.book()` and
487
+ `recorder.cancellations()` together, with nothing awaited in between,
488
+ so that the two you keep belong to the same moment. A cancellation given in that way is written into the book once
489
+ its date has come, and stops the writer from the start. A copy that
490
+ the writer holds takes the time-stamps handed over with it later: each
491
+ is judged by itself, those the writer counts come first, and a copy
492
+ holds four at most. The writer is stopped before it reads its clock, so
493
+ a clock that fails then does not lose the cancellation, nor the sound
494
+ time-stamps, nor the acknowledgements handed over with it. A clock that
495
+ gives anything but a time counts as a clock that failed. An
496
+ acknowledgement that is handed over with a cancellation is taken as the
497
+ agent's own only if it checks by itself, and only the first such one is
498
+ taken: the writer never puts a second one of the agent's own with a
499
+ copy (a copy given at opening that already holds two is handed back as
500
+ it was given). A copy holds four
501
+ acknowledgements at most; the writer's own is always among them, and
502
+ where the copy already holds four, the last of the others gives way to
503
+ it. A writer that is stopped
504
+ takes no action, but `record` still writes the stub of an action that
505
+ was taken all the same: the record is of what happened, and a checker
506
+ reports such a stub.
507
+
508
+ ### The agent's tools
509
+
510
+ An agent acts by calling tools: functions that its software offers it.
511
+ `recordTools` puts each tool behind the stub writer, so that no call is
512
+ forgotten. The agent's software calls the tools as before.
513
+
514
+ ```js
515
+ import { openRecorder, recordTools } from 'provared';
516
+
517
+ const recorder = await openRecorder({ book, slip, privateKeys, issuerKeys });
518
+ const tools = recordTools(recorder, {
519
+ placeOrder: {
520
+ action: 'provared.order.place', // the name the slip uses for it
521
+ with: 'supplier',
522
+ amount: (order) => ({ unit: 'GBP', value: order.total }),
523
+ run: (order) => sendTheOrder(order), // run only if the slip allows it
524
+ countersign: (stub) => askTheSupplierToCountersign(stub),
525
+ },
526
+ readStock: { action: 'provared.data.read', run: (query) => lookUpStock(query) },
527
+ });
528
+
529
+ await tools.placeOrder({ item: 'paper', boxes: 5, total: 45 });
530
+ ```
531
+
532
+ A call that the slip does not allow throws `NotTaken`, with the answer
533
+ of the check before acting, and the tool is not run. Where the slip asks
534
+ for the person's own approval, a tool's `approve` function is asked for
535
+ it once. By default the stub of a call names one document,
536
+ `arguments`: the fingerprint of the call's arguments, written in one
537
+ form (RFC 8785). So the record fixes exactly what was asked for without
538
+ holding it; `argumentsFingerprint` works the same fingerprint out again.
539
+ The arguments must be JSON: a list with a gap in it, or with a named
540
+ member, or a member that is `undefined`, is refused, and the tool is
541
+ not run. A fingerprint of arguments
542
+ that are few and easy to guess can be guessed. What a tool is, and what
543
+ it is called with, is copied when it is handed over; each function a
544
+ tool is described by gets a copy of the arguments of its own, and is
545
+ called by itself, without `this`: it is not handed the description and
546
+ cannot change it. What `amount`, `with` and `details` give is
547
+ copied too, so a function that keeps what it gave cannot change it
548
+ afterwards. `amount` and `with` must give their value at once,
549
+ not a promise. The numbers `0` and `-0` share a fingerprint, as RFC
550
+ 8785 writes both as `0`. If `onStub` fails, its error is passed on,
551
+ though the tool has run and its stub is in the book. `recordTools`
552
+ names no agent software, and works with any function.
553
+
554
+ ## How signing works
555
+
556
+ - **The person** signs a slip with a passkey. A passkey signs a short
557
+ value handed to it; that value is the SHA-256 fingerprint of the slip's
558
+ exact signed bytes, so the signature covers the whole slip.
559
+ - **The agent and the service** each sign with two keys at once: Ed25519,
560
+ which is today's kind, and ML-DSA-87, which is quantum-safe. A record
561
+ counts only if both signatures check. A forger must break both.
562
+ - **The passkey signature is of today's kind only**, because no device
563
+ offers a quantum-safe passkey yet.
564
+ - **Whoever keeps the book** signs each seal with three keys: the same
565
+ two, and SLH-DSA-SHA2-256s, which rests only on fingerprint functions.
566
+ - **The browsers tried have ML-DSA; none has SLH-DSA.** Tried on the
567
+ author's computer on 13 October 2026: Chrome 155, Edge 155 and
568
+ Brave 155 each offer Ed25519 and ML-DSA (all three strengths) to a
569
+ page, and check real signatures made with them. None of the three
570
+ offers SLH-DSA. So in these browsers the checking page checks every
571
+ signature of a slip, a stub, a countersignature and the other records,
572
+ but not a seal's third signature: a book with a seal reads "not
573
+ complete" there, and the time-stamp beside the seal is not counted.
574
+ Where a browser lacks a method, the checking page says that those
575
+ signatures were not checked, and does not show a pass. Node.js checks
576
+ all of them.
577
+
578
+ ## Using the library
579
+
580
+ ```js
581
+ import { checkBook } from 'provared/check';
582
+
583
+ const result = await checkBook(textOfTheBook, {
584
+ issuerKeys: ['the thumbprint of an issuer key you already trust'],
585
+ });
586
+
587
+ result.summary.intact; // no problem found, and every signature checked
588
+ result.summary.problemFound; // was a problem found in the record?
589
+ result.summary.fullyChecked; // was every signature checked on this device?
590
+ result.summary.withinSlips; // every stub compared with its slip, none outside it
591
+ result.summary.firstBreach; // the first entry that shows the agent outside its slip
592
+ ```
593
+
594
+ Type declarations come with the library (`src/types.d.ts`), so an editor
595
+ can check how it is called.
596
+
597
+ **A book that grows.** `checkBook` reads a whole book each time. To
598
+ follow a book as lines are added to it, open a checker once and carry it
599
+ on. It reads each new line against what it worked out from the lines
600
+ before it, and checks no signature twice. Its answer is the answer
601
+ `checkBook` gives for the same text; a test compares the two at every
602
+ length of a book that holds every kind of entry.
603
+
604
+ ```js
605
+ import { openChecker } from 'provared/check';
606
+
607
+ const checker = await openChecker(textOfTheBookSoFar, { issuerKeys });
608
+ await checker.add(newLines); // whole lines, each ending with a line feed
609
+ const result = await checker.result(); // the same shape as checkBook returns
610
+ ```
611
+
612
+ What the checker is told to trust is fixed when it is opened. Each answer
613
+ is a copy of its own.
614
+
615
+ The checker never throws for a bad record. A failed check is a normal
616
+ answer with a named reason. It keeps two things apart:
617
+
618
+ - **problems**: reasons the record cannot be relied on (a changed entry, a
619
+ signature that does not fit, a broken chain);
620
+ - **breaches**: what a sound record shows about the agent (an action the
621
+ slip does not allow, a limit passed).
622
+
623
+ ### What the checker returns
624
+
625
+ | Member | Meaning |
626
+ |---|---|
627
+ | `size`, `root` | How many entries the book holds, and its top fingerprint |
628
+ | `problems` | Problems with the book as a whole |
629
+ | `entries` | One for each line: `index`, `kind` (`slip`, `stub`, `refusal`, `terms`, `seal`, `cancellation`, `acknowledgement`, `vouching`, `withdrawal`, `pass` or `unreadable`), `fingerprint`, `verified` (`all`, `some` or `none` of its signatures confirmed here), `problems`, `breaches`, `notes`, the signature states, and `content`. Where a time-stamp you trust on a later seal covers the entry: `existedBy`, the time by which it is shown to have existed. On a seal or a cancellation that passed its check: `stampedAt`, the time its own time-stamps give it |
630
+ | `held` | One for each cancellation handed over beside the book: whether it passed its check and was used, whether the book holds it (`inBook`), the time its time-stamp gives, and the acknowledgements handed over with it (`acknowledgements`: who acknowledged, and when by their own word) |
631
+ | `summary.problemFound` | A problem was found somewhere in the record |
632
+ | `summary.fullyChecked` | Every signature was checked on this device |
633
+ | `summary.intact` | No problem was found **and** every signature was checked. The only answer that is a pass |
634
+ | `summary.methodsMissing` | The signing methods this device lacks, if any, for example `ML-DSA-87` |
635
+ | `summary.withinSlips` | Every stub was compared with its slip, and none is outside it |
636
+ | `summary.firstBreach` | The first entry that shows the agent outside its slip, if any |
637
+ | `summary.counts` | Slips, stubs, countersigned stubs (the countersignature confirmed on this device), one-sided stubs (no countersignature; a stub that was not checked, because its slip failed, is neither), stubs the person approved, refusals, sets of terms, and the other kinds of entry |
638
+ | `summary.sealed` | How far the last time-stamp you trust reaches: how many entries, the time they existed by, and whether a service or a block (`by`) gave it. Otherwise nothing |
639
+ | `summary.limits` | What no check can show, in plain sentences |
640
+
641
+ A signature state is `valid`, `invalid`, `unavailable` (this device
642
+ lacks the method; never a pass), or `unchecked` (not checked, because
643
+ what it rests on failed its check). Each problem and each breach has a `code`
644
+ in plain words and a `message`. The codes are listed in
645
+ `docs/threat-model.md`, section 8.
646
+
647
+ The `content` of an entry that has problems, or whose `verified` is
648
+ `none`, is unverified. It is there for whoever must find out what went
649
+ wrong. Never show it as fact: the checking page and the command-line
650
+ checker do not.
651
+
652
+ Options: `issuerKeys` (the thumbprints of issuer keys you trust),
653
+ `sealKeys` (the fingerprints of the key sets of the recorders you
654
+ expect), `stampServices` (the fingerprints of the certificates of the
655
+ time-stamp services you trust), `blocks` (the fingerprints of the
656
+ blocks of a public blockchain you trust), `vouchers` (the fingerprints of the key
657
+ sets of the organisations whose vouching you trust), `disclosures` (the
658
+ disclosures of covered fields that you were handed), `cancellations`
659
+ (the person's own copies of cancellations), `expectedRoot` and
660
+ `expectedSize` (a top fingerprint and a number of entries you already
661
+ hold a copy of). Give
662
+ them whenever you have them.
663
+
664
+ The checker reads a whole book into memory. A book of several hundred
665
+ megabytes may be too large for a browser or for Node.js to hold as one
666
+ text.
667
+
668
+ The command-line checker ends with code 0 (intact, nothing outside the
669
+ slip, and checked against keys you named: the passkey you trust and,
670
+ where the record has a seal, the recorder you expect), 1 (no problem
671
+ found, but the agent is outside its slip, not every signature could be
672
+ checked, or you did not name whom you trust) or 2 (a problem was found,
673
+ or what you gave on the command line could not be used). A value that
674
+ is not a fingerprint is refused, never set aside. The checking page
675
+ gives its green answer on the same terms.
676
+
677
+ Writing records: `prepareSlip` and `assembleSlip`, `prepareApproval` and
678
+ `assembleApproval` (the person), `writeStub` (the agent), `countersign`,
679
+ `prepareCancellation` and `assembleCancellation` (the person),
680
+ `writeRefusal` and `writeTerms` (the service), `writeVouching` and
681
+ `writeWithdrawal` (an organisation), `writePass` (an agent, to a helper
682
+ agent), `writeSeal` (whoever keeps
683
+ the book; it needs Node.js), `writeBook` and `makeShow`, all from
684
+ `provared`. The library asks no time-stamp service itself: you send
685
+ the seal's fingerprint to the service you choose and put its answer
686
+ beside the seal. In a browser, `createPasskey`, `signSlipWithPasskey`,
687
+ `approveWithPasskey` and `cancelWithPasskey` from
688
+ `provared/passkey-browser`. For a slip with covered fields in a browser:
689
+ `prepareSlip` with `cover`, then `signChallengeWithPasskey` with what it
690
+ prepared, then `assembleSlip`; keep the disclosures it prepared.
691
+
692
+ ## What a record does not prove
693
+
694
+ - It proves what was recorded, not what was left out. An agent that acts
695
+ and writes no stub leaves no trace.
696
+ - A one-sided stub (one with no countersignature) proves what the agent's
697
+ side said, not what the other side did.
698
+ - A refusal proves what the service's side said, not what the agent did.
699
+ - A rule of conduct in a slip is the person's signed instruction. No
700
+ check can show that the agent kept it.
701
+ - An approval proves that the passkey approved that one action, not that
702
+ the person read what they approved.
703
+ - It does not show that an action was wise, lawful or wanted.
704
+ - A badly written slip is faithfully recorded as a badly written slip.
705
+ - It does not prove who was holding the device when the passkey signed.
706
+ - A name in a record is a label. Only keys are checked. A vouching
707
+ record shows which organisation stood behind a name, not how well it
708
+ checked.
709
+
710
+ And about time:
711
+
712
+ - only an outside time-stamp you trust, from a service or a block you
713
+ named, proves when entries existed. The times inside records are the signer's own word,
714
+ so a limit within a period rests on the agent's word;
715
+ - after the last seal, the latest stubs under a slip or a pass, or a
716
+ slip or a pass with all its stubs, can be removed unnoticed, which can hide a stub that shows
717
+ the agent outside its slip;
718
+ - the last seal, with everything after it, can itself be removed. Only a
719
+ copy of it, or of its top fingerprint and number of entries, held by
720
+ someone else shows that;
721
+ - time-stamp services sign with methods of today's kind, which are not
722
+ quantum-safe.
723
+
724
+ ## What it never does
725
+
726
+ It never holds money, never starts a payment and issues no token. It
727
+ reports nothing back from the machine it runs on. The checker makes no
728
+ network request. Only `provared-headers`, which you run yourself,
729
+ connects to anything: to the Bitcoin nodes you name, for block headers.
730
+
731
+ ## Not built yet
732
+
733
+ A public place where seals are kept; covering fields other than a slip's
734
+ names and purpose; connectors for particular agent software (the
735
+ general one, `recordTools`, is built); a compact binary form; the
736
+ recorder service. The checking page cannot yet take a chain of block
737
+ headers: use the command-line checker.
738
+
739
+ ## Licences
740
+
741
+ Copyright 2026 Pavel Izmaylov.
742
+
743
+ The code, the pages and the sample files: Apache License 2.0
744
+ (`LICENSE`). The format description and the other documents: Creative
745
+ Commons Attribution 4.0 International (`LICENSE-CC-BY-4.0.txt`).