@bkazemi/verily 0.1.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.
@@ -1,9 +1,15 @@
1
- import { attestationLabel, externalLink, externalName, localSide, proofTitle, statusLabel, } from '../core/index.js';
1
+ import { attestationLabel, externalLink, externalName, lastProved, localSide, proofTitle, statusLabel, } from '../core/index.js';
2
2
  import { markStyles, verificationMark } from './mark.js';
3
3
  import { providerMark } from './provider-mark.js';
4
4
  import { onSignatureResult, signature, standing } from './signed.js';
5
- import { verilyLogo } from './logo.js';
5
+ import { projectUrl, verilyLogo } from './logo.js';
6
6
  import { version } from '../version.js';
7
+ /** The logotype as a link to the project, as both dialogs carry it in their footer. */
8
+ export function stampLink() {
9
+ const link = outward(node('a'), projectUrl);
10
+ link.append(verilyLogo());
11
+ return link;
12
+ }
7
13
  const openDialogs = new WeakMap();
8
14
  export const styles = `
9
15
  * { box-sizing: border-box; }
@@ -36,6 +42,14 @@ export const styles = `
36
42
  .summary a, .method a, .additional a { font: inherit; }
37
43
  .reference { margin-top: 2px; }
38
44
  .method { margin-top: 8px; }
45
+ /* In the corner the heading leaves empty, apart from everything that was checked. */
46
+ .listing { position: absolute; top: 12px; right: 14px; padding: 1px 8px; border: 1px solid #e0e6df; border-radius: 999px; color: #6b786f; font-size: 11px; font-weight: 550; }
47
+ .listing.preferred { border-color: #245f43; color: #245f43; }
48
+ /* Quiet, as its pill is: the surface and the name step back, and the controls do not. */
49
+ .account.retired { background: #f6f8f5; }
50
+ .account.retired > a, .account.retired > strong { color: #6b786f; }
51
+ .account.retired .mark { opacity: .45; }
52
+ .group { margin: 20px 0 0; color: #6b786f; font-size: 11px; font-weight: 550; }
39
53
  .additional { margin-top: 2px; padding-left: 12px; }
40
54
  .joiner { display: block; width: 20px; height: 20px; margin: 8px auto -4px; color: #90a096; }
41
55
  dl { margin: 16px 0 0; padding-top: 12px; border-top: 1px solid #e5e9e3; display: grid; grid-template-columns: auto 1fr; gap: 5px 16px; font-size: 11px; }
@@ -48,9 +62,10 @@ export const styles = `
48
62
  .action:disabled { opacity: .6; cursor: progress; }
49
63
  .action.danger:hover { border-color: #b3261e; background: #fdecea; color: #b3261e; }
50
64
  .row { display: flex; justify-content: end; gap: 8px; margin-top: 16px; }
51
- .account .row { align-items: center; justify-content: start; margin-top: 12px; }
65
+ .account .row { align-items: center; justify-content: start; flex-wrap: wrap; margin-top: 12px; }
52
66
  .account .action { padding: 5px 10px; font-size: 12px; }
53
- footer { display: flex; align-items: center; justify-content: start; gap: 6px; margin-top: 14px; color: #9aa9a0; font-size: 11px; }
67
+ footer { display: flex; align-items: center; justify-content: start; gap: 6px; margin: 20px 0 -8px; color: #9aa9a0; font-size: 11px; }
68
+ footer a { display: flex; color: inherit; }
54
69
  .logo { display: block; height: 13px; }
55
70
  ${markStyles}
56
71
  `;
@@ -138,6 +153,8 @@ export function accountCard(heading, name, reference, url, ...extra) {
138
153
  card.append(...extra);
139
154
  return card;
140
155
  }
156
+ /** What the holder is told before an account is retired, since it cannot be undone from here. */
157
+ const retiringNote = 'Retiring keeps this record as history. It will no longer read as verified, and who can read it cannot be changed afterwards.';
141
158
  /**
142
159
  * The links removing an account would remove. The record shown may itself be revoked while
143
160
  * older ones of the account are not, and those must stay removable, so this is not read
@@ -147,6 +164,9 @@ const removable = (evidence) => evidence.links ?? (evidence.status === 'revoked'
147
164
  /**
148
165
  * Under a holder's own account: renewing it, which is showing the same account again, and
149
166
  * removing it, which asks once more before it does. A removed link has nothing left to do.
167
+ *
168
+ * An account still in use can also be listed another way, or retired, which asks first as
169
+ * removing does. A retired one is renewed or removed and nothing else.
150
170
  */
151
171
  function manageRow(evidence, manage) {
152
172
  const row = node('div', '', 'row');
@@ -155,12 +175,49 @@ function manageRow(evidence, manage) {
155
175
  control.type = 'button';
156
176
  return control;
157
177
  };
178
+ const [named] = removable(evidence);
179
+ const { mark } = manage;
180
+ // A mark is the account's, set through any one of its links that still stands.
181
+ const listable = mark !== undefined && named !== undefined && !['revoked', 'retired'].includes(evidence.status);
182
+ /** A control that lists the account one way, and says so if that did not go through. */
183
+ const list = (label, as) => {
184
+ const control = act(label);
185
+ control.onclick = async () => {
186
+ for (const other of row.children)
187
+ other.disabled = true;
188
+ try {
189
+ await mark(named, as);
190
+ }
191
+ catch {
192
+ offer('That did not go through.');
193
+ }
194
+ };
195
+ return control;
196
+ };
158
197
  const offer = (note = '') => {
159
198
  const renew = act('Renew');
160
199
  const remove = act('Remove', true);
161
- renew.onclick = () => manage.connect(evidence.provider);
200
+ // The record the card is drawn from is the one renewed. A retired one comes back by the
201
+ // method it was first shown by alone, and a removed one cannot be renewed at all, so
202
+ // the account behind it is connected again.
203
+ renew.onclick = () => manage.renew && evidence.status !== 'revoked'
204
+ ? manage.renew(evidence.id, evidence.provider, evidence.status === 'retired' ? evidence.attestations.external[0].method : undefined)
205
+ : manage.connect(evidence.provider);
162
206
  remove.onclick = confirm;
163
- row.replaceChildren(renew, remove, ...(note ? [node('span', note, 'muted')] : []));
207
+ const listing = [];
208
+ if (listable) {
209
+ const retire = act('Retire');
210
+ retire.onclick = retiring;
211
+ listing.push(...(evidence.mark === 'preferred' ? [] : [list('Mark as preferred', 'preferred')]), ...(evidence.mark === 'unused' ? [] : [list('Mark as unused', 'unused')]), ...(evidence.mark === undefined ? [] : [list('Mark as current', 'current')]), retire);
212
+ }
213
+ row.replaceChildren(renew, ...listing, remove, ...(note ? [node('span', note, 'muted')] : []));
214
+ };
215
+ const retiring = () => {
216
+ const cancel = act('Cancel');
217
+ const sure = list('Retire', 'retired');
218
+ cancel.onclick = () => offer();
219
+ row.replaceChildren(node('span', retiringNote, 'muted'), cancel, sure);
220
+ cancel.focus();
164
221
  };
165
222
  const confirm = () => {
166
223
  const cancel = act('Cancel');
@@ -290,13 +347,22 @@ function externalCard(evidence, manage, later = () => { }) {
290
347
  if (evidence.signedUrl)
291
348
  attribution.append(signedMark(evidence, later));
292
349
  copy.append(node('div', status, 'state'), attribution);
293
- summary.append(verificationMark(current ? 'current' : 'inactive'), copy);
350
+ const retired = evidence.status === 'retired';
351
+ // A retired account claims nothing now, which is not the same as a claim that failed.
352
+ summary.append(verificationMark(current ? 'current' : retired ? 'pending' : 'inactive'), copy);
294
353
  const dates = node('dl');
295
354
  const dateRows = [['Approved', evidence.approvedAt]];
296
355
  if (evidence.status === 'revoked') {
297
356
  if (evidence.revokedAt !== undefined)
298
357
  dateRows.push(['Revoked on', evidence.revokedAt]);
299
358
  }
359
+ else if (retired) {
360
+ // Two rows, always, so a reader sees any gap between them: an account retired weeks
361
+ // after it was last proved went unproved for those weeks.
362
+ dateRows.push(['Last verified', lastProved(evidence)]);
363
+ if (evidence.retiredAt !== undefined)
364
+ dateRows.push(['Retired', evidence.retiredAt]);
365
+ }
300
366
  else {
301
367
  // A proof that has gone unread has not reached its expiry, so it still reads forward.
302
368
  dateRows.push([
@@ -307,7 +373,7 @@ function externalCard(evidence, manage, later = () => { }) {
307
373
  // When an artifact was last read belongs with the other times, not inside a sentence.
308
374
  // Only artifact methods drift: a sign-in is established once and does not go stale.
309
375
  const main = evidence.attestations.external[0];
310
- if (main.artifactUrl)
376
+ if (main.artifactUrl && !retired)
311
377
  dateRows.push(['Last checked', main.confirmedAt]);
312
378
  for (const [label, time] of dateRows) {
313
379
  dates.append(node('dt', label), node('dd', moment(time)));
@@ -322,7 +388,18 @@ function externalCard(evidence, manage, later = () => { }) {
322
388
  evidence.external.kind === 'mailbox' ? undefined : evidence.external.id, externalLink(evidence.external),
323
389
  // Status first, then how it was shown, then when. The proof explains the state
324
390
  // above it, so it cannot sit before that state has been given.
325
- summary, ...attestationNote(main, names), ...evidence.attestations.external.slice(1).flatMap((a) => attestationNote(a, names, true)), dates, ...(manage && removable(evidence).length ? [manageRow(evidence, manage)] : []));
391
+ summary, ...attestationNote(main, names), ...evidence.attestations.external.slice(1).flatMap((a) => attestationNote(a, names, true)),
392
+ // How the holder lists the account, in their words and apart from how it was shown:
393
+ // nothing checked it, so it is never among the methods above it.
394
+ ...(evidence.status === 'revoked' || retired
395
+ ? []
396
+ : evidence.mark === 'preferred'
397
+ ? [node('div', 'Preferred', 'listing preferred')]
398
+ : evidence.mark === 'unused'
399
+ ? [node('div', 'No longer used', 'listing')]
400
+ : []), dates, ...(manage && removable(evidence).length ? [manageRow(evidence, manage)] : []));
401
+ const surface = retired ? 'account retired' : 'account';
402
+ card.className = surface;
326
403
  // A signed record is not read, or acted on, before its signature has been checked: until
327
404
  // the check comes back the whole card is veiled and out of reach, with a word over it
328
405
  // saying why. A record handed over altered is then never read as it was handed over.
@@ -331,12 +408,12 @@ function externalCard(evidence, manage, later = () => { }) {
331
408
  const checking = node('div', '', 'checking');
332
409
  checking.setAttribute('role', 'status');
333
410
  checking.append(node('span', '', 'spinner'), document.createTextNode('Checking signature…'));
334
- card.className = 'account veiled';
411
+ card.className = `${surface} veiled`;
335
412
  for (const part of veiled)
336
413
  part.inert = true;
337
414
  card.append(checking);
338
415
  void signatures(evidence).then(() => {
339
- card.className = 'account';
416
+ card.className = surface;
340
417
  for (const part of veiled)
341
418
  part.inert = false;
342
419
  checking.remove();
@@ -354,8 +431,9 @@ function addRow(manage) {
354
431
  return row;
355
432
  }
356
433
  /**
357
- * The subject once, then each account linked to it on its own card, in the order they
358
- * were connected. Every record given is of the one subject, which the caller has checked.
434
+ * The subject once, then each account linked to it on its own card, in the order given.
435
+ * Retired accounts sit in a group of their own at the foot, under what says they are.
436
+ * Every record given is of the one subject, which the caller has checked.
359
437
  */
360
438
  function render(content, records, manage, later) {
361
439
  const [first] = records;
@@ -369,7 +447,15 @@ function render(content, records, manage, later) {
369
447
  site: first.siteName,
370
448
  provider: first.providerName,
371
449
  }));
372
- content.replaceChildren(localCard, linkMark(), ...records.map((record) => externalCard(record, manage, later)), ...(manage ? [addRow(manage)] : []),
450
+ const retired = records.filter((record) => record.status === 'retired');
451
+ content.replaceChildren(localCard, linkMark(), ...records
452
+ .filter((record) => record.status !== 'retired')
453
+ .map((record) => externalCard(record, manage, later)), ...(manage ? [addRow(manage)] : []), ...(retired.length
454
+ ? [
455
+ node('h3', 'Retired accounts', 'group'),
456
+ ...retired.map((record) => externalCard(record, manage, later)),
457
+ ]
458
+ : []),
373
459
  // Nothing below the cards may name a provider. Approval, method and dates belong to
374
460
  // the card they came from, and a second provider on this subject gets its own card.
375
461
  node('p', 'Verification does not guarantee legal identity, trustworthiness, or permanent ownership.', 'muted explanation'));
@@ -415,12 +501,44 @@ manage) {
415
501
  close.type = 'button';
416
502
  close.setAttribute('aria-label', 'Close verification details');
417
503
  content.setAttribute('aria-live', 'polite');
418
- stamp.append(verilyLogo(), node('span', version));
504
+ stamp.append(stampLink(), node('span', version));
419
505
  header.append(heading, close);
420
506
  dialog.append(header, content, stamp);
421
507
  /** Links removed from this dialog, shown as removed before the page hands over new records. */
422
508
  const removed = new Map();
509
+ /**
510
+ * How accounts were listed from this dialog, in the order it was asked, shown as asked
511
+ * before the page hands over new records: each names every link of its account, and
512
+ * holds how those links read when it was asked. Once any of them reads differently the
513
+ * backend has spoken, for this change or a later one made elsewhere, and what it says
514
+ * is shown from then on.
515
+ */
516
+ const listed = [];
517
+ /** What of a record a listing, a retirement or a renewal changes. */
518
+ const listing = (record) => JSON.stringify([record.status, record.mark, record.retiredAt, record.approvedAt]);
519
+ /** How each record given reads now, by its id. */
520
+ const listings = (given) => new Map([given].flat().map((record) => [record.id, listing(record)]));
423
521
  let held;
522
+ /** A record as the listings made here leave it, each applied as the backend applies it. */
523
+ const relisted = (evidence) => {
524
+ let record = evidence;
525
+ for (const { ids, as, at } of listed) {
526
+ if (['revoked', 'retired'].includes(record.status))
527
+ break;
528
+ if (!ids.includes(record.id)) {
529
+ // One preferred account for a holder: picking another moves the mark.
530
+ if (as === 'preferred' && record.mark === 'preferred')
531
+ record = { ...record, mark: undefined };
532
+ continue;
533
+ }
534
+ record =
535
+ as === 'retired'
536
+ ? // Its signed form is another document now, to be read with the records to come.
537
+ { ...record, status: 'retired', retiredAt: at, mark: undefined, signedUrl: undefined }
538
+ : { ...record, mark: as === 'current' ? undefined : as };
539
+ }
540
+ return record;
541
+ };
424
542
  const actions = manage && {
425
543
  // The other dialog opens over this one before this one goes, so the page behind is
426
544
  // never uncovered between the two.
@@ -428,6 +546,14 @@ manage) {
428
546
  manage.connect(provider);
429
547
  dialog.close();
430
548
  },
549
+ ...(manage.renew
550
+ ? {
551
+ renew(id, provider, method) {
552
+ manage.renew(id, provider, method);
553
+ dialog.close();
554
+ },
555
+ }
556
+ : {}),
431
557
  async remove(ids) {
432
558
  await manage.remove(ids);
433
559
  for (const id of ids)
@@ -435,6 +561,23 @@ manage) {
435
561
  if (held && dialog.open)
436
562
  draw(held);
437
563
  },
564
+ ...(manage.mark
565
+ ? {
566
+ async mark(id, as) {
567
+ await manage.mark(id, as);
568
+ // Every link of the account the one named belongs to, as the cards group them.
569
+ const account = arrange([held ?? []].flat()).find((card) => removable(card).includes(id));
570
+ listed.push({
571
+ ids: account ? removable(account) : [id],
572
+ as,
573
+ at: Date.now(),
574
+ before: listings(held ?? []),
575
+ });
576
+ if (held && dialog.open)
577
+ draw(held);
578
+ },
579
+ }
580
+ : {}),
438
581
  };
439
582
  root.append(dialog);
440
583
  document.body.append(host);
@@ -447,11 +590,15 @@ manage) {
447
590
  /** Redraws only for a record that reads differently from the one already on screen. */
448
591
  const draw = (given) => {
449
592
  held = given;
593
+ const now = listings(given);
594
+ for (const change of [...listed])
595
+ if (change.ids.some((id) => now.get(id) !== change.before.get(id)))
596
+ listed.splice(listed.indexOf(change), 1);
450
597
  const records = arrange([given]
451
598
  .flat()
452
599
  .map((e) => removed.has(e.id) && e.status !== 'revoked'
453
600
  ? { ...e, status: 'revoked', revokedAt: removed.get(e.id) }
454
- : standing(e)));
601
+ : relisted(standing(e))));
455
602
  const key = JSON.stringify(records.map((e) => [e, statusLabel(e, Date.now())]));
456
603
  if (key === drawn)
457
604
  return;
@@ -1,4 +1,5 @@
1
1
  import type { Evidence } from '../core/index.js';
2
+ import { type Listing } from './evidence-dialog.js';
2
3
  export type { Evidence } from '../core/index.js';
3
4
  export interface Result {
4
5
  outcome: 'complete' | 'cancelled' | 'failed';
@@ -29,7 +30,10 @@ export declare function init({ backendUrl, handoffUrl }: {
29
30
  * Opens the connect dialog over the current page. Resolves when it closes, with how
30
31
  * the last attempt in it ended. Call from a click: a sign-in method opens a window.
31
32
  */
32
- openConnect(opener?: HTMLElement, back?: () => void, provider?: string): Promise<Result>;
33
+ openConnect(opener?: HTMLElement, back?: () => void, provider?: string, renew?: {
34
+ id: string;
35
+ method?: string;
36
+ }): Promise<Result>;
33
37
  /**
34
38
  * Draws the pill that opens the connect dialog. The host receives a `verily-result`
35
39
  * event, whose detail is the Result, whenever the dialog closes on a new connection.
@@ -54,6 +58,11 @@ export declare function init({ backendUrl, handoffUrl }: {
54
58
  connectionIds: string[];
55
59
  } & Arrangement): Promise<void>;
56
60
  disconnect(id: string): Promise<unknown>;
61
+ /**
62
+ * Says how the holder would have an account listed, or retires it. Names one record
63
+ * and acts on the account: every record of it the holder has.
64
+ */
65
+ mark(id: string, as: Listing): Promise<unknown>;
57
66
  issueShare: (id: string) => Promise<unknown>;
58
67
  revokeShare: (id: string) => Promise<unknown>;
59
68
  };
@@ -119,9 +119,11 @@ function arrange(element, { stacked, peek }) {
119
119
  }
120
120
  const live = (evidence) => evidence.status === 'verified' && evidence.expiresAt > Date.now();
121
121
  /**
122
- * A subject's accounts, one entry for each, in the order they were connected, earliest
123
- * first. Records are not accounts: the same account can stand on several, shown a second
124
- * way, or revoked once and connected again. Those are one account and get one card.
122
+ * A subject's accounts, one entry for each. The one its holder prefers comes first, then
123
+ * the rest in the order they were connected, earliest first, then those the holder no
124
+ * longer uses, and last those retired. Records are not accounts: the same account can
125
+ * stand on several, shown a second way, or revoked once and connected again. Those are one
126
+ * account and get one card.
125
127
  *
126
128
  * Where any of an account's records is verified now, the earliest of those speaks for it,
127
129
  * and the ways the others show it are listed beneath as other methods. Its lapsed
@@ -159,14 +161,26 @@ function accounts(records) {
159
161
  sources: verified.length ? verified : [base],
160
162
  };
161
163
  })
162
- .sort((a, b) => a.connectedAt - b.connectedAt);
164
+ .sort((a, b) => place(a) - place(b) || a.connectedAt - b.connectedAt);
163
165
  }
164
166
  /**
165
- * One subject's accounts as a single pill: the first connected, then how many more stand
166
- * behind it. The order is the order they were connected in, which a renewal never changes.
167
- * Only accounts verified now are counted, and one of those leads when the first connected
168
- * has lapsed, so the pill never puts a lapsed account forward while a good one sits behind
169
- * a number. The lapsed ones are all still there in the dialog, each in its place.
167
+ * Where an account is listed among a subject's others, before the order they were
168
+ * connected in. This is all there is to preferring an account: there is no separate pin.
169
+ */
170
+ function place(account) {
171
+ if (account.status === 'retired')
172
+ return 3;
173
+ return account.mark === 'preferred' ? 0 : account.mark === 'unused' ? 2 : 1;
174
+ }
175
+ /**
176
+ * One subject's accounts as a single pill: the first of them, then how many more stand
177
+ * behind it. The order is the holder's preferred account, then the order the rest were
178
+ * connected in, which a renewal never changes, then the ones no longer used.
179
+ * Only accounts verified now are counted, and one of those leads when the first has
180
+ * lapsed, so the pill never puts a lapsed account forward while a good one sits behind
181
+ * a number. The lapsed ones are all still there in the dialog, each in its place. A
182
+ * preferred account that has lapsed keeps its mark, and leads again once renewed. A
183
+ * retired account is never counted, and leads only where nothing else is shown.
170
184
  *
171
185
  * `load` gives the records afresh when the dialog opens and as it stays open.
172
186
  */
@@ -317,7 +331,9 @@ export function init({ backendUrl, handoffUrl }) {
317
331
  safeUrl(data.local.profileUrl);
318
332
  return data;
319
333
  },
320
- start: async (provider, method) => flowView(await ask('/sessions', { kind: 'connect', provider, method })),
334
+ start: async (provider, method, renew) => flowView(await ask('/sessions', renew
335
+ ? { kind: 'renew', connectionId: renew, provider, method }
336
+ : { kind: 'connect', provider, method })),
321
337
  read: async (id) => flowView(await ask(`/flows/${encodeURIComponent(id)}?format=json`, undefined, id)),
322
338
  submit: async (id, artifact) => flowView(await ask(`/flows/${encodeURIComponent(id)}/submit`, { artifact }, id)),
323
339
  async approve(id, visibility, cancel) {
@@ -362,12 +378,14 @@ export function init({ backendUrl, handoffUrl }) {
362
378
  * Opens the connect dialog over the current page. Resolves when it closes, with how
363
379
  * the last attempt in it ended. Call from a click: a sign-in method opens a window.
364
380
  */
365
- openConnect(opener = document.body, back, provider) {
381
+ openConnect(opener = document.body, back, provider,
382
+ /** A record of the holder's to renew, in place of connecting: see `openConnectDialog`. */
383
+ renew) {
366
384
  if (remote && !handoffUrl)
367
385
  throw new Error('A backend on another origin requires a handoff URL');
368
386
  // Each opening is vouched for afresh, for whoever is signed in to this site now.
369
387
  session = undefined;
370
- return openConnectDialog(opener, flows, back, provider);
388
+ return openConnectDialog(opener, flows, back, provider, renew);
371
389
  },
372
390
  /**
373
391
  * Draws the pill that opens the connect dialog. The host receives a `verily-result`
@@ -530,6 +548,15 @@ export function init({ backendUrl, handoffUrl }) {
530
548
  session = undefined;
531
549
  return ask(`/connections/${encodeURIComponent(id)}/disconnect`, {});
532
550
  },
551
+ /**
552
+ * Says how the holder would have an account listed, or retires it. Names one record
553
+ * and acts on the account: every record of it the holder has.
554
+ */
555
+ mark(id, as) {
556
+ if (remote)
557
+ session = undefined;
558
+ return ask(`/connections/${encodeURIComponent(id)}/mark`, { as });
559
+ },
533
560
  issueShare: (id) => request(`/connections/${encodeURIComponent(id)}/share`, {}),
534
561
  revokeShare: (id) => request(`/connections/${encodeURIComponent(id)}/share-revoke`, {}),
535
562
  };
@@ -645,7 +672,12 @@ function validEvidence(value) {
645
672
  ['account', 'key', 'page', 'mailbox'].includes(String(value.external.kind))) &&
646
673
  (value.revokedAt === undefined ||
647
674
  (typeof value.revokedAt === 'number' && Number.isFinite(value.revokedAt))) &&
648
- ['verified', 'unconfirmed', 'expired', 'revoked'].includes(String(value.status)) &&
675
+ ['verified', 'unconfirmed', 'expired', 'retired', 'revoked'].includes(String(value.status)) &&
676
+ // A retired record stands on its dates, so one that does not say when is not one.
677
+ (value.status === 'retired'
678
+ ? typeof value.retiredAt === 'number' && Number.isFinite(value.retiredAt)
679
+ : value.retiredAt === undefined) &&
680
+ (value.mark === undefined || ['preferred', 'unused'].includes(String(value.mark))) &&
649
681
  ['public', 'unlisted'].includes(String(value.visibility)) &&
650
682
  (value.connectedAt === undefined ||
651
683
  (typeof value.connectedAt === 'number' && Number.isFinite(value.connectedAt))) &&
@@ -766,6 +798,14 @@ if (typeof customElements !== 'undefined' && !customElements.get('verily-badge')
766
798
  changed(result);
767
799
  });
768
800
  },
801
+ renew: (id, provider, method) => {
802
+ void client
803
+ .openConnect(this, () => details(this), provider, { id, method })
804
+ .then((result) => {
805
+ if (result.outcome === 'complete')
806
+ changed(result);
807
+ });
808
+ },
769
809
  // One at a time, each told as it goes: if a later one fails, the host still
770
810
  // hears of those that went and reads its records again.
771
811
  remove: async (ids) => {
@@ -774,6 +814,10 @@ if (typeof customElements !== 'undefined' && !customElements.get('verily-badge')
774
814
  changed({ outcome: 'removed', connectionId: id });
775
815
  }
776
816
  },
817
+ mark: async (id, as) => {
818
+ await client.mark(id, as);
819
+ changed({ outcome: 'marked', connectionId: id, as });
820
+ },
777
821
  });
778
822
  }
779
823
  else
@@ -1,2 +1,4 @@
1
+ /** Where the logotype under a dialog leads: the project, for a reader asking what drew this. */
2
+ export declare const projectUrl = "https://github.com/bkazemi/verily";
1
3
  /** The logotype, drawn as nodes: nothing in the badge sets innerHTML. */
2
4
  export declare function verilyLogo(): SVGSVGElement;
@@ -1,4 +1,6 @@
1
1
  import { logoPaths, logoViewBox } from '../logo.js';
2
+ /** Where the logotype under a dialog leads: the project, for a reader asking what drew this. */
3
+ export const projectUrl = 'https://github.com/bkazemi/verily';
2
4
  /** The logotype, drawn as nodes: nothing in the badge sets innerHTML. */
3
5
  export function verilyLogo() {
4
6
  const namespace = 'http://www.w3.org/2000/svg';
@@ -15,5 +15,7 @@ export declare function paintMark(svg: SVGSVGElement, tone: MarkTone): void;
15
15
  export declare function verificationMark(tone?: MarkTone): SVGSVGElement;
16
16
  /** The clock on an expired record: a face, and hands at three o'clock. */
17
17
  export declare const clockMark: () => SVGSVGElement;
18
+ /** The box on a retired record: a lid, what it holds, and a slot to take it down by. */
19
+ export declare const boxMark: () => SVGSVGElement;
18
20
  /** The dash on a record that is revoked or cannot be shown. */
19
21
  export declare const dashMark: () => SVGSVGElement;
@@ -73,5 +73,11 @@ export const clockMark = () => statusMark('clock', [
73
73
  ['circle', { cx: '8', cy: '8', r: '6.2' }],
74
74
  ['path', { d: 'M8 4.6V8h2.6' }],
75
75
  ]);
76
+ /** The box on a retired record: a lid, what it holds, and a slot to take it down by. */
77
+ export const boxMark = () => statusMark('box', [
78
+ ['path', { d: 'M2.6 3.8h10.8v2.6H2.6z' }],
79
+ ['path', { d: 'M3.7 6.4v5.8h8.6V6.4' }],
80
+ ['path', { d: 'M6.6 9h2.8' }],
81
+ ]);
76
82
  /** The dash on a record that is revoked or cannot be shown. */
77
83
  export const dashMark = () => statusMark('dash', [['path', { d: 'M4.4 8h7.2' }]]);
@@ -28,6 +28,10 @@ export declare function signature(evidence: Evidence): Promise<Signature>;
28
28
  * it, with only its present state taken from the record as read, so what stands beside the
29
29
  * signature mark is what was signed. One whose signed record failed is not shown as verified: the
30
30
  * verifier's own signature does not bear it out, so it reads as unconfirmed.
31
+ *
32
+ * The holder's mark is taken from the record as read too, each time, and never from the
33
+ * signed record kept: a mark would otherwise vanish the moment its record checked, and one
34
+ * changed since would go on being shown as it was.
31
35
  */
32
36
  export declare function standing<T extends Evidence>(evidence: T): T;
33
37
  /**
@@ -27,9 +27,12 @@ function canonical(value) {
27
27
  .map(([key, field]) => `${JSON.stringify(key)}:${canonical(field)}`)
28
28
  .join(',')}}`;
29
29
  }
30
- /** What a record claims, apart from whether it stands now: all a signed record can bear out. */
30
+ /**
31
+ * What a record claims, apart from whether it stands now: all a signed record can bear out.
32
+ * The holder's mark is no claim of the verifier's and is in no signed record.
33
+ */
31
34
  function claims(record) {
32
- const { status: _status, revokedAt: _revokedAt, signedUrl: _signedUrl, links: _links, type: _type, version: _version, issuedAt: _issuedAt, ...rest } = record;
35
+ const { status: _status, revokedAt: _revokedAt, signedUrl: _signedUrl, mark: _mark, links: _links, type: _type, version: _version, issuedAt: _issuedAt, ...rest } = record;
33
36
  return canonical(rest);
34
37
  }
35
38
  /**
@@ -124,13 +127,20 @@ async function check(evidence) {
124
127
  document.provider !== evidence.provider ||
125
128
  document.external.id !== evidence.external.id)
126
129
  return { state: 'invalid' };
130
+ // A retired record is signed as a version of its own, and a record that stands as the
131
+ // other. One served for the other is the record caught between the two, and no verdict.
132
+ if ((document.version === 2) !== (evidence.status === 'retired'))
133
+ return { state: 'unchecked', why: 'unreadable' };
127
134
  // The record as it was signed. Whether it stands now is no part of a signed record, so
128
- // that alone is kept from the record as it was read.
129
- const { type: _type, version: _version, issuedAt: _issuedAt, ...said } = document;
135
+ // that is kept from the record as it was read, and so is the holder's mark, which is in
136
+ // no signed record: whatever a document says of either is left behind.
137
+ const { type: _type, version: _version, issuedAt: _issuedAt, status: _status, retiredAt: _retiredAt, mark: _mark, ...said } = document;
130
138
  const record = {
131
139
  ...said,
132
140
  status: evidence.status,
133
141
  ...(evidence.revokedAt === undefined ? {} : { revokedAt: evidence.revokedAt }),
142
+ ...(document.version === 2 ? { retiredAt: document.retiredAt } : {}),
143
+ ...(evidence.mark === undefined ? {} : { mark: evidence.mark }),
134
144
  signedUrl: evidence.signedUrl,
135
145
  };
136
146
  // Signed, but not something a page may draw: it is shown as read, with no mark.
@@ -178,18 +188,25 @@ export function signature(evidence) {
178
188
  * it, with only its present state taken from the record as read, so what stands beside the
179
189
  * signature mark is what was signed. One whose signed record failed is not shown as verified: the
180
190
  * verifier's own signature does not bear it out, so it reads as unconfirmed.
191
+ *
192
+ * The holder's mark is taken from the record as read too, each time, and never from the
193
+ * signed record kept: a mark would otherwise vanish the moment its record checked, and one
194
+ * changed since would go on being shown as it was.
181
195
  */
182
196
  export function standing(evidence) {
183
197
  if (evidence.signedUrl === undefined)
184
198
  return evidence;
185
199
  const key = shown(evidence);
186
200
  const signed = signedAs.get(key);
187
- if (signed)
201
+ if (signed) {
202
+ const { mark: _mark, ...said } = signed;
188
203
  return {
189
- ...signed,
204
+ ...said,
190
205
  status: evidence.status,
191
206
  ...(evidence.revokedAt === undefined ? {} : { revokedAt: evidence.revokedAt }),
207
+ ...(evidence.mark === undefined ? {} : { mark: evidence.mark }),
192
208
  };
209
+ }
193
210
  return evidence.status === 'verified' && failures.has(key)
194
211
  ? { ...evidence, status: 'unconfirmed' }
195
212
  : evidence;