@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.
- package/README.md +42 -27
- package/dist/browser/badge.js +16 -5
- package/dist/browser/connect-dialog.d.ts +21 -2
- package/dist/browser/connect-dialog.js +28 -13
- package/dist/browser/evidence-dialog.d.ts +19 -1
- package/dist/browser/evidence-dialog.js +163 -16
- package/dist/browser/index.d.ts +10 -1
- package/dist/browser/index.js +57 -13
- package/dist/browser/logo.d.ts +2 -0
- package/dist/browser/logo.js +2 -0
- package/dist/browser/mark.d.ts +2 -0
- package/dist/browser/mark.js +6 -0
- package/dist/browser/signed.d.ts +4 -0
- package/dist/browser/signed.js +23 -6
- package/dist/core/index.d.ts +76 -4
- package/dist/core/index.js +14 -0
- package/dist/core/signed.d.ts +20 -5
- package/dist/core/signed.js +5 -2
- package/dist/server/copy.d.ts +6 -0
- package/dist/server/copy.js +20 -0
- package/dist/server/discord.js +3 -1
- package/dist/server/github.js +4 -2
- package/dist/server/index.js +134 -50
- package/dist/server/link.js +24 -4
- package/dist/server/service.d.ts +92 -6
- package/dist/server/service.js +362 -19
- package/dist/server/youtube.js +3 -1
- package/dist/site/index.d.ts +2 -1
- package/dist/verily.js +212 -37
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)),
|
|
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 =
|
|
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 =
|
|
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
|
|
358
|
-
*
|
|
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
|
-
|
|
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(
|
|
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;
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
};
|
package/dist/browser/index.js
CHANGED
|
@@ -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
|
|
123
|
-
*
|
|
124
|
-
*
|
|
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
|
-
*
|
|
166
|
-
*
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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',
|
|
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
|
package/dist/browser/logo.d.ts
CHANGED
|
@@ -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;
|
package/dist/browser/logo.js
CHANGED
|
@@ -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';
|
package/dist/browser/mark.d.ts
CHANGED
|
@@ -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;
|
package/dist/browser/mark.js
CHANGED
|
@@ -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' }]]);
|
package/dist/browser/signed.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/browser/signed.js
CHANGED
|
@@ -27,9 +27,12 @@ function canonical(value) {
|
|
|
27
27
|
.map(([key, field]) => `${JSON.stringify(key)}:${canonical(field)}`)
|
|
28
28
|
.join(',')}}`;
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
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
|
|
129
|
-
|
|
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
|
-
...
|
|
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;
|