provared 0.4.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/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, 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
@@ -149,6 +149,17 @@ function stampRows(list, stamps) {
149
149
  }
150
150
  }
151
151
 
152
+ // The witness statements beside a seal. What a statement says is shown only where it passed its check.
153
+ function witnessRows(list, witnesses) {
154
+ for (const w of witnesses ?? []) {
155
+ if (w.state === 'invalid') row(list, 'Witness statement, not used', 'it did not pass its check (see the problems)');
156
+ else if (w.state === 'unavailable') row(list, 'Witness statement, not checked', 'this browser cannot check both of its signatures');
157
+ 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)`);
158
+ 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)`);
159
+ else row(list, 'Witness statement, not counted', `from a witness you did not name as trusted (key set ${w.witness})`);
160
+ }
161
+ }
162
+
152
163
  function cancellationCard(e) {
153
164
  const card = el('div', `card slip${e.problems.length ? ' broken' : ''}`);
154
165
  card.append(el('h3', 'kind', `Entry ${e.index}: Cancellation (the person ends a slip)`));
@@ -188,8 +199,39 @@ function acknowledgementCard(e) {
188
199
  return card;
189
200
  }
190
201
 
202
+ // A seal held elsewhere, handed over beside the book.
203
+ function heldSealCard(h, n) {
204
+ const card = el('div', `card seal${h.problems.length ? ' broken' : ''}`);
205
+ card.append(el('h3', 'kind', `Handed over beside the book, ${n}: a seal of the book held elsewhere`));
206
+ if (h.problems.length) {
207
+ card.append(el('p', 'quiet', 'It did not pass its check, so it was not used. See the problems above.'));
208
+ return card;
209
+ }
210
+ if (h.verified === 'none') {
211
+ card.append(el('p', 'quiet', 'What this seal says is not shown, because this device could not check its signatures.'));
212
+ } else {
213
+ const c = h.content;
214
+ const list = el('dl');
215
+ row(list, 'Covers', `the first ${c.size} entries`);
216
+ row(list, 'Stands at', `entry ${h.place}`);
217
+ row(list, 'Top fingerprint', c.root, true);
218
+ row(list, 'Sealed at', `${showTime(c.when)} (the recorder's own word)`);
219
+ row(list, 'Recorder', `${label(c.by.name)} (a label; it is not checked)`);
220
+ row(list, 'Recorder\'s key set', h.sealer, true);
221
+ row(list, 'Recorder\'s signatures', signatureWords(h.signatures));
222
+ stampRows(list, h.stamps);
223
+ witnessRows(list, h.witnesses);
224
+ 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');
225
+ row(list, 'Fingerprint', h.fingerprint, true);
226
+ card.append(list);
227
+ }
228
+ for (const note of h.notes) flag(card, 'note', note);
229
+ return card;
230
+ }
231
+
191
232
  // A cancellation the person kept, handed over beside the book.
192
233
  function heldCard(h, n) {
234
+ if (h.kind === 'seal') return heldSealCard(h, n);
193
235
  const card = el('div', `card slip${h.problems.length ? ' broken' : ''}`);
194
236
  card.append(el('h3', 'kind', `Handed over beside the book, ${n}: the person's own copy of a cancellation`));
195
237
  if (h.problems.length) {
@@ -365,6 +407,29 @@ function refusalCard(e) {
365
407
  return card;
366
408
  }
367
409
 
410
+ // A denial: the agent's own side says it did not take an action, and why.
411
+ function denialCard(e) {
412
+ const card = el('div', `card refusal${e.problems.length ? ' broken' : ''}`);
413
+ card.append(el('h3', 'kind', `Entry ${e.index}: Denial (the agent's side says it did not take an action)`));
414
+ if (hidden(e)) {
415
+ card.append(el('p', 'quiet', hidden(e)));
416
+ return card;
417
+ }
418
+ const c = e.content;
419
+ const list = el('dl');
420
+ row(list, 'From', c.pass ? `the helper agent of pass ${c.pass}` : 'the slip\'s own agent');
421
+ row(list, 'When', `${showTime(c.when)} (the agent's side's own word)`);
422
+ row(list, 'Not taken', c.action + (c.amount ? `, ${c.amount.value} ${c.amount.unit}` : '') + (c.with ? `, with [${c.with}]` : ''));
423
+ if (c.details) row(list, 'Documents named', `${c.details.length}, by their fingerprints only`);
424
+ row(list, 'Who said no', DENIAL_SOURCES[c.source]);
425
+ if (c.codes) row(list, 'Reasons given', c.codes.join(', '));
426
+ row(list, 'Agent\'s signatures', signatureWords(e.signatures));
427
+ row(list, 'Fingerprint', e.fingerprint, true);
428
+ card.append(list);
429
+ 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.');
430
+ return card;
431
+ }
432
+
368
433
  function sealCard(e) {
369
434
  const card = el('div', `card seal${e.problems.length ? ' broken' : ''}`);
370
435
  card.append(el('h3', 'kind', `Entry ${e.index}: Seal (the book, signed by whoever keeps it)`));
@@ -381,6 +446,7 @@ function sealCard(e) {
381
446
  row(list, 'Recorder\'s key set', `${e.sealer}. Compare this fingerprint with the one the recorder gave you.`, true);
382
447
  row(list, 'Recorder\'s signatures', signatureWords(e.signatures));
383
448
  stampRows(list, e.stamps);
449
+ witnessRows(list, e.witnesses);
384
450
  row(list, 'Fingerprint', e.fingerprint, true);
385
451
  card.append(list);
386
452
  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 +483,79 @@ function termsCard(e) {
417
483
  * @param {object} [o.options] what the check was told to trust: the green answer needs the passkey the
418
484
  * person trusts and, where the record has a seal, the recorder they expect
419
485
  */
486
+ // A copy of a cancellation, a seal, or a file of disclosures, is small.
487
+ const MAX_HANDED_BYTES = 1000000;
488
+ const MAX_COPIES = 16;
489
+ const MAX_SEALS = 64;
490
+
491
+ async function jsonFrom(file, what) {
492
+ if (file.size > MAX_HANDED_BYTES) throw new Error(`The file of ${what}, ${shown(file.name)}, is too large. Nothing was checked.`);
493
+ try {
494
+ return JSON.parse(await file.text());
495
+ } catch {
496
+ throw new Error(`The file of ${what}, ${shown(file.name)}, is not JSON. Nothing was checked. Choose another file, or clear these files.`);
497
+ }
498
+ }
499
+
500
+ /**
501
+ * What the person was handed with the record: their own copies of
502
+ * cancellations (up to 16), seals held elsewhere (up to 64), and one file of
503
+ * disclosures for covered fields. A file that cannot be read stops the
504
+ * check: it is never set aside silently.
505
+ * @param {{cancellations?: File[], seals?: File[], disclosures?: File|null}} files the files chosen, as the
506
+ * browser gives them (anything with "name", "size" and "text()")
507
+ * @returns {Promise<{options: object, said: string[]}>} the options to check with, and what to say was handed over
508
+ */
509
+ export async function readHanded({ cancellations = [], seals = [], disclosures = null } = {}) {
510
+ const options = {};
511
+ const said = [];
512
+ if (cancellations.length > MAX_COPIES) throw new Error('At most 16 copies of cancellations can be handed over at once. Nothing was checked.');
513
+ if (cancellations.length) {
514
+ options.cancellations = [];
515
+ for (const file of cancellations) options.cancellations.push(await jsonFrom(file, 'a cancellation'));
516
+ said.push(cancellations.length === 1 ? 'one copy of a cancellation' : `${cancellations.length} copies of cancellations`);
517
+ }
518
+ if (seals.length > MAX_SEALS) throw new Error('At most 64 seals held elsewhere can be handed over at once. Nothing was checked.');
519
+ if (seals.length) {
520
+ options.seals = [];
521
+ for (const file of seals) options.seals.push(await jsonFrom(file, 'a seal'));
522
+ said.push(seals.length === 1 ? 'one seal held elsewhere' : `${seals.length} seals held elsewhere`);
523
+ }
524
+ if (disclosures) {
525
+ options.disclosures = await jsonFrom(disclosures, 'disclosures');
526
+ said.push('disclosures for covered fields');
527
+ }
528
+ return { options, said };
529
+ }
530
+
531
+ /**
532
+ * What the check rests on: whether the person named the passkey they trust
533
+ * and, where the record has a seal or seals held elsewhere were handed
534
+ * over, the recorder they expect; and whether those seals went unused
535
+ * because no recorder was named. Without the keys the person named, a
536
+ * record agrees only with the keys inside it, and anyone could have made
537
+ * those.
538
+ * @param {object} result what checkBook or checkShow returned
539
+ * @param {object} [options] what the check was told
540
+ */
541
+ export function trustOf(result, options = {}) {
542
+ const s = result.summary;
543
+ const issuerNamed = Array.isArray(options.issuerKeys) && options.issuerKeys.length > 0;
544
+ const sealerNamed = Array.isArray(options.sealKeys) && options.sealKeys.length > 0;
545
+ const sealsHanded = Array.isArray(options.seals) && options.seals.length > 0;
546
+ const named = issuerNamed && ((s.counts.seals === 0 && !sealsHanded) || sealerNamed);
547
+ // A seal handed over that was not compared with the book (it gives another book number, or could not be used) gives no pass.
548
+ // (Where no recorder was named, the seals handed over were not compared at all, and heldUnused says so.)
549
+ const heldCompared = !sealsHanded || !sealerNamed || (result.held ?? []).filter((h) => h.kind === 'seal').every((h) => h.used);
550
+ return { issuerNamed, named, sure: s.intact && named && heldCompared, heldUnused: sealsHanded && !sealerNamed };
551
+ }
552
+
420
553
  export function renderResult(target, result, { isShow = false, checkedBy = 'Checked in this browser.', options = {} } = {}) {
421
554
  target.replaceChildren();
422
555
  const s = result.summary;
423
556
  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;
557
+ const { issuerNamed, named, sure, heldUnused } = trustOf(result, options);
558
+ const heldCompared = heldUnused || (result.held ?? []).filter((h) => h.kind === 'seal').every((h) => h.used);
429
559
 
430
560
  target.append(el('h2', '', 'The answer'));
431
561
  const answers = el('ul', 'answers');
@@ -433,14 +563,23 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
433
563
  answer(answers, 'no', 'The record is NOT intact.', 'See the problems marked below. A record that is not intact is not evidence.');
434
564
  } else if (sure) {
435
565
  answer(answers, 'yes', 'The record is intact.', `Every signature fits its key, every label is right and nothing has been changed. ${checkedBy}`);
566
+ } else if (s.intact && named && !heldCompared) {
567
+ answer(
568
+ answers,
569
+ 'partly',
570
+ 'Not every seal you handed over was compared with this book.',
571
+ '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.',
572
+ );
436
573
  } else if (s.intact) {
437
574
  answer(
438
575
  answers,
439
576
  'partly',
440
577
  '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.',
578
+ !issuerNamed
579
+ ? '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.'
580
+ : s.counts.seals === 0
581
+ ? '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.'
582
+ : '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
583
  );
445
584
  } else {
446
585
  answer(
@@ -477,6 +616,11 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
477
616
 
478
617
  const held = result.held ?? [];
479
618
  if (held.length) target.append(el('h2', '', 'Handed over beside the book'));
619
+ if (heldUnused) {
620
+ const card = el('div', 'card show');
621
+ 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.');
622
+ target.append(card);
623
+ }
480
624
  held.forEach((h, i) => target.append(heldCard(h, i + 1)));
481
625
 
482
626
  if (result.root) {
@@ -497,6 +641,9 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
497
641
  : '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
642
  ),
499
643
  );
644
+ if (s.witnessed) {
645
+ 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.' : ''}`));
646
+ }
500
647
  target.append(seal);
501
648
  }
502
649
 
@@ -510,6 +657,7 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
510
657
  else if (e.kind === 'seal') card = sealCard(e);
511
658
  else if (e.kind === 'cancellation') card = cancellationCard(e);
512
659
  else if (e.kind === 'acknowledgement') card = acknowledgementCard(e);
660
+ else if (e.kind === 'denial') card = denialCard(e);
513
661
  else if (e.kind === 'pass') card = passCard(e);
514
662
  else if (e.kind === 'vouching') card = vouchingCard(e);
515
663
  else if (e.kind === 'withdrawal') card = withdrawalCard(e);