provared 0.2.0 → 0.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/CHANGELOG.md +85 -0
- package/README.md +116 -653
- package/bin/provared-check.mjs +36 -1
- package/docs/guide.md +736 -0
- package/package.json +25 -2
- package/src/dev.d.ts +54 -0
- package/src/dev.js +123 -0
package/docs/guide.md
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
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.3.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.) The file for
|
|
153
|
+
version 0.3.0 is attached to the release on GitHub; it is the same
|
|
154
|
+
file, byte for byte, as for version 0.2.0, because nothing the page
|
|
155
|
+
holds changed. Its SHA-256 fingerprint is
|
|
156
|
+
`7e142fa119458ce00c5fdd6e1cafc75cb3bb4672624157a0a0ef4624f92812ae`.
|
|
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, and the action is inside the slip
|
|
392
|
+
answer.breaches; // why not, in the checker's own codes
|
|
393
|
+
answer.problems; // why the question could not be answered with a yes
|
|
394
|
+
answer.needs; // what the slip asks to go with it: 'approval', 'countersignature'
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
It must be told whose passkey it trusts: a slip signed by any other key
|
|
398
|
+
allows nothing. Where the slip asks for the person's approval, the action
|
|
399
|
+
is allowed only if the signed approval is handed over. Where the slip
|
|
400
|
+
asks for a countersignature, `needs` says so: the action is inside the
|
|
401
|
+
slip only if the other side then countersigns it.
|
|
402
|
+
|
|
403
|
+
It is a help, not evidence: nothing in a record shows that it was called.
|
|
404
|
+
|
|
405
|
+
## Beside an agent
|
|
406
|
+
|
|
407
|
+
`openRecorder` puts the two things an agent's software must do into one
|
|
408
|
+
call: ask first, and write the receipt after.
|
|
409
|
+
|
|
410
|
+
```js
|
|
411
|
+
import { openRecorder } from 'provared';
|
|
412
|
+
|
|
413
|
+
const recorder = await openRecorder({ book, slip, privateKeys, issuerKeys });
|
|
414
|
+
|
|
415
|
+
const outcome = await recorder.act(
|
|
416
|
+
{ action: 'provared.order.place', amount: { unit: 'GBP', value: 45 }, with: 'supplier' },
|
|
417
|
+
async () => placeTheOrder(), // taken only if the answer is yes
|
|
418
|
+
{ countersign: askTheSupplierToCountersign },
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
outcome.done; // false: the action was outside the slip, was not taken, and nothing was written
|
|
422
|
+
recorder.book(); // the book, with the new stub in the agent's chain
|
|
423
|
+
await recorder.check(); // what the record shows as it stands, as checkBook would answer
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
If taking the action fails with an error, no stub is written; the caller
|
|
427
|
+
must deal with whatever part of the action did happen. `record` writes a
|
|
428
|
+
stub without asking, for an action that was taken some other way: the
|
|
429
|
+
record is of what happened, inside the slip or not.
|
|
430
|
+
|
|
431
|
+
The writer's calls wait for one another, so two actions asked for at the
|
|
432
|
+
same moment are taken one after the other, each against the book as the
|
|
433
|
+
one before left it. A countersignature that the other side hands back is
|
|
434
|
+
checked before it goes into the book; one that does not check is left
|
|
435
|
+
out, the stub stands one-sided, and `stub.countersignature.problem` says
|
|
436
|
+
why. `add` puts an entry that someone else made (a seal, a cancellation,
|
|
437
|
+
a refusal) into the book only if the book still passes its check with
|
|
438
|
+
it, and it is not dated ahead of the clock. The other side is given 30
|
|
439
|
+
seconds to countersign (`countersignWithin`); after that the stub is
|
|
440
|
+
written one-sided. Where the slip asks for a countersignature, `act`
|
|
441
|
+
takes the action only if it is given a way to ask for one. Before an
|
|
442
|
+
action is taken, its stub is written and checked with the whole book, so
|
|
443
|
+
that the receipt is known to fit. `act` takes an action now: a date
|
|
444
|
+
handed to it must be within 300 seconds of the writer's clock (`now`).
|
|
445
|
+
Nothing is written that is dated more than 300 seconds ahead of that
|
|
446
|
+
clock. What the writer is handed (the request, an approval, what the
|
|
447
|
+
other side hands back) is copied when it is handed over, and the copy is
|
|
448
|
+
what is checked and written. `record` hands a stub to the other side
|
|
449
|
+
only once it has passed the check of the whole book. The function
|
|
450
|
+
that takes the action must not call the writer. Use one writer for one
|
|
451
|
+
chain: two writers on two copies of a book know nothing of each other.
|
|
452
|
+
|
|
453
|
+
The writer reads its book once, when it is opened, and then carries it
|
|
454
|
+
on: each new line is read by itself, and no signature is checked twice.
|
|
455
|
+
So the time an action takes grows only slowly with the length of the
|
|
456
|
+
book. Measured on one computer on 4 October 2026, with countersigned
|
|
457
|
+
stubs: 9, 14 and 23 thousandths of a second for one action after 400,
|
|
458
|
+
1,600 and 6,400 stubs. (Before the book was carried on: 2.0 seconds
|
|
459
|
+
after 400 stubs and 7.7 after 1,600.) Opening the writer still reads
|
|
460
|
+
the whole book: about one second for every 450 stubs. Every line
|
|
461
|
+
is tried on a reader split off the book's own before it joins the book,
|
|
462
|
+
so an entry that does not fit, or an error part of the way through a
|
|
463
|
+
call, leaves the writer's view of its book exact. `recorder.check()`
|
|
464
|
+
gives the checker's answer for the book as it stands, without reading
|
|
465
|
+
it again. `before` waits its turn with the other calls. What the writer
|
|
466
|
+
is told to trust (`options`) is read once, when it is opened, and
|
|
467
|
+
fixed.
|
|
468
|
+
|
|
469
|
+
A cancellation that the person signed is never lost in `add`. If it
|
|
470
|
+
cannot be added as it was handed over (a faulty time-stamp beside it, a
|
|
471
|
+
member that cannot be written as a line, such as a list with a gap in
|
|
472
|
+
it), the writer adds the cancellation with its time-stamps, or with
|
|
473
|
+
those of them that pass their check, or by itself. If it cannot be added even so (it is dated ahead
|
|
474
|
+
of the clock), the writer keeps it, allows nothing more under the slip,
|
|
475
|
+
and writes it into the book, at its next call, once the clock has
|
|
476
|
+
reached its date. In both cases `add` fails, and the error says what was
|
|
477
|
+
done. (Where the clock fails, the error is the clock's own; the
|
|
478
|
+
cancellation is held all the same, as `cancellations()` shows.) A
|
|
479
|
+
cancellation that the writer keeps lives only in that writer:
|
|
480
|
+
`recorder.cancellations()` hands back what it holds, each with the
|
|
481
|
+
acknowledgement the writer signed for it, and so does an acknowledgement
|
|
482
|
+
that still waits to follow its cancellation into the book. Keep that
|
|
483
|
+
with the book, and hand it in again (`options.cancellations`, an empty
|
|
484
|
+
list included) when you open a writer again; a writer opened from the
|
|
485
|
+
book alone knows nothing of it. Read `recorder.book()` and
|
|
486
|
+
`recorder.cancellations()` together, with nothing awaited in between,
|
|
487
|
+
so that the two you keep belong to the same moment. A cancellation given in that way is written into the book once
|
|
488
|
+
its date has come, and stops the writer from the start. A copy that
|
|
489
|
+
the writer holds takes the time-stamps handed over with it later: each
|
|
490
|
+
is judged by itself, those the writer counts come first, and a copy
|
|
491
|
+
holds four at most. The writer is stopped before it reads its clock, so
|
|
492
|
+
a clock that fails then does not lose the cancellation, nor the sound
|
|
493
|
+
time-stamps, nor the acknowledgements handed over with it. A clock that
|
|
494
|
+
gives anything but a time counts as a clock that failed. An
|
|
495
|
+
acknowledgement that is handed over with a cancellation is taken as the
|
|
496
|
+
agent's own only if it checks by itself, and only the first such one is
|
|
497
|
+
taken: the writer never puts a second one of the agent's own with a
|
|
498
|
+
copy (a copy given at opening that already holds two is handed back as
|
|
499
|
+
it was given). A copy holds four
|
|
500
|
+
acknowledgements at most; the writer's own is always among them, and
|
|
501
|
+
where the copy already holds four, the last of the others gives way to
|
|
502
|
+
it. A writer that is stopped
|
|
503
|
+
takes no action, but `record` still writes the stub of an action that
|
|
504
|
+
was taken all the same: the record is of what happened, and a checker
|
|
505
|
+
reports such a stub.
|
|
506
|
+
|
|
507
|
+
### The agent's tools
|
|
508
|
+
|
|
509
|
+
An agent acts by calling tools: functions that its software offers it.
|
|
510
|
+
`recordTools` puts each tool behind the stub writer, so that no call is
|
|
511
|
+
forgotten. The agent's software calls the tools as before.
|
|
512
|
+
|
|
513
|
+
```js
|
|
514
|
+
import { openRecorder, recordTools } from 'provared';
|
|
515
|
+
|
|
516
|
+
const recorder = await openRecorder({ book, slip, privateKeys, issuerKeys });
|
|
517
|
+
const tools = recordTools(recorder, {
|
|
518
|
+
placeOrder: {
|
|
519
|
+
action: 'provared.order.place', // the name the slip uses for it
|
|
520
|
+
with: 'supplier',
|
|
521
|
+
amount: (order) => ({ unit: 'GBP', value: order.total }),
|
|
522
|
+
run: (order) => sendTheOrder(order), // run only if the slip allows it
|
|
523
|
+
countersign: (stub) => askTheSupplierToCountersign(stub),
|
|
524
|
+
},
|
|
525
|
+
readStock: { action: 'provared.data.read', run: (query) => lookUpStock(query) },
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
await tools.placeOrder({ item: 'paper', boxes: 5, total: 45 });
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
A call that the slip does not allow throws `NotTaken`, with the answer
|
|
532
|
+
of the check before acting, and the tool is not run. Where the slip asks
|
|
533
|
+
for the person's own approval, a tool's `approve` function is asked for
|
|
534
|
+
it once. By default the stub of a call names one document,
|
|
535
|
+
`arguments`: the fingerprint of the call's arguments, written in one
|
|
536
|
+
form (RFC 8785). So the record fixes exactly what was asked for without
|
|
537
|
+
holding it; `argumentsFingerprint` works the same fingerprint out again.
|
|
538
|
+
The arguments must be JSON: a list with a gap in it, or with a named
|
|
539
|
+
member, or a member that is `undefined`, is refused, and the tool is
|
|
540
|
+
not run. A fingerprint of arguments
|
|
541
|
+
that are few and easy to guess can be guessed. What a tool is, and what
|
|
542
|
+
it is called with, is copied when it is handed over; each function a
|
|
543
|
+
tool is described by gets a copy of the arguments of its own, and is
|
|
544
|
+
called by itself, without `this`: it is not handed the description and
|
|
545
|
+
cannot change it. What `amount`, `with` and `details` give is
|
|
546
|
+
copied too, so a function that keeps what it gave cannot change it
|
|
547
|
+
afterwards. `amount` and `with` must give their value at once,
|
|
548
|
+
not a promise. The numbers `0` and `-0` share a fingerprint, as RFC
|
|
549
|
+
8785 writes both as `0`. If `onStub` fails, its error is passed on,
|
|
550
|
+
though the tool has run and its stub is in the book. `recordTools`
|
|
551
|
+
names no agent software, and works with any function.
|
|
552
|
+
|
|
553
|
+
## How signing works
|
|
554
|
+
|
|
555
|
+
- **The person** signs a slip with a passkey. A passkey signs a short
|
|
556
|
+
value handed to it; that value is the SHA-256 fingerprint of the slip's
|
|
557
|
+
exact signed bytes, so the signature covers the whole slip.
|
|
558
|
+
- **The agent and the service** each sign with two keys at once: Ed25519,
|
|
559
|
+
which is today's kind, and ML-DSA-87, which is quantum-safe. A record
|
|
560
|
+
counts only if both signatures check. A forger must break both.
|
|
561
|
+
- **The passkey signature is of today's kind only**, because no device
|
|
562
|
+
offers a quantum-safe passkey yet.
|
|
563
|
+
- **Whoever keeps the book** signs each seal with three keys: the same
|
|
564
|
+
two, and SLH-DSA-SHA2-256s, which rests only on fingerprint functions.
|
|
565
|
+
- **Browsers are only now adding ML-DSA, and have no SLH-DSA.** Where a
|
|
566
|
+
browser lacks a method, the checking page says that those signatures
|
|
567
|
+
were not checked, and does not show a pass. Node.js checks all of them.
|
|
568
|
+
|
|
569
|
+
## Using the library
|
|
570
|
+
|
|
571
|
+
```js
|
|
572
|
+
import { checkBook } from 'provared/check';
|
|
573
|
+
|
|
574
|
+
const result = await checkBook(textOfTheBook, {
|
|
575
|
+
issuerKeys: ['the thumbprint of an issuer key you already trust'],
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
result.summary.intact; // no problem found, and every signature checked
|
|
579
|
+
result.summary.problemFound; // was a problem found in the record?
|
|
580
|
+
result.summary.fullyChecked; // was every signature checked on this device?
|
|
581
|
+
result.summary.withinSlips; // every stub compared with its slip, none outside it
|
|
582
|
+
result.summary.firstBreach; // the first entry that shows the agent outside its slip
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
Type declarations come with the library (`src/types.d.ts`), so an editor
|
|
586
|
+
can check how it is called.
|
|
587
|
+
|
|
588
|
+
**A book that grows.** `checkBook` reads a whole book each time. To
|
|
589
|
+
follow a book as lines are added to it, open a checker once and carry it
|
|
590
|
+
on. It reads each new line against what it worked out from the lines
|
|
591
|
+
before it, and checks no signature twice. Its answer is the answer
|
|
592
|
+
`checkBook` gives for the same text; a test compares the two at every
|
|
593
|
+
length of a book that holds every kind of entry.
|
|
594
|
+
|
|
595
|
+
```js
|
|
596
|
+
import { openChecker } from 'provared/check';
|
|
597
|
+
|
|
598
|
+
const checker = await openChecker(textOfTheBookSoFar, { issuerKeys });
|
|
599
|
+
await checker.add(newLines); // whole lines, each ending with a line feed
|
|
600
|
+
const result = await checker.result(); // the same shape as checkBook returns
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
What the checker is told to trust is fixed when it is opened. Each answer
|
|
604
|
+
is a copy of its own.
|
|
605
|
+
|
|
606
|
+
The checker never throws for a bad record. A failed check is a normal
|
|
607
|
+
answer with a named reason. It keeps two things apart:
|
|
608
|
+
|
|
609
|
+
- **problems**: reasons the record cannot be relied on (a changed entry, a
|
|
610
|
+
signature that does not fit, a broken chain);
|
|
611
|
+
- **breaches**: what a sound record shows about the agent (an action the
|
|
612
|
+
slip does not allow, a limit passed).
|
|
613
|
+
|
|
614
|
+
### What the checker returns
|
|
615
|
+
|
|
616
|
+
| Member | Meaning |
|
|
617
|
+
|---|---|
|
|
618
|
+
| `size`, `root` | How many entries the book holds, and its top fingerprint |
|
|
619
|
+
| `problems` | Problems with the book as a whole |
|
|
620
|
+
| `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 |
|
|
621
|
+
| `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) |
|
|
622
|
+
| `summary.problemFound` | A problem was found somewhere in the record |
|
|
623
|
+
| `summary.fullyChecked` | Every signature was checked on this device |
|
|
624
|
+
| `summary.intact` | No problem was found **and** every signature was checked. The only answer that is a pass |
|
|
625
|
+
| `summary.methodsMissing` | The signing methods this device lacks, if any, for example `ML-DSA-87` |
|
|
626
|
+
| `summary.withinSlips` | Every stub was compared with its slip, and none is outside it |
|
|
627
|
+
| `summary.firstBreach` | The first entry that shows the agent outside its slip, if any |
|
|
628
|
+
| `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 |
|
|
629
|
+
| `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 |
|
|
630
|
+
| `summary.limits` | What no check can show, in plain sentences |
|
|
631
|
+
|
|
632
|
+
A signature state is `valid`, `invalid`, `unavailable` (this device
|
|
633
|
+
lacks the method; never a pass), or `unchecked` (not checked, because
|
|
634
|
+
what it rests on failed its check). Each problem and each breach has a `code`
|
|
635
|
+
in plain words and a `message`. The codes are listed in
|
|
636
|
+
`docs/threat-model.md`, section 8.
|
|
637
|
+
|
|
638
|
+
The `content` of an entry that has problems, or whose `verified` is
|
|
639
|
+
`none`, is unverified. It is there for whoever must find out what went
|
|
640
|
+
wrong. Never show it as fact: the checking page and the command-line
|
|
641
|
+
checker do not.
|
|
642
|
+
|
|
643
|
+
Options: `issuerKeys` (the thumbprints of issuer keys you trust),
|
|
644
|
+
`sealKeys` (the fingerprints of the key sets of the recorders you
|
|
645
|
+
expect), `stampServices` (the fingerprints of the certificates of the
|
|
646
|
+
time-stamp services you trust), `blocks` (the fingerprints of the
|
|
647
|
+
blocks of a public blockchain you trust), `vouchers` (the fingerprints of the key
|
|
648
|
+
sets of the organisations whose vouching you trust), `disclosures` (the
|
|
649
|
+
disclosures of covered fields that you were handed), `cancellations`
|
|
650
|
+
(the person's own copies of cancellations), `expectedRoot` and
|
|
651
|
+
`expectedSize` (a top fingerprint and a number of entries you already
|
|
652
|
+
hold a copy of). Give
|
|
653
|
+
them whenever you have them.
|
|
654
|
+
|
|
655
|
+
The checker reads a whole book into memory. A book of several hundred
|
|
656
|
+
megabytes may be too large for a browser or for Node.js to hold as one
|
|
657
|
+
text.
|
|
658
|
+
|
|
659
|
+
The command-line checker ends with code 0 (intact, nothing outside the
|
|
660
|
+
slip, and checked against keys you named: the passkey you trust and,
|
|
661
|
+
where the record has a seal, the recorder you expect), 1 (no problem
|
|
662
|
+
found, but the agent is outside its slip, not every signature could be
|
|
663
|
+
checked, or you did not name whom you trust) or 2 (a problem was found,
|
|
664
|
+
or what you gave on the command line could not be used). A value that
|
|
665
|
+
is not a fingerprint is refused, never set aside. The checking page
|
|
666
|
+
gives its green answer on the same terms.
|
|
667
|
+
|
|
668
|
+
Writing records: `prepareSlip` and `assembleSlip`, `prepareApproval` and
|
|
669
|
+
`assembleApproval` (the person), `writeStub` (the agent), `countersign`,
|
|
670
|
+
`prepareCancellation` and `assembleCancellation` (the person),
|
|
671
|
+
`writeRefusal` and `writeTerms` (the service), `writeVouching` and
|
|
672
|
+
`writeWithdrawal` (an organisation), `writePass` (an agent, to a helper
|
|
673
|
+
agent), `writeSeal` (whoever keeps
|
|
674
|
+
the book; it needs Node.js), `writeBook` and `makeShow`, all from
|
|
675
|
+
`provared`. The library asks no time-stamp service itself: you send
|
|
676
|
+
the seal's fingerprint to the service you choose and put its answer
|
|
677
|
+
beside the seal. In a browser, `createPasskey`, `signSlipWithPasskey`,
|
|
678
|
+
`approveWithPasskey` and `cancelWithPasskey` from
|
|
679
|
+
`provared/passkey-browser`. For a slip with covered fields in a browser:
|
|
680
|
+
`prepareSlip` with `cover`, then `signChallengeWithPasskey` with what it
|
|
681
|
+
prepared, then `assembleSlip`; keep the disclosures it prepared.
|
|
682
|
+
|
|
683
|
+
## What a record does not prove
|
|
684
|
+
|
|
685
|
+
- It proves what was recorded, not what was left out. An agent that acts
|
|
686
|
+
and writes no stub leaves no trace.
|
|
687
|
+
- A one-sided stub (one with no countersignature) proves what the agent's
|
|
688
|
+
side said, not what the other side did.
|
|
689
|
+
- A refusal proves what the service's side said, not what the agent did.
|
|
690
|
+
- A rule of conduct in a slip is the person's signed instruction. No
|
|
691
|
+
check can show that the agent kept it.
|
|
692
|
+
- An approval proves that the passkey approved that one action, not that
|
|
693
|
+
the person read what they approved.
|
|
694
|
+
- It does not show that an action was wise, lawful or wanted.
|
|
695
|
+
- A badly written slip is faithfully recorded as a badly written slip.
|
|
696
|
+
- It does not prove who was holding the device when the passkey signed.
|
|
697
|
+
- A name in a record is a label. Only keys are checked. A vouching
|
|
698
|
+
record shows which organisation stood behind a name, not how well it
|
|
699
|
+
checked.
|
|
700
|
+
|
|
701
|
+
And about time:
|
|
702
|
+
|
|
703
|
+
- only an outside time-stamp you trust, from a service or a block you
|
|
704
|
+
named, proves when entries existed. The times inside records are the signer's own word,
|
|
705
|
+
so a limit within a period rests on the agent's word;
|
|
706
|
+
- after the last seal, the latest stubs under a slip or a pass, or a
|
|
707
|
+
slip or a pass with all its stubs, can be removed unnoticed, which can hide a stub that shows
|
|
708
|
+
the agent outside its slip;
|
|
709
|
+
- the last seal, with everything after it, can itself be removed. Only a
|
|
710
|
+
copy of it, or of its top fingerprint and number of entries, held by
|
|
711
|
+
someone else shows that;
|
|
712
|
+
- time-stamp services sign with methods of today's kind, which are not
|
|
713
|
+
quantum-safe.
|
|
714
|
+
|
|
715
|
+
## What it never does
|
|
716
|
+
|
|
717
|
+
It never holds money, never starts a payment and issues no token. It
|
|
718
|
+
reports nothing back from the machine it runs on. The checker makes no
|
|
719
|
+
network request. Only `provared-headers`, which you run yourself,
|
|
720
|
+
connects to anything: to the Bitcoin nodes you name, for block headers.
|
|
721
|
+
|
|
722
|
+
## Not built yet
|
|
723
|
+
|
|
724
|
+
A public place where seals are kept; covering fields other than a slip's
|
|
725
|
+
names and purpose; connectors for particular agent software (the
|
|
726
|
+
general one, `recordTools`, is built); a compact binary form; the
|
|
727
|
+
recorder service. The checking page cannot yet take a chain of block
|
|
728
|
+
headers: use the command-line checker.
|
|
729
|
+
|
|
730
|
+
## Licences
|
|
731
|
+
|
|
732
|
+
Copyright 2026 Pavel Izmaylov.
|
|
733
|
+
|
|
734
|
+
The code, the pages and the sample files: Apache License 2.0
|
|
735
|
+
(`LICENSE`). The format description and the other documents: Creative
|
|
736
|
+
Commons Attribution 4.0 International (`LICENSE-CC-BY-4.0.txt`).
|