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/page/render.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // is kept inside its own box, so that a record cannot hide, reorder or
8
8
  // draw over the words around it.
9
9
 
10
- import { REFUSAL_REASONS, conditionWords, limitWords, neverWords } from '../src/check.js';
10
+ import { DENIAL_SOURCES, REFUSAL_REASONS, conditionWords, isPageAddress, limitWords, neverWords } from '../src/check.js';
11
11
 
12
12
  // The same holds for characters that cannot be seen (zero-width characters,
13
13
  // soft hyphens, line separators, fillers), with which two different labels
@@ -54,6 +54,20 @@ export function readBlocks(text) {
54
54
  return { values: lines.filter((line) => /^[0-9a-fA-F]{64}$/.test(line)), bad: lines.filter((line) => !/^[0-9a-fA-F]{64}$/.test(line)) };
55
55
  }
56
56
 
57
+ /**
58
+ * Read the box of page addresses, one to a line: the addresses at which the
59
+ * person expects the permissions to have been signed, each in the one form
60
+ * the format allows (https://, a website name in lower case, a port only if
61
+ * it is needed, and nothing more). As with the other boxes, a line that is
62
+ * not one is never dropped silently.
63
+ * @param {string} text
64
+ * @returns {{values: string[], bad: string[]}}
65
+ */
66
+ export function readOrigins(text) {
67
+ const lines = String(text).split(/\s+/).filter((line) => line !== '');
68
+ return { values: lines.filter((line) => isPageAddress(line)), bad: lines.filter((line) => !isPageAddress(line)) };
69
+ }
70
+
57
71
  /**
58
72
  * @param {unknown} text
59
73
  * @returns {string} the text, safe to place among other words
@@ -82,6 +96,17 @@ export function el(tag, className, text) {
82
96
 
83
97
  const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
84
98
 
99
+ /**
100
+ * The heading of the card of the top fingerprint. It names the Seal only
101
+ * where the record holds one: a book, or the pages of a Show, with no seal
102
+ * has a top fingerprint and no seal.
103
+ * @param {{summary?: {counts?: {seals?: number}}}} result the checker's answer
104
+ * @returns {string}
105
+ */
106
+ export function topFingerprintHeading(result) {
107
+ return (result?.summary?.counts?.seals ?? 0) > 0 ? 'The top fingerprint (the Seal)' : 'The top fingerprint';
108
+ }
109
+
85
110
  /**
86
111
  * A time from a record, in words. The checker has already confirmed its
87
112
  * form; anything else is shown as it is.
@@ -149,6 +174,17 @@ function stampRows(list, stamps) {
149
174
  }
150
175
  }
151
176
 
177
+ // The witness statements beside a seal. What a statement says is shown only where it passed its check.
178
+ function witnessRows(list, witnesses) {
179
+ for (const w of witnesses ?? []) {
180
+ if (w.state === 'invalid') row(list, 'Witness statement, not used', 'it did not pass its check (see the problems)');
181
+ else if (w.state === 'unavailable') row(list, 'Witness statement, not checked', 'this browser cannot check both of its signatures');
182
+ else if (w.counted) row(list, 'Witnessed', `by ${label(w.name)} (a label), a witness you named as trusted (key set ${w.witness}), at ${showTime(w.when)} (the witness's own word)`);
183
+ else if (w.trusted) row(list, 'Witness statement, not counted', `from a witness you named as trusted (key set ${w.witness}), but it was not counted (see the notes on this card)`);
184
+ else row(list, 'Witness statement, not counted', `from a witness you did not name as trusted (key set ${w.witness})`);
185
+ }
186
+ }
187
+
152
188
  function cancellationCard(e) {
153
189
  const card = el('div', `card slip${e.problems.length ? ' broken' : ''}`);
154
190
  card.append(el('h3', 'kind', `Entry ${e.index}: Cancellation (the person ends a slip)`));
@@ -188,8 +224,39 @@ function acknowledgementCard(e) {
188
224
  return card;
189
225
  }
190
226
 
227
+ // A seal held elsewhere, handed over beside the book.
228
+ function heldSealCard(h, n) {
229
+ const card = el('div', `card seal${h.problems.length ? ' broken' : ''}`);
230
+ card.append(el('h3', 'kind', `Handed over beside the book, ${n}: a seal of the book held elsewhere`));
231
+ if (h.problems.length) {
232
+ card.append(el('p', 'quiet', 'It did not pass its check, so it was not used. See the problems above.'));
233
+ return card;
234
+ }
235
+ if (h.verified === 'none') {
236
+ card.append(el('p', 'quiet', 'What this seal says is not shown, because this device could not check its signatures.'));
237
+ } else {
238
+ const c = h.content;
239
+ const list = el('dl');
240
+ row(list, 'Covers', `the first ${c.size} entries`);
241
+ row(list, 'Stands at', `entry ${h.place}`);
242
+ row(list, 'Top fingerprint', c.root, true);
243
+ row(list, 'Sealed at', `${showTime(c.when)} (the recorder's own word)`);
244
+ row(list, 'Recorder', `${label(c.by.name)} (a label; it is not checked)`);
245
+ row(list, 'Recorder\'s key set', h.sealer, true);
246
+ row(list, 'Recorder\'s signatures', signatureWords(h.signatures));
247
+ stampRows(list, h.stamps);
248
+ witnessRows(list, h.witnesses);
249
+ row(list, 'In the book at that place', h.used ? (h.inBook === null ? 'no' : `yes, at entry ${h.inBook}`) : 'not compared, because it was not used');
250
+ row(list, 'Fingerprint', h.fingerprint, true);
251
+ card.append(list);
252
+ }
253
+ for (const note of h.notes) flag(card, 'note', note);
254
+ return card;
255
+ }
256
+
191
257
  // A cancellation the person kept, handed over beside the book.
192
258
  function heldCard(h, n) {
259
+ if (h.kind === 'seal') return heldSealCard(h, n);
193
260
  const card = el('div', `card slip${h.problems.length ? ' broken' : ''}`);
194
261
  card.append(el('h3', 'kind', `Handed over beside the book, ${n}: the person's own copy of a cancellation`));
195
262
  if (h.problems.length) {
@@ -365,6 +432,29 @@ function refusalCard(e) {
365
432
  return card;
366
433
  }
367
434
 
435
+ // A denial: the agent's own side says it did not take an action, and why.
436
+ function denialCard(e) {
437
+ const card = el('div', `card refusal${e.problems.length ? ' broken' : ''}`);
438
+ card.append(el('h3', 'kind', `Entry ${e.index}: Denial (the agent's side says it did not take an action)`));
439
+ if (hidden(e)) {
440
+ card.append(el('p', 'quiet', hidden(e)));
441
+ return card;
442
+ }
443
+ const c = e.content;
444
+ const list = el('dl');
445
+ row(list, 'From', c.pass ? `the helper agent of pass ${c.pass}` : 'the slip\'s own agent');
446
+ row(list, 'When', `${showTime(c.when)} (the agent's side's own word)`);
447
+ row(list, 'Not taken', c.action + (c.amount ? `, ${c.amount.value} ${c.amount.unit}` : '') + (c.with ? `, with [${c.with}]` : ''));
448
+ if (c.details) row(list, 'Documents named', `${c.details.length}, by their fingerprints only`);
449
+ row(list, 'Who said no', DENIAL_SOURCES[c.source]);
450
+ if (c.codes) row(list, 'Reasons given', c.codes.join(', '));
451
+ row(list, 'Agent\'s signatures', signatureWords(e.signatures));
452
+ row(list, 'Fingerprint', e.fingerprint, true);
453
+ card.append(list);
454
+ flag(card, 'onesided', '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.');
455
+ return card;
456
+ }
457
+
368
458
  function sealCard(e) {
369
459
  const card = el('div', `card seal${e.problems.length ? ' broken' : ''}`);
370
460
  card.append(el('h3', 'kind', `Entry ${e.index}: Seal (the book, signed by whoever keeps it)`));
@@ -381,6 +471,7 @@ function sealCard(e) {
381
471
  row(list, 'Recorder\'s key set', `${e.sealer}. Compare this fingerprint with the one the recorder gave you.`, true);
382
472
  row(list, 'Recorder\'s signatures', signatureWords(e.signatures));
383
473
  stampRows(list, e.stamps);
474
+ witnessRows(list, e.witnesses);
384
475
  row(list, 'Fingerprint', e.fingerprint, true);
385
476
  card.append(list);
386
477
  if (e.stamps.length === 0) flag(card, 'onesided', 'There is no outside time-stamp: the time is only the recorder\'s own word.');
@@ -417,15 +508,79 @@ function termsCard(e) {
417
508
  * @param {object} [o.options] what the check was told to trust: the green answer needs the passkey the
418
509
  * person trusts and, where the record has a seal, the recorder they expect
419
510
  */
511
+ // A copy of a cancellation, a seal, or a file of disclosures, is small.
512
+ const MAX_HANDED_BYTES = 1000000;
513
+ const MAX_COPIES = 16;
514
+ const MAX_SEALS = 64;
515
+
516
+ async function jsonFrom(file, what) {
517
+ if (file.size > MAX_HANDED_BYTES) throw new Error(`The file of ${what}, ${shown(file.name)}, is too large. Nothing was checked.`);
518
+ try {
519
+ return JSON.parse(await file.text());
520
+ } catch {
521
+ throw new Error(`The file of ${what}, ${shown(file.name)}, is not JSON. Nothing was checked. Choose another file, or clear these files.`);
522
+ }
523
+ }
524
+
525
+ /**
526
+ * What the person was handed with the record: their own copies of
527
+ * cancellations (up to 16), seals held elsewhere (up to 64), and one file of
528
+ * disclosures for covered fields. A file that cannot be read stops the
529
+ * check: it is never set aside silently.
530
+ * @param {{cancellations?: File[], seals?: File[], disclosures?: File|null}} files the files chosen, as the
531
+ * browser gives them (anything with "name", "size" and "text()")
532
+ * @returns {Promise<{options: object, said: string[]}>} the options to check with, and what to say was handed over
533
+ */
534
+ export async function readHanded({ cancellations = [], seals = [], disclosures = null } = {}) {
535
+ const options = {};
536
+ const said = [];
537
+ if (cancellations.length > MAX_COPIES) throw new Error('At most 16 copies of cancellations can be handed over at once. Nothing was checked.');
538
+ if (cancellations.length) {
539
+ options.cancellations = [];
540
+ for (const file of cancellations) options.cancellations.push(await jsonFrom(file, 'a cancellation'));
541
+ said.push(cancellations.length === 1 ? 'one copy of a cancellation' : `${cancellations.length} copies of cancellations`);
542
+ }
543
+ if (seals.length > MAX_SEALS) throw new Error('At most 64 seals held elsewhere can be handed over at once. Nothing was checked.');
544
+ if (seals.length) {
545
+ options.seals = [];
546
+ for (const file of seals) options.seals.push(await jsonFrom(file, 'a seal'));
547
+ said.push(seals.length === 1 ? 'one seal held elsewhere' : `${seals.length} seals held elsewhere`);
548
+ }
549
+ if (disclosures) {
550
+ options.disclosures = await jsonFrom(disclosures, 'disclosures');
551
+ said.push('disclosures for covered fields');
552
+ }
553
+ return { options, said };
554
+ }
555
+
556
+ /**
557
+ * What the check rests on: whether the person named the passkey they trust
558
+ * and, where the record has a seal or seals held elsewhere were handed
559
+ * over, the recorder they expect; and whether those seals went unused
560
+ * because no recorder was named. Without the keys the person named, a
561
+ * record agrees only with the keys inside it, and anyone could have made
562
+ * those.
563
+ * @param {object} result what checkBook or checkShow returned
564
+ * @param {object} [options] what the check was told
565
+ */
566
+ export function trustOf(result, options = {}) {
567
+ const s = result.summary;
568
+ const issuerNamed = Array.isArray(options.issuerKeys) && options.issuerKeys.length > 0;
569
+ const sealerNamed = Array.isArray(options.sealKeys) && options.sealKeys.length > 0;
570
+ const sealsHanded = Array.isArray(options.seals) && options.seals.length > 0;
571
+ const named = issuerNamed && ((s.counts.seals === 0 && !sealsHanded) || sealerNamed);
572
+ // A seal handed over that was not compared with the book (it gives another book number, or could not be used) gives no pass.
573
+ // (Where no recorder was named, the seals handed over were not compared at all, and heldUnused says so.)
574
+ const heldCompared = !sealsHanded || !sealerNamed || (result.held ?? []).filter((h) => h.kind === 'seal').every((h) => h.used);
575
+ return { issuerNamed, named, sure: s.intact && named && heldCompared, heldUnused: sealsHanded && !sealerNamed };
576
+ }
577
+
420
578
  export function renderResult(target, result, { isShow = false, checkedBy = 'Checked in this browser.', options = {} } = {}) {
421
579
  target.replaceChildren();
422
580
  const s = result.summary;
423
581
  const missing = s.methodsMissing.join(' or ');
424
- // Without the keys the person named, a record agrees only with the keys
425
- // inside it, and anyone could have made those.
426
- const issuerNamed = Array.isArray(options.issuerKeys) && options.issuerKeys.length > 0;
427
- const named = issuerNamed && (s.counts.seals === 0 || (Array.isArray(options.sealKeys) && options.sealKeys.length > 0));
428
- const sure = s.intact && named;
582
+ const { issuerNamed, named, sure, heldUnused } = trustOf(result, options);
583
+ const heldCompared = heldUnused || (result.held ?? []).filter((h) => h.kind === 'seal').every((h) => h.used);
429
584
 
430
585
  target.append(el('h2', '', 'The answer'));
431
586
  const answers = el('ul', 'answers');
@@ -433,14 +588,23 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
433
588
  answer(answers, 'no', 'The record is NOT intact.', 'See the problems marked below. A record that is not intact is not evidence.');
434
589
  } else if (sure) {
435
590
  answer(answers, 'yes', 'The record is intact.', `Every signature fits its key, every label is right and nothing has been changed. ${checkedBy}`);
591
+ } else if (s.intact && named && !heldCompared) {
592
+ answer(
593
+ answers,
594
+ 'partly',
595
+ 'Not every seal you handed over was compared with this book.',
596
+ 'A seal held elsewhere that you handed over gives another book number than this book, or could not be used, so it was not compared with the book. See the note beside it. This is not a pass.',
597
+ );
436
598
  } else if (s.intact) {
437
599
  answer(
438
600
  answers,
439
601
  'partly',
440
602
  'Intact only as far as its own keys show.',
441
- issuerNamed
442
- ? 'The record has a seal, and you named no recorder you expect, so anyone could have sealed it. Put the fingerprint of the recorder you expect in its box. This is not a pass.'
443
- : 'You named no passkey you trust, so anyone could have made a record like this. Put the thumbprint of the passkey you trust in its box. This is not a pass.',
603
+ !issuerNamed
604
+ ? 'You named no passkey you trust, so anyone could have made a record like this. Put the thumbprint of the passkey you trust in its box. This is not a pass.'
605
+ : s.counts.seals === 0
606
+ ? 'You were handed seals held elsewhere, and you named no recorder you expect, so those seals were not compared with the book. Put the fingerprint of the recorder you expect in its box. This is not a pass.'
607
+ : 'The record has a seal, and you named no recorder you expect, so anyone could have sealed it. Put the fingerprint of the recorder you expect in its box. This is not a pass.',
444
608
  );
445
609
  } else {
446
610
  answer(
@@ -477,11 +641,16 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
477
641
 
478
642
  const held = result.held ?? [];
479
643
  if (held.length) target.append(el('h2', '', 'Handed over beside the book'));
644
+ if (heldUnused) {
645
+ const card = el('div', 'card show');
646
+ flag(card, 'note', 'The seals held elsewhere that you handed over were not compared with the book, because you named no recorder you expect. Put the fingerprint of the recorder you expect in its box.');
647
+ target.append(card);
648
+ }
480
649
  held.forEach((h, i) => target.append(heldCard(h, i + 1)));
481
650
 
482
651
  if (result.root) {
483
652
  const seal = el('div', 'card seal');
484
- seal.append(el('h3', 'kind', 'The top fingerprint (the Seal)'));
653
+ seal.append(el('h3', 'kind', topFingerprintHeading(result)));
485
654
  const list = el('dl');
486
655
  row(list, 'Top fingerprint', result.root, true);
487
656
  row(list, 'Entries in the book', String(result.size));
@@ -497,6 +666,9 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
497
666
  : 'It stands for the whole book. No outside time-stamp from a service you trust covers it: compare it, and the number of entries, with a copy you already trust.',
498
667
  ),
499
668
  );
669
+ if (s.witnessed) {
670
+ seal.append(el('p', 'quiet', `A witness you named as trusted signed for the seal at entry ${s.witnessed.seal}, which covers the first ${s.witnessed.entries} entries (${s.witnessed.seals.length} of ${s.counts.seals} seals witnessed). Being witnessed excuses nothing.${isShow ? ' These are single pages: they cannot show that a limit over the whole book was kept, so being witnessed says nothing here about totals.' : ''}`));
671
+ }
500
672
  target.append(seal);
501
673
  }
502
674
 
@@ -510,6 +682,7 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
510
682
  else if (e.kind === 'seal') card = sealCard(e);
511
683
  else if (e.kind === 'cancellation') card = cancellationCard(e);
512
684
  else if (e.kind === 'acknowledgement') card = acknowledgementCard(e);
685
+ else if (e.kind === 'denial') card = denialCard(e);
513
686
  else if (e.kind === 'pass') card = passCard(e);
514
687
  else if (e.kind === 'vouching') card = vouchingCard(e);
515
688
  else if (e.kind === 'withdrawal') card = withdrawalCard(e);