provared 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -86,6 +86,9 @@ agent = create_agent(model, tools=[order_supplies, send_message],
86
86
  })]) # last among the middleware that wrap tool calls
87
87
  ```
88
88
 
89
+ The package is listed among the middleware in
90
+ [LangChain's own documentation](https://docs.langchain.com/oss/python/integrations/middleware).
91
+
89
92
  ## The four parts
90
93
 
91
94
  | Part | What it is |
@@ -125,7 +128,7 @@ each part protects against, is in
125
128
 
126
129
  - **[The guide](docs/guide.md)**: the whole library, in one document.
127
130
  Installing and importing; the command-line checker; what a slip can
128
- say; seals, time-stamps and the chain of block headers; cancelling;
131
+ say; seals, time-stamps, witnesses and the chain of block headers; cancelling;
129
132
  who stands behind a name; covered fields; passing a slip on; the check
130
133
  before acting; the stub writer beside an agent and the connector for
131
134
  its tools; what the checker returns; what it never does; what is not
@@ -156,9 +159,9 @@ each part protects against, is in
156
159
 
157
160
  ## Status
158
161
 
159
- **Version 0.5.0, a draft.** Published as it is, by one author in their
162
+ **Version 0.6.0, a draft.** Published as it is, by one author in their
160
163
  own time, built with AI tools since 3 October 2026; fixes as time
161
- allows. The code has been through 50 rounds of review by AI reviewers,
164
+ allows. The code has been through more than 80 rounds of review by AI reviewers,
162
165
  each reviewer a fresh session given the code, the documents and a brief
163
166
  of what to look at; every fault was fixed, or stated as a limit, and
164
167
  kept as a test. No person other than the author has reviewed it yet.
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // provared-check <file> [--issuer <thumbprint>]... [--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, 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.
@@ -31,7 +31,8 @@ process.on('warning', (w) => {
31
31
 
32
32
  const USAGE = `Usage: provared-check <file> [--issuer <thumbprint>]... [--sealer <fingerprint>]... [--stamp-service <fingerprint>]...
33
33
  [--block <fingerprint>]... [--voucher <fingerprint>]... [--disclosures <file>] [--cancellation <file>]...
34
- [--root <top fingerprint>] [--size <entries>] [--headers <file>] [--json]
34
+ [--seal <file>]... [--witness <fingerprint>]... [--root <top fingerprint>] [--size <entries>]
35
+ [--headers <file>] [--json]
35
36
  provared-check --sample [--json]
36
37
 
37
38
  <file> a book (one entry to a line) or a Show (one JSON object)
@@ -47,6 +48,12 @@ const USAGE = `Usage: provared-check <file> [--issuer <thumbprint>]... [--sealer
47
48
  --cancellation a JSON file of the person's own copy of a cancellation, as the entry it would be in a book:
48
49
  {"cancellation": {...}, "stamps": [...]}. It counts even where the book leaves it out; may be repeated.
49
50
  It may hold "acknowledgements": what the agent's side signed when it was handed the cancellation
51
+ --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:
52
+ {"seal": {...}, "stamps": [...]}. The book must hold that seal at the place it states: one that ends
53
+ before it was cut short. It counts only with --sealer naming its recorder; may be repeated.
54
+ It may hold "witnesses": statements that witnesses signed for that seal, such as ones that came later
55
+ --witness the fingerprint of the key set of a witness you trust: a seal counts as witnessed only by such a
56
+ witness. Being witnessed excuses nothing; may be repeated
50
57
  --root the top fingerprint you already trust
51
58
  --size the number of entries you already trust; keep it with the top fingerprint
52
59
  --headers a file of Bitcoin block headers, as provared-headers keeps it. It is checked here, from the chain's
@@ -61,14 +68,14 @@ const FINGERPRINT = /^[A-Za-z0-9_-]{43}$/;
61
68
  const BLOCK = /^[0-9a-f]{64}$/;
62
69
 
63
70
  // 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' };
71
+ const LISTS = { '--issuer': 'issuerKeys', '--sealer': 'sealKeys', '--stamp-service': 'stampServices', '--block': 'blocks', '--voucher': 'vouchers', '--witness': 'witnesses', '--cancellation': 'cancellations', '--seal': 'seals' };
65
72
  const ONCE = { '--disclosures': 'disclosures', '--root': 'expectedRoot', '--size': 'expectedSize', '--headers': 'headers' };
66
73
 
67
74
  // The arguments, or the reason they cannot be used. A value that is not a
68
75
  // fingerprint is refused, never set aside: a typing error must not quietly
69
76
  // leave a time-stamp or a name uncounted.
70
77
  function parseArguments(argv) {
71
- const out = { file: null, sample: false, issuerKeys: [], sealKeys: [], stampServices: [], blocks: [], vouchers: [], cancellations: [], disclosures: undefined, expectedRoot: undefined, expectedSize: undefined, json: false };
78
+ const out = { file: null, sample: false, issuerKeys: [], sealKeys: [], stampServices: [], blocks: [], vouchers: [], witnesses: [], cancellations: [], seals: [], disclosures: undefined, expectedRoot: undefined, expectedSize: undefined, json: false };
72
79
  for (let i = 0; i < argv.length; i++) {
73
80
  const a = argv[i];
74
81
  if (a === '--help' || a === '-h') return { help: true };
@@ -82,7 +89,7 @@ function parseArguments(argv) {
82
89
  if (i + 1 >= argv.length) return { error: `${a} needs a value.` };
83
90
  const value = argv[++i];
84
91
  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)) {
92
+ if (['--issuer', '--sealer', '--stamp-service', '--voucher', '--witness', '--root'].includes(a) && !FINGERPRINT.test(value)) {
86
93
  return { error: `The value of ${a} is not a fingerprint. A fingerprint is 43 letters, digits, hyphens or underscores, with no spaces.` };
87
94
  }
88
95
  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.' };
@@ -94,7 +101,7 @@ function parseArguments(argv) {
94
101
  }
95
102
  if (out.sample) {
96
103
  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']) {
104
+ for (const name of ['issuerKeys', 'sealKeys', 'stampServices', 'blocks', 'vouchers', 'witnesses', 'cancellations', 'seals']) {
98
105
  if (out[name].length) return { error: '--sample names what to trust itself: give no other option with it but --json.' };
99
106
  }
100
107
  if (out.disclosures !== undefined || out.expectedRoot !== undefined || out.expectedSize !== undefined || out.headers !== undefined) {
@@ -282,10 +289,22 @@ function printSeal(e) {
282
289
  console.log(` Recorder's key set fingerprint: ${e.sealer}`);
283
290
  printStamps(e.stamps);
284
291
  if (e.stamps.length === 0) console.log(' No outside time-stamp: the time is only the recorder\'s own word.');
292
+ printWitnesses(e.witnesses);
285
293
  }
286
294
  if (e.signatures && e.signatures.length) console.log(` Recorder's signatures: ${e.signatures.map((s) => `${s.method} ${s.state}`).join('; ')}`);
287
295
  }
288
296
 
297
+ // The witness statements beside a seal. What a statement says is shown only where it passed its check.
298
+ function printWitnesses(list) {
299
+ for (const w of list ?? []) {
300
+ if (w.state === 'invalid') console.log(' Witness statement, NOT USED: it did not pass its check (see the problems)');
301
+ else if (w.state === 'unavailable') console.log(' Witness statement, NOT CHECKED: this device cannot check both of its signatures');
302
+ 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)`);
303
+ 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)`);
304
+ else console.log(` Witness statement, NOT COUNTED: from a witness you did not name as trusted (key set ${w.witness})`);
305
+ }
306
+ }
307
+
289
308
  function printCancellation(e) {
290
309
  console.log(`\nEntry ${e.index}: CANCELLATION ${e.fingerprint ?? ''}`);
291
310
  if (hidden(e)) console.log(hidden(e));
@@ -313,8 +332,53 @@ function printAcknowledgement(e) {
313
332
  if (e.signatures && e.signatures.length) console.log(` Agent's signatures: ${e.signatures.map((s) => `${s.method} ${s.state}`).join('; ')}`);
314
333
  }
315
334
 
335
+ // A denial: the agent's own side says it did not take an action, and why.
336
+ function printDenial(e) {
337
+ console.log(`\nEntry ${e.index}: DENIAL ${e.fingerprint ?? ''}`);
338
+ if (hidden(e)) console.log(hidden(e));
339
+ else {
340
+ const c = e.content;
341
+ const amount = c.amount ? `, ${c.amount.value} ${safe(c.amount.unit)}` : '';
342
+ const service = c.with ? `, with [${safe(c.with)}]` : '';
343
+ console.log(` ${c.pass ? `The helper agent of pass ${c.pass}` : 'The slip\'s own agent'} states that it did not take an action`);
344
+ console.log(` Under slip ${c.slip}`);
345
+ console.log(` ${c.when} (the agent's side's own word): not taken ${safe(c.action)}${amount}${service}`);
346
+ if (c.details) console.log(` Documents named: ${c.details.length}, by their fingerprints only`);
347
+ console.log(` Who said no: ${DENIAL_SOURCES[c.source]}`);
348
+ if (c.codes) console.log(` Reasons given: ${c.codes.join(', ')}`);
349
+ 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.');
350
+ }
351
+ if (e.signatures && e.signatures.length) console.log(` Agent's signatures: ${e.signatures.map((s) => `${s.method} ${s.state}`).join('; ')}`);
352
+ }
353
+
354
+ // A seal held elsewhere, handed over beside the book.
355
+ function printHeldSeal(h, n) {
356
+ console.log(`\nHanded over beside the book, ${n}: SEAL HELD ELSEWHERE ${h.fingerprint ?? ''}`);
357
+ if (h.problems.length) {
358
+ console.log(' It did not pass its check, so it was not used. See the problems at the top.');
359
+ return;
360
+ }
361
+ // What a seal says is shown only where this device checked at least one of its signatures.
362
+ if (h.verified === 'none') {
363
+ console.log(' What this seal says is not shown, because this device could not check its signatures.');
364
+ for (const n of h.notes) console.log(` Note: ${n}`);
365
+ return;
366
+ }
367
+ const c = h.content;
368
+ console.log(` Covers the first ${c.size} entries, with the top fingerprint ${c.root}, and stands at entry ${h.place}`);
369
+ console.log(` Sealed at (the recorder's own word): ${c.when}`);
370
+ console.log(` Recorder: ${label(c.by.name)} (a label, not checked)`);
371
+ console.log(` Recorder's key set fingerprint: ${h.sealer}`);
372
+ printStamps(h.stamps);
373
+ printWitnesses(h.witnesses);
374
+ 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');
375
+ console.log(` Recorder's signatures: ${h.signatures.map((s) => `${s.method} ${s.state}`).join('; ')}`);
376
+ for (const n of h.notes) console.log(` Note: ${n}`);
377
+ }
378
+
316
379
  // A cancellation the person kept, handed over beside the book.
317
380
  function printHeld(h, n) {
381
+ if (h.kind === 'seal') return printHeldSeal(h, n);
318
382
  console.log(`\nHanded over beside the book, ${n}: CANCELLATION ${h.fingerprint ?? ''}`);
319
383
  if (h.problems.length) {
320
384
  console.log(' It did not pass its check, so it was not used. See the problems at the top.');
@@ -403,6 +467,7 @@ function print(result, isShow) {
403
467
  else if (e.kind === 'seal') printSeal(e);
404
468
  else if (e.kind === 'cancellation') printCancellation(e);
405
469
  else if (e.kind === 'acknowledgement') printAcknowledgement(e);
470
+ else if (e.kind === 'denial') printDenial(e);
406
471
  else if (e.kind === 'pass') printPass(e);
407
472
  else if (e.kind === 'vouching') printVouching(e);
408
473
  else if (e.kind === 'withdrawal') printWithdrawal(e);
@@ -436,7 +501,9 @@ function print(result, isShow) {
436
501
  if (!s.problemFound) {
437
502
  const stamped = s.sealed ? `yes: the first ${s.sealed.entries} entries existed by ${s.sealed.when}` : 'no';
438
503
  console.log(` Time-stamped in a way you trust? ${stamped}`);
504
+ 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
505
  console.log(` Checked against keys you named? ${trustWords(s)}`);
506
+ if (args.seals.length) console.log(` Seals held elsewhere compared? ${heldWords(result)}`);
440
507
  }
441
508
  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
509
  if (s.counts.cancellations || s.counts.acknowledgements || s.counts.vouchings || s.counts.passes) {
@@ -445,15 +512,39 @@ function print(result, isShow) {
445
512
  if (s.counts.approved || s.counts.refusals || s.counts.terms) {
446
513
  console.log(` Approved by the person ${s.counts.approved}, refusals ${s.counts.refusals}, terms for agents ${s.counts.terms}`);
447
514
  }
515
+ if (s.counts.denials) console.log(` Denials (the agent's side's own word, not counted against it) ${s.counts.denials}`);
448
516
  console.log('\nWhat this check does not show');
449
517
  for (const l of s.limits) console.log(` - ${l}`);
450
518
  }
451
519
 
452
520
  // Whether the check rests on keys the person named: the passkey they trust
453
- // and, where the record has a seal, the recorder they expect. Without them a
454
- // record agrees only with the keys inside it, which anyone could have made.
521
+ // and, where the record has a seal or seals held elsewhere were handed over,
522
+ // the recorder they expect. Without them a record agrees only with the keys
523
+ // inside it, which anyone could have made, and a seal handed over is not
524
+ // compared with the book.
455
525
  function named(s) {
456
- return args.issuerKeys.length > 0 && (s.counts.seals === 0 || args.sealKeys.length > 0);
526
+ return args.issuerKeys.length > 0 && ((s.counts.seals === 0 && args.seals.length === 0) || args.sealKeys.length > 0);
527
+ }
528
+
529
+ // How far a witness the person named reaches. It excuses nothing, and is not needed for exit code 0.
530
+ function witnessedWords(s) {
531
+ if (!s.witnessed) return args.witnesses.length ? 'no' : 'no (you named no witness you trust, --witness)';
532
+ const w = s.witnessed;
533
+ return `yes: the seal at entry ${w.seal}, which covers the first ${w.entries} entries (${w.seals.length} of ${s.counts.seals} seals witnessed)`;
534
+ }
535
+
536
+ // Whether the seals held elsewhere that were handed over were compared with the book.
537
+ function heldWords(result) {
538
+ 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.';
539
+ const seals = (result.held ?? []).filter((h) => h.kind === 'seal');
540
+ const used = seals.filter((h) => h.used).length;
541
+ 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.`;
542
+ }
543
+
544
+ // Whether every seal handed over beside the book was compared with it. One that was not (it gives another book
545
+ // number, or could not be used) gives no pass, as one handed over with no recorder named does not.
546
+ function heldAllCompared(result) {
547
+ return (result.held ?? []).filter((h) => h.kind === 'seal').every((h) => h.used);
457
548
  }
458
549
 
459
550
  function trustWords(s) {
@@ -461,6 +552,7 @@ function trustWords(s) {
461
552
  const recorders = new Set(args.sealKeys).size === 1 ? 'the recorder you expect' : 'the recorders you expect';
462
553
  if (named(s)) return s.counts.seals ? `yes: ${passkeys}, and ${recorders}` : `yes: ${passkeys}`;
463
554
  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.';
555
+ 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
556
  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
557
  }
466
558
 
@@ -520,6 +612,7 @@ if (args.sealKeys.length) options.sealKeys = args.sealKeys;
520
612
  if (args.stampServices.length) options.stampServices = args.stampServices;
521
613
  if (args.blocks.length) options.blocks = args.blocks;
522
614
  if (args.vouchers.length) options.vouchers = args.vouchers;
615
+ if (args.witnesses.length) options.witnesses = args.witnesses;
523
616
  if (args.disclosures !== undefined) {
524
617
  try {
525
618
  options.disclosures = JSON.parse(readFileSync(args.disclosures, 'utf8'));
@@ -539,6 +632,17 @@ if (args.cancellations.length) {
539
632
  }
540
633
  }
541
634
  }
635
+ if (args.seals.length) {
636
+ options.seals = [];
637
+ for (const file of args.seals) {
638
+ try {
639
+ options.seals.push(JSON.parse(readFileSync(file, 'utf8')));
640
+ } catch {
641
+ console.error(`The file of a seal could not be read: ${file}`);
642
+ process.exit(2);
643
+ }
644
+ }
645
+ }
542
646
  if (args.expectedRoot !== undefined) options.expectedRoot = args.expectedRoot;
543
647
  if (args.expectedSize !== undefined) options.expectedSize = args.expectedSize;
544
648
 
@@ -594,7 +698,7 @@ try {
594
698
  if (chain) printChain(chain, fromChain);
595
699
  }
596
700
  const s = result.summary;
597
- code = s.problemFound ? 2 : s.intact && s.withinSlips && named(s) ? 0 : 1;
701
+ code = s.problemFound ? 2 : s.intact && s.withinSlips && named(s) && heldAllCompared(result) ? 0 : 1;
598
702
  } catch {
599
703
  // The checker is written never to throw. If it ever does, that is not a pass.
600
704
  console.error('The check could not be completed. This is not a pass.');