provared 0.5.0 → 0.7.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 +1496 -0
- package/README.md +18 -3
- package/bin/provared-check.mjs +128 -13
- package/docs/guide.md +346 -24
- package/docs/threat-model.md +299 -21
- package/net/headers.js +1 -0
- package/package.json +2 -2
- package/page/check-page.js +18 -32
- package/page/index.html +6 -0
- package/page/render.js +183 -10
- package/spec/provared-format.md +771 -37
- package/src/book.js +544 -19
- package/src/check.d.ts +4 -0
- package/src/check.js +3 -1
- package/src/denial.js +123 -0
- package/src/der.js +56 -6
- package/src/dev.d.ts +1 -1
- package/src/dev.js +1 -1
- package/src/documents.js +8 -3
- package/src/guard.js +12 -1
- package/src/index.js +10 -0
- package/src/jws.js +8 -0
- package/src/keys.js +48 -1
- package/src/recorder.js +131 -5
- package/src/seal.js +32 -4
- package/src/sealer.js +812 -0
- package/src/signatures.js +12 -6
- package/src/slip.js +72 -5
- package/src/timestamp.js +55 -16
- package/src/tools.js +30 -13
- package/src/tree.js +139 -7
- package/src/types.d.ts +272 -11
- package/src/witness.js +63 -0
- package/src/witnessing.js +362 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# <img src="https://prova.red/favicon.svg" alt="" width="36"> Provared
|
|
2
2
|
|
|
3
|
+
[](https://scorecard.dev/viewer/?uri=github.com/provared/provared)
|
|
4
|
+
|
|
3
5
|
Proof of what an AI agent was allowed to do, and what it then did.
|
|
4
6
|
|
|
5
7
|
A person signs the permission with a passkey. The agent signs a receipt
|
|
@@ -86,6 +88,11 @@ agent = create_agent(model, tools=[order_supplies, send_message],
|
|
|
86
88
|
})]) # last among the middleware that wrap tool calls
|
|
87
89
|
```
|
|
88
90
|
|
|
91
|
+
The package is listed among the middleware in
|
|
92
|
+
[LangChain's own documentation](https://docs.langchain.com/oss/python/integrations/middleware).
|
|
93
|
+
For JavaScript agents, [examples/frameworks/](examples/frameworks/) shows the
|
|
94
|
+
same with the Vercel AI SDK, through `recordTools`.
|
|
95
|
+
|
|
89
96
|
## The four parts
|
|
90
97
|
|
|
91
98
|
| Part | What it is |
|
|
@@ -125,7 +132,7 @@ each part protects against, is in
|
|
|
125
132
|
|
|
126
133
|
- **[The guide](docs/guide.md)**: the whole library, in one document.
|
|
127
134
|
Installing and importing; the command-line checker; what a slip can
|
|
128
|
-
say; seals, time-stamps and the chain of block headers; cancelling;
|
|
135
|
+
say; seals, time-stamps, witnesses and the chain of block headers; cancelling;
|
|
129
136
|
who stands behind a name; covered fields; passing a slip on; the check
|
|
130
137
|
before acting; the stub writer beside an agent and the connector for
|
|
131
138
|
its tools; what the checker returns; what it never does; what is not
|
|
@@ -136,6 +143,14 @@ each part protects against, is in
|
|
|
136
143
|
- **[The Python version](python/README.md)**, which gives the same
|
|
137
144
|
answers, with examples for three agent frameworks and packages for
|
|
138
145
|
two of them.
|
|
146
|
+
- **Two programs beside an agent, each on npm with its own README:**
|
|
147
|
+
[the recorder beside Claude Code](integrations/claude-code/README.md)
|
|
148
|
+
(`npm install -g provared-claude-code`, also installable as a Claude
|
|
149
|
+
Code plugin), which checks and receipts every tool call Claude Code
|
|
150
|
+
makes; and [the executor for MCP tool servers](mcp-executor/README.md)
|
|
151
|
+
(`npm install -g provared-mcp-executor`), which stands between an
|
|
152
|
+
agent and its tool servers, holds their credentials, and countersigns
|
|
153
|
+
each receipt with keys of its own.
|
|
139
154
|
- **[The pages at prova.red](https://prova.red/)**: the checker's answer
|
|
140
155
|
for the sample record, a worked example for developers, and the same
|
|
141
156
|
in plain words for a reader who does not write code.
|
|
@@ -156,9 +171,9 @@ each part protects against, is in
|
|
|
156
171
|
|
|
157
172
|
## Status
|
|
158
173
|
|
|
159
|
-
**Version 0.
|
|
174
|
+
**Version 0.7.0, a draft.** Published as it is, by one author in their
|
|
160
175
|
own time, built with AI tools since 3 October 2026; fixes as time
|
|
161
|
-
allows. The code has been through
|
|
176
|
+
allows. The code has been through more than 80 rounds of review by AI reviewers,
|
|
162
177
|
each reviewer a fresh session given the code, the documents and a brief
|
|
163
178
|
of what to look at; every fault was fixed, or stated as a limit, and
|
|
164
179
|
kept as a test. No person other than the author has reviewed it yet.
|
package/bin/provared-check.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// provared-check: check a book or a Show from the command line.
|
|
3
3
|
//
|
|
4
|
-
// provared-check <file> [--issuer <thumbprint>]... [--sealer <fingerprint>]... [--stamp-service <fingerprint>]...
|
|
4
|
+
// provared-check <file> [--issuer <thumbprint>]... [--origin <address>]... [--sealer <fingerprint>]... [--stamp-service <fingerprint>]...
|
|
5
5
|
// [--block <fingerprint>]... [--voucher <fingerprint>]... [--disclosures <file>] [--cancellation <file>]...
|
|
6
|
-
// [--root <top fingerprint>] [--size <entries>] [--json]
|
|
6
|
+
// [--seal <file>]... [--witness <fingerprint>]... [--root <top fingerprint>] [--size <entries>] [--json]
|
|
7
7
|
//
|
|
8
8
|
// Exit codes:
|
|
9
9
|
// 0 the record is intact (no problem found, every signature checked),
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { readFileSync } from 'node:fs';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
|
-
import { MIN_BLOCKS_AFTER, REFUSAL_REASONS, blocksInChain, checkBook, checkHeaderChain, checkShow, conditionWords, limitWords, neverWords } from '../src/check.js';
|
|
23
|
+
import { DENIAL_SOURCES, MIN_BLOCKS_AFTER, REFUSAL_REASONS, blocksInChain, checkBook, checkHeaderChain, checkShow, conditionWords, isPageAddress, limitWords, neverWords } from '../src/check.js';
|
|
24
24
|
|
|
25
25
|
// Node.js marks its ML-DSA and SLH-DSA as experimental and says so on every
|
|
26
26
|
// run. The checker does not repeat that warning; any other warning is shown.
|
|
@@ -29,8 +29,9 @@ process.on('warning', (w) => {
|
|
|
29
29
|
if (w.name !== 'ExperimentalWarning') console.error(`${w.name}: ${w.message}`);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
const USAGE = `Usage: provared-check <file> [--issuer <thumbprint>]... [--
|
|
33
|
-
[--
|
|
32
|
+
const USAGE = `Usage: provared-check <file> [--issuer <thumbprint>]... [--origin <address>]... [--sealer <fingerprint>]...
|
|
33
|
+
[--stamp-service <fingerprint>]... [--block <fingerprint>]... [--voucher <fingerprint>]...
|
|
34
|
+
[--disclosures <file>] [--cancellation <file>]... [--seal <file>]... [--witness <fingerprint>]...
|
|
34
35
|
[--root <top fingerprint>] [--size <entries>] [--headers <file>] [--json]
|
|
35
36
|
provared-check --sample [--json]
|
|
36
37
|
|
|
@@ -38,6 +39,9 @@ const USAGE = `Usage: provared-check <file> [--issuer <thumbprint>]... [--sealer
|
|
|
38
39
|
--sample check the sample record that comes with the library, naming the passkey, the recorder and the
|
|
39
40
|
time-stamp service it was made with as trusted (they are stand-ins, and the people are invented)
|
|
40
41
|
--issuer the thumbprint of an issuer key you already trust; may be repeated
|
|
42
|
+
--origin the address of the page at which the person signs permissions, such as https://sign.example.org
|
|
43
|
+
(https://, a website name in lower case, a port only if it is needed, and nothing more); a
|
|
44
|
+
permission signed at any other address is refused; may be repeated
|
|
41
45
|
--sealer the fingerprint of the key set of a recorder you expect to have sealed the book; may be repeated
|
|
42
46
|
--stamp-service the fingerprint of the certificate of a time-stamp service you trust; may be repeated
|
|
43
47
|
--block the fingerprint (64 hex characters) of a block of a public blockchain that you trust to be part of the chain;
|
|
@@ -47,6 +51,12 @@ const USAGE = `Usage: provared-check <file> [--issuer <thumbprint>]... [--sealer
|
|
|
47
51
|
--cancellation a JSON file of the person's own copy of a cancellation, as the entry it would be in a book:
|
|
48
52
|
{"cancellation": {...}, "stamps": [...]}. It counts even where the book leaves it out; may be repeated.
|
|
49
53
|
It may hold "acknowledgements": what the agent's side signed when it was handed the cancellation
|
|
54
|
+
--seal a JSON file of a seal held elsewhere (by you, an auditor, a copy kept apart), as the entry it is in the book:
|
|
55
|
+
{"seal": {...}, "stamps": [...]}. The book must hold that seal at the place it states: one that ends
|
|
56
|
+
before it was cut short. It counts only with --sealer naming its recorder; may be repeated.
|
|
57
|
+
It may hold "witnesses": statements that witnesses signed for that seal, such as ones that came later
|
|
58
|
+
--witness the fingerprint of the key set of a witness you trust: a seal counts as witnessed only by such a
|
|
59
|
+
witness. Being witnessed excuses nothing; may be repeated
|
|
50
60
|
--root the top fingerprint you already trust
|
|
51
61
|
--size the number of entries you already trust; keep it with the top fingerprint
|
|
52
62
|
--headers a file of Bitcoin block headers, as provared-headers keeps it. It is checked here, from the chain's
|
|
@@ -61,14 +71,14 @@ const FINGERPRINT = /^[A-Za-z0-9_-]{43}$/;
|
|
|
61
71
|
const BLOCK = /^[0-9a-f]{64}$/;
|
|
62
72
|
|
|
63
73
|
// Options that may be repeated, and those that may be given once.
|
|
64
|
-
const LISTS = { '--issuer': 'issuerKeys', '--sealer': 'sealKeys', '--stamp-service': 'stampServices', '--block': 'blocks', '--voucher': 'vouchers', '--cancellation': 'cancellations' };
|
|
74
|
+
const LISTS = { '--issuer': 'issuerKeys', '--origin': 'issuerOrigins', '--sealer': 'sealKeys', '--stamp-service': 'stampServices', '--block': 'blocks', '--voucher': 'vouchers', '--witness': 'witnesses', '--cancellation': 'cancellations', '--seal': 'seals' };
|
|
65
75
|
const ONCE = { '--disclosures': 'disclosures', '--root': 'expectedRoot', '--size': 'expectedSize', '--headers': 'headers' };
|
|
66
76
|
|
|
67
77
|
// The arguments, or the reason they cannot be used. A value that is not a
|
|
68
78
|
// fingerprint is refused, never set aside: a typing error must not quietly
|
|
69
79
|
// leave a time-stamp or a name uncounted.
|
|
70
80
|
function parseArguments(argv) {
|
|
71
|
-
const out = { file: null, sample: false, issuerKeys: [], sealKeys: [], stampServices: [], blocks: [], vouchers: [], cancellations: [], disclosures: undefined, expectedRoot: undefined, expectedSize: undefined, json: false };
|
|
81
|
+
const out = { file: null, sample: false, issuerKeys: [], issuerOrigins: [], sealKeys: [], stampServices: [], blocks: [], vouchers: [], witnesses: [], cancellations: [], seals: [], disclosures: undefined, expectedRoot: undefined, expectedSize: undefined, json: false };
|
|
72
82
|
for (let i = 0; i < argv.length; i++) {
|
|
73
83
|
const a = argv[i];
|
|
74
84
|
if (a === '--help' || a === '-h') return { help: true };
|
|
@@ -82,10 +92,13 @@ function parseArguments(argv) {
|
|
|
82
92
|
if (i + 1 >= argv.length) return { error: `${a} needs a value.` };
|
|
83
93
|
const value = argv[++i];
|
|
84
94
|
if (Object.hasOwn(ONCE, a) && out[ONCE[a]] !== undefined) return { error: `${a} may be given once.` };
|
|
85
|
-
if (['--issuer', '--sealer', '--stamp-service', '--voucher', '--root'].includes(a) && !FINGERPRINT.test(value)) {
|
|
95
|
+
if (['--issuer', '--sealer', '--stamp-service', '--voucher', '--witness', '--root'].includes(a) && !FINGERPRINT.test(value)) {
|
|
86
96
|
return { error: `The value of ${a} is not a fingerprint. A fingerprint is 43 letters, digits, hyphens or underscores, with no spaces.` };
|
|
87
97
|
}
|
|
88
98
|
if (a === '--block' && !BLOCK.test(value)) return { error: 'The value of --block is not the fingerprint of a block: 64 characters, the digits 0 to 9 and the letters a to f.' };
|
|
99
|
+
if (a === '--origin' && !isPageAddress(value)) {
|
|
100
|
+
return { error: 'The value of --origin is not a page address in its plain form: https://, a website name in lower case, a port only if it is needed, and nothing more, such as https://sign.example.org.' };
|
|
101
|
+
}
|
|
89
102
|
if (a === '--size' && !/^\d{1,6}$/.test(value)) return { error: 'The value of --size is not a number of entries.' };
|
|
90
103
|
if (Object.hasOwn(LISTS, a)) out[LISTS[a]].push(value);
|
|
91
104
|
else out[ONCE[a]] = a === '--size' ? Number(value) : value;
|
|
@@ -94,7 +107,7 @@ function parseArguments(argv) {
|
|
|
94
107
|
}
|
|
95
108
|
if (out.sample) {
|
|
96
109
|
if (out.file !== null) return { error: '--sample names the sample record itself: give no file with it.' };
|
|
97
|
-
for (const name of ['issuerKeys', 'sealKeys', 'stampServices', 'blocks', 'vouchers', 'cancellations']) {
|
|
110
|
+
for (const name of ['issuerKeys', 'issuerOrigins', 'sealKeys', 'stampServices', 'blocks', 'vouchers', 'witnesses', 'cancellations', 'seals']) {
|
|
98
111
|
if (out[name].length) return { error: '--sample names what to trust itself: give no other option with it but --json.' };
|
|
99
112
|
}
|
|
100
113
|
if (out.disclosures !== undefined || out.expectedRoot !== undefined || out.expectedSize !== undefined || out.headers !== undefined) {
|
|
@@ -282,10 +295,22 @@ function printSeal(e) {
|
|
|
282
295
|
console.log(` Recorder's key set fingerprint: ${e.sealer}`);
|
|
283
296
|
printStamps(e.stamps);
|
|
284
297
|
if (e.stamps.length === 0) console.log(' No outside time-stamp: the time is only the recorder\'s own word.');
|
|
298
|
+
printWitnesses(e.witnesses);
|
|
285
299
|
}
|
|
286
300
|
if (e.signatures && e.signatures.length) console.log(` Recorder's signatures: ${e.signatures.map((s) => `${s.method} ${s.state}`).join('; ')}`);
|
|
287
301
|
}
|
|
288
302
|
|
|
303
|
+
// The witness statements beside a seal. What a statement says is shown only where it passed its check.
|
|
304
|
+
function printWitnesses(list) {
|
|
305
|
+
for (const w of list ?? []) {
|
|
306
|
+
if (w.state === 'invalid') console.log(' Witness statement, NOT USED: it did not pass its check (see the problems)');
|
|
307
|
+
else if (w.state === 'unavailable') console.log(' Witness statement, NOT CHECKED: this device cannot check both of its signatures');
|
|
308
|
+
else if (w.counted) console.log(` WITNESSED by ${label(w.name)} (a label), a witness you named as trusted (key set ${w.witness}), at ${w.when} (the witness's own word)`);
|
|
309
|
+
else if (w.trusted) console.log(` Witness statement, NOT COUNTED: from a witness you named as trusted (key set ${w.witness}), but it was not counted (see the notes beside the seal)`);
|
|
310
|
+
else console.log(` Witness statement, NOT COUNTED: from a witness you did not name as trusted (key set ${w.witness})`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
289
314
|
function printCancellation(e) {
|
|
290
315
|
console.log(`\nEntry ${e.index}: CANCELLATION ${e.fingerprint ?? ''}`);
|
|
291
316
|
if (hidden(e)) console.log(hidden(e));
|
|
@@ -313,8 +338,53 @@ function printAcknowledgement(e) {
|
|
|
313
338
|
if (e.signatures && e.signatures.length) console.log(` Agent's signatures: ${e.signatures.map((s) => `${s.method} ${s.state}`).join('; ')}`);
|
|
314
339
|
}
|
|
315
340
|
|
|
341
|
+
// A denial: the agent's own side says it did not take an action, and why.
|
|
342
|
+
function printDenial(e) {
|
|
343
|
+
console.log(`\nEntry ${e.index}: DENIAL ${e.fingerprint ?? ''}`);
|
|
344
|
+
if (hidden(e)) console.log(hidden(e));
|
|
345
|
+
else {
|
|
346
|
+
const c = e.content;
|
|
347
|
+
const amount = c.amount ? `, ${c.amount.value} ${safe(c.amount.unit)}` : '';
|
|
348
|
+
const service = c.with ? `, with [${safe(c.with)}]` : '';
|
|
349
|
+
console.log(` ${c.pass ? `The helper agent of pass ${c.pass}` : 'The slip\'s own agent'} states that it did not take an action`);
|
|
350
|
+
console.log(` Under slip ${c.slip}`);
|
|
351
|
+
console.log(` ${c.when} (the agent's side's own word): not taken ${safe(c.action)}${amount}${service}`);
|
|
352
|
+
if (c.details) console.log(` Documents named: ${c.details.length}, by their fingerprints only`);
|
|
353
|
+
console.log(` Who said no: ${DENIAL_SOURCES[c.source]}`);
|
|
354
|
+
if (c.codes) console.log(` Reasons given: ${c.codes.join(', ')}`);
|
|
355
|
+
console.log(' This shows what the agent\'s side said. It is not counted against the agent, and it does not show that the action was not taken in some other way.');
|
|
356
|
+
}
|
|
357
|
+
if (e.signatures && e.signatures.length) console.log(` Agent's signatures: ${e.signatures.map((s) => `${s.method} ${s.state}`).join('; ')}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// A seal held elsewhere, handed over beside the book.
|
|
361
|
+
function printHeldSeal(h, n) {
|
|
362
|
+
console.log(`\nHanded over beside the book, ${n}: SEAL HELD ELSEWHERE ${h.fingerprint ?? ''}`);
|
|
363
|
+
if (h.problems.length) {
|
|
364
|
+
console.log(' It did not pass its check, so it was not used. See the problems at the top.');
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// What a seal says is shown only where this device checked at least one of its signatures.
|
|
368
|
+
if (h.verified === 'none') {
|
|
369
|
+
console.log(' What this seal says is not shown, because this device could not check its signatures.');
|
|
370
|
+
for (const n of h.notes) console.log(` Note: ${n}`);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const c = h.content;
|
|
374
|
+
console.log(` Covers the first ${c.size} entries, with the top fingerprint ${c.root}, and stands at entry ${h.place}`);
|
|
375
|
+
console.log(` Sealed at (the recorder's own word): ${c.when}`);
|
|
376
|
+
console.log(` Recorder: ${label(c.by.name)} (a label, not checked)`);
|
|
377
|
+
console.log(` Recorder's key set fingerprint: ${h.sealer}`);
|
|
378
|
+
printStamps(h.stamps);
|
|
379
|
+
printWitnesses(h.witnesses);
|
|
380
|
+
console.log(h.used ? (h.inBook === null ? ' In the book at that place: NO' : ` In the book at that place: yes, at entry ${h.inBook}`) : ' In the book at that place: not compared, because it was not used');
|
|
381
|
+
console.log(` Recorder's signatures: ${h.signatures.map((s) => `${s.method} ${s.state}`).join('; ')}`);
|
|
382
|
+
for (const n of h.notes) console.log(` Note: ${n}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
316
385
|
// A cancellation the person kept, handed over beside the book.
|
|
317
386
|
function printHeld(h, n) {
|
|
387
|
+
if (h.kind === 'seal') return printHeldSeal(h, n);
|
|
318
388
|
console.log(`\nHanded over beside the book, ${n}: CANCELLATION ${h.fingerprint ?? ''}`);
|
|
319
389
|
if (h.problems.length) {
|
|
320
390
|
console.log(' It did not pass its check, so it was not used. See the problems at the top.');
|
|
@@ -403,6 +473,7 @@ function print(result, isShow) {
|
|
|
403
473
|
else if (e.kind === 'seal') printSeal(e);
|
|
404
474
|
else if (e.kind === 'cancellation') printCancellation(e);
|
|
405
475
|
else if (e.kind === 'acknowledgement') printAcknowledgement(e);
|
|
476
|
+
else if (e.kind === 'denial') printDenial(e);
|
|
406
477
|
else if (e.kind === 'pass') printPass(e);
|
|
407
478
|
else if (e.kind === 'vouching') printVouching(e);
|
|
408
479
|
else if (e.kind === 'withdrawal') printWithdrawal(e);
|
|
@@ -436,7 +507,13 @@ function print(result, isShow) {
|
|
|
436
507
|
if (!s.problemFound) {
|
|
437
508
|
const stamped = s.sealed ? `yes: the first ${s.sealed.entries} entries existed by ${s.sealed.when}` : 'no';
|
|
438
509
|
console.log(` Time-stamped in a way you trust? ${stamped}`);
|
|
510
|
+
if (s.counts.seals || args.witnesses.length) console.log(` Witnessed by a witness you trust? ${witnessedWords(s)}${isShow && s.witnessed ? ' (single pages: this says nothing about totals over the whole book)' : ''}`);
|
|
439
511
|
console.log(` Checked against keys you named? ${trustWords(s)}`);
|
|
512
|
+
// Shown only where the person named an address: a permission signed
|
|
513
|
+
// elsewhere is a problem, reported above, so here every one was signed
|
|
514
|
+
// at an address they named.
|
|
515
|
+
if (args.issuerOrigins.length) console.log(` Signed at an address you named? yes: every permission was signed at ${[...new Set(args.issuerOrigins)].join(' or ')}`);
|
|
516
|
+
if (args.seals.length) console.log(` Seals held elsewhere compared? ${heldWords(result)}`);
|
|
440
517
|
}
|
|
441
518
|
console.log(` Slips ${s.counts.slips}, stubs ${s.counts.stubs} (countersigned ${s.counts.countersigned}, one-sided ${s.counts.oneSided})${s.counts.seals ? `, seals ${s.counts.seals}` : ''}`);
|
|
442
519
|
if (s.counts.cancellations || s.counts.acknowledgements || s.counts.vouchings || s.counts.passes) {
|
|
@@ -445,15 +522,39 @@ function print(result, isShow) {
|
|
|
445
522
|
if (s.counts.approved || s.counts.refusals || s.counts.terms) {
|
|
446
523
|
console.log(` Approved by the person ${s.counts.approved}, refusals ${s.counts.refusals}, terms for agents ${s.counts.terms}`);
|
|
447
524
|
}
|
|
525
|
+
if (s.counts.denials) console.log(` Denials (the agent's side's own word, not counted against it) ${s.counts.denials}`);
|
|
448
526
|
console.log('\nWhat this check does not show');
|
|
449
527
|
for (const l of s.limits) console.log(` - ${l}`);
|
|
450
528
|
}
|
|
451
529
|
|
|
452
530
|
// Whether the check rests on keys the person named: the passkey they trust
|
|
453
|
-
// and, where the record has a seal
|
|
454
|
-
//
|
|
531
|
+
// and, where the record has a seal or seals held elsewhere were handed over,
|
|
532
|
+
// the recorder they expect. Without them a record agrees only with the keys
|
|
533
|
+
// inside it, which anyone could have made, and a seal handed over is not
|
|
534
|
+
// compared with the book.
|
|
455
535
|
function named(s) {
|
|
456
|
-
return args.issuerKeys.length > 0 && (s.counts.seals === 0 || args.sealKeys.length > 0);
|
|
536
|
+
return args.issuerKeys.length > 0 && ((s.counts.seals === 0 && args.seals.length === 0) || args.sealKeys.length > 0);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// How far a witness the person named reaches. It excuses nothing, and is not needed for exit code 0.
|
|
540
|
+
function witnessedWords(s) {
|
|
541
|
+
if (!s.witnessed) return args.witnesses.length ? 'no' : 'no (you named no witness you trust, --witness)';
|
|
542
|
+
const w = s.witnessed;
|
|
543
|
+
return `yes: the seal at entry ${w.seal}, which covers the first ${w.entries} entries (${w.seals.length} of ${s.counts.seals} seals witnessed)`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Whether the seals held elsewhere that were handed over were compared with the book.
|
|
547
|
+
function heldWords(result) {
|
|
548
|
+
if (args.sealKeys.length === 0) return 'NO: you named no recorder you expect (--sealer), so the seals handed over were not compared with the book. This is not a pass.';
|
|
549
|
+
const seals = (result.held ?? []).filter((h) => h.kind === 'seal');
|
|
550
|
+
const used = seals.filter((h) => h.used).length;
|
|
551
|
+
return used === seals.length ? 'yes' : `NO: ${seals.length - used} of ${seals.length} handed over were not compared with the book (see the note beside each one). This is not a pass.`;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Whether every seal handed over beside the book was compared with it. One that was not (it gives another book
|
|
555
|
+
// number, or could not be used) gives no pass, as one handed over with no recorder named does not.
|
|
556
|
+
function heldAllCompared(result) {
|
|
557
|
+
return (result.held ?? []).filter((h) => h.kind === 'seal').every((h) => h.used);
|
|
457
558
|
}
|
|
458
559
|
|
|
459
560
|
function trustWords(s) {
|
|
@@ -461,6 +562,7 @@ function trustWords(s) {
|
|
|
461
562
|
const recorders = new Set(args.sealKeys).size === 1 ? 'the recorder you expect' : 'the recorders you expect';
|
|
462
563
|
if (named(s)) return s.counts.seals ? `yes: ${passkeys}, and ${recorders}` : `yes: ${passkeys}`;
|
|
463
564
|
if (args.issuerKeys.length === 0) return 'NO: you named no passkey you trust (--issuer), so anyone could have made this record. This is not a pass.';
|
|
565
|
+
if (s.counts.seals === 0) return `NO: you named ${passkeys}, but no recorder you expect (--sealer) for the seals handed over beside the book. This is not a pass.`;
|
|
464
566
|
return 'NO: the record has a seal, and you named no recorder you expect (--sealer), so anyone could have sealed it. This is not a pass.';
|
|
465
567
|
}
|
|
466
568
|
|
|
@@ -516,10 +618,12 @@ try {
|
|
|
516
618
|
|
|
517
619
|
const options = {};
|
|
518
620
|
if (args.issuerKeys.length) options.issuerKeys = args.issuerKeys;
|
|
621
|
+
if (args.issuerOrigins.length) options.issuerOrigins = args.issuerOrigins;
|
|
519
622
|
if (args.sealKeys.length) options.sealKeys = args.sealKeys;
|
|
520
623
|
if (args.stampServices.length) options.stampServices = args.stampServices;
|
|
521
624
|
if (args.blocks.length) options.blocks = args.blocks;
|
|
522
625
|
if (args.vouchers.length) options.vouchers = args.vouchers;
|
|
626
|
+
if (args.witnesses.length) options.witnesses = args.witnesses;
|
|
523
627
|
if (args.disclosures !== undefined) {
|
|
524
628
|
try {
|
|
525
629
|
options.disclosures = JSON.parse(readFileSync(args.disclosures, 'utf8'));
|
|
@@ -539,6 +643,17 @@ if (args.cancellations.length) {
|
|
|
539
643
|
}
|
|
540
644
|
}
|
|
541
645
|
}
|
|
646
|
+
if (args.seals.length) {
|
|
647
|
+
options.seals = [];
|
|
648
|
+
for (const file of args.seals) {
|
|
649
|
+
try {
|
|
650
|
+
options.seals.push(JSON.parse(readFileSync(file, 'utf8')));
|
|
651
|
+
} catch {
|
|
652
|
+
console.error(`The file of a seal could not be read: ${file}`);
|
|
653
|
+
process.exit(2);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
542
657
|
if (args.expectedRoot !== undefined) options.expectedRoot = args.expectedRoot;
|
|
543
658
|
if (args.expectedSize !== undefined) options.expectedSize = args.expectedSize;
|
|
544
659
|
|
|
@@ -594,7 +709,7 @@ try {
|
|
|
594
709
|
if (chain) printChain(chain, fromChain);
|
|
595
710
|
}
|
|
596
711
|
const s = result.summary;
|
|
597
|
-
code = s.problemFound ? 2 : s.intact && s.withinSlips && named(s) ? 0 : 1;
|
|
712
|
+
code = s.problemFound ? 2 : s.intact && s.withinSlips && named(s) && heldAllCompared(result) ? 0 : 1;
|
|
598
713
|
} catch {
|
|
599
714
|
// The checker is written never to throw. If it ever does, that is not a pass.
|
|
600
715
|
console.error('The check could not be completed. This is not a pass.');
|