@bkazemi/verily 0.6.0 → 0.8.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 +197 -91
- package/dist/browser/connect-dialog.js +3 -4
- package/dist/browser/evidence-dialog.d.ts +7 -2
- package/dist/browser/evidence-dialog.js +62 -26
- package/dist/browser/index.js +3 -0
- package/dist/browser/mark.d.ts +2 -6
- package/dist/browser/mark.js +6 -12
- package/dist/browser/provider-mark.d.ts +3 -3
- package/dist/browser/provider-mark.js +15 -90
- package/dist/core/index.d.ts +78 -2
- package/dist/core/index.js +26 -2
- package/dist/instance/auth.d.ts +14 -0
- package/dist/instance/auth.js +80 -0
- package/dist/instance/bin.d.ts +2 -0
- package/dist/instance/bin.js +36 -0
- package/dist/instance/cloudflare.d.ts +57 -0
- package/dist/instance/cloudflare.js +71 -0
- package/dist/instance/env.d.ts +51 -0
- package/dist/instance/env.js +84 -0
- package/dist/instance/index.d.ts +136 -0
- package/dist/instance/index.js +777 -0
- package/dist/instance/init.d.ts +6 -0
- package/dist/instance/init.js +28 -0
- package/dist/instance/limits.d.ts +30 -0
- package/dist/instance/limits.js +45 -0
- package/dist/instance/node.d.ts +13 -0
- package/dist/instance/node.js +43 -0
- package/dist/instance/page.d.ts +18 -0
- package/dist/instance/page.js +30 -0
- package/dist/instance/postgres.d.ts +19 -0
- package/dist/instance/postgres.js +83 -0
- package/dist/instance/routes.d.ts +7 -0
- package/dist/instance/routes.js +22 -0
- package/dist/instance/serve.d.ts +31 -0
- package/dist/instance/serve.js +49 -0
- package/dist/instance/sites.d.ts +169 -0
- package/dist/instance/sites.js +413 -0
- package/dist/instance/starter.d.ts +6 -0
- package/dist/instance/starter.js +99 -0
- package/dist/instance/store.d.ts +20 -0
- package/dist/instance/store.js +11 -0
- package/dist/instance/undici.d.ts +8 -0
- package/dist/instance/undici.js +9 -0
- package/dist/marks.d.ts +44 -0
- package/dist/marks.js +98 -0
- package/dist/server/discord.js +3 -0
- package/dist/server/domain.js +37 -10
- package/dist/server/github-gist.js +16 -4
- package/dist/server/github.js +3 -0
- package/dist/server/index.js +20 -47
- package/dist/server/link.js +9 -4
- package/dist/server/render.d.ts +49 -0
- package/dist/server/render.js +104 -0
- package/dist/server/service.d.ts +44 -1
- package/dist/server/service.js +225 -19
- package/dist/server/style.d.ts +1 -1
- package/dist/server/style.js +132 -2
- package/dist/server/youtube.js +5 -0
- package/dist/verily.js +155 -124
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +30 -2
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { accountCard, backMark, closeMark, linkMark, node, outward, stampLink, styles, } from './evidence-dialog.js';
|
|
1
|
+
import { accountCard, backMark, closeMark, linkMark, node, namedCard, outward, stampLink, styles, } from './evidence-dialog.js';
|
|
3
2
|
import { providerMark } from './provider-mark.js';
|
|
4
3
|
import { version } from '../version.js';
|
|
5
4
|
const connectStyles = `
|
|
@@ -435,9 +434,9 @@ renew) {
|
|
|
435
434
|
}
|
|
436
435
|
function approve(flow) {
|
|
437
436
|
const logo = providerMark(flow.provider.id, flow.provider.method);
|
|
438
|
-
const external =
|
|
437
|
+
const external = namedCard(logo
|
|
439
438
|
? [logo, document.createTextNode(flow.provider.name)]
|
|
440
|
-
: [document.createTextNode(flow.provider.name)],
|
|
439
|
+
: [document.createTextNode(flow.provider.name)], flow.external);
|
|
441
440
|
const parts = [subjectCard(flow.local), linkMark(), external];
|
|
442
441
|
const choice = node('fieldset');
|
|
443
442
|
const visibilityText = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type Evidence } from '../core/index.js';
|
|
1
|
+
import { type Evidence, type ExternalAccount } from '../core/index.js';
|
|
2
2
|
/** The logotype as a link to the project, as both dialogs carry it in their footer. */
|
|
3
3
|
export declare function stampLink(): HTMLAnchorElement;
|
|
4
|
-
export declare const styles = "\n * { box-sizing: border-box; }\n dialog { width: min(460px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); margin: auto; padding: 24px; border: 1px solid #dce2de; border-radius: 16px; background: #fff; color: #23312b; box-shadow: 0 24px 90px #10201935; font: 13px/1.6 system-ui, sans-serif; }\n dialog::backdrop { background: #15271f66; }\n header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }\n h2 { margin: 0; font-size: 18px; font-weight: 650; letter-spacing: -.3px; }\n button { width: 32px; height: 32px; border: 1px solid #dce2de; border-radius: 8px; background: #fff; color: #52645a; font: 20px system-ui, sans-serif; cursor: pointer; }\n button:hover { background: #f2f5f1; }\n .icon { display: grid; place-items: center; padding: 0; }\n .icon svg { width: 16px; height: 16px; }\n .title { display: flex; align-items: center; gap: 6px; min-width: 0; }\n .all { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; color: #52645a; }\n .all:hover { background: #f2f5f1; }\n .all svg { width: 16px; height: 16px; }\n a { color: #245f43; text-underline-offset: 3px; overflow-wrap: anywhere; }\n a:focus-visible, button:focus-visible, summary:focus-visible { outline: 2px solid #357ce5; outline-offset: 3px; }\n /* An element given its own display would otherwise go on showing while hidden. */\n [hidden] { display: none !important; }\n .summary { display: flex; align-items: center; gap: 8px; margin-top: 16px; }\n .mark { width: 36px; height: 36px; flex-shrink: 0; }\n .provider { width: 14px; height: 14px; }\n .state { font-size: 14px; font-weight: 650; line-height: 1.4; }\n .muted { color: #6b786f; font-size: 12px; }\n .account { position: relative; }\n .veiled > :not(.checking) { filter: blur(4px); opacity: .55; user-select: none; }\n .checking { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 8px; color: #23312b; font-size: 13px; font-weight: 600; }\n .spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin .7s linear infinite; }\n @keyframes spin { to { transform: rotate(360deg); } }\n @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }\n .
|
|
4
|
+
export declare const styles = "\n * { box-sizing: border-box; }\n dialog { width: min(460px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); margin: auto; padding: 24px; border: 1px solid #dce2de; border-radius: 16px; background: #fff; color: #23312b; box-shadow: 0 24px 90px #10201935; font: 13px/1.6 system-ui, sans-serif; }\n dialog::backdrop { background: #15271f66; }\n header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }\n h2 { margin: 0; font-size: 18px; font-weight: 650; letter-spacing: -.3px; }\n button { width: 32px; height: 32px; border: 1px solid #dce2de; border-radius: 8px; background: #fff; color: #52645a; font: 20px system-ui, sans-serif; cursor: pointer; }\n button:hover { background: #f2f5f1; }\n .icon { display: grid; place-items: center; padding: 0; }\n .icon svg { width: 16px; height: 16px; }\n .title { display: flex; align-items: center; gap: 6px; min-width: 0; }\n .all { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; color: #52645a; }\n .all:hover { background: #f2f5f1; }\n .all svg { width: 16px; height: 16px; }\n a { color: #245f43; text-underline-offset: 3px; overflow-wrap: anywhere; }\n a:focus-visible, button:focus-visible, summary:focus-visible { outline: 2px solid #357ce5; outline-offset: 3px; }\n /* An element given its own display would otherwise go on showing while hidden. */\n [hidden] { display: none !important; }\n .summary { display: flex; align-items: center; gap: 8px; margin-top: 16px; }\n .mark { width: 36px; height: 36px; flex-shrink: 0; }\n .provider { width: 14px; height: 14px; }\n .state { font-size: 14px; font-weight: 650; line-height: 1.4; }\n .muted { color: #6b786f; font-size: 12px; }\n .account { position: relative; }\n .veiled > :not(.checking) { filter: blur(4px); opacity: .55; user-select: none; }\n .checking { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 8px; color: #23312b; font-size: 13px; font-weight: 600; }\n .spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin .7s linear infinite; }\n @keyframes spin { to { transform: rotate(360deg); } }\n @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }\n /* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n .summary .signed { display: inline-block; margin-left: 5px; padding: 0 6px; border: 1px solid #cfdcd3; border-radius: 999px; font-size: 10px; font-weight: 550; line-height: 1.3; text-decoration: none; vertical-align: 1px; }\n .summary a.signed:hover { background: #f2f5f1; }\n .summary .signed.bad { border-color: #b3261e; color: #b3261e; font-weight: 650; cursor: help; }\n .caution { display: inline-block; margin-left: 3px; color: #a15c00; vertical-align: -1.5px; }\n .caution svg { display: block; width: 10px; height: 10px; }\n .account { padding: 14px 16px; border: 1px solid #e0e6df; border-radius: 10px; margin-top: 12px; }\n .account h3 { display: flex; align-items: center; gap: 6px; margin: 0 0 3px; color: #6b786f; font-size: 11px; font-weight: 550; }\n .account a, .account strong { font-weight: 650; font-size: 14px; }\n /* The account's own name carries that weight; a link inside a line of prose does not. */\n .summary a, .method a, .additional a { font: inherit; }\n .reference { margin-top: 2px; }\n /* A fingerprint in fours: it breaks between groups, and copies as one unbroken word. */\n .fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }\n .fingerprint span { display: inline-block; margin-right: .5ch; }\n .method { margin-top: 8px; }\n /* In the corner the heading leaves empty, apart from everything that was checked. */\n .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; }\n .listing.preferred { border-color: #245f43; color: #245f43; }\n /* Quiet, as its pill is: the surface and the name step back, and the controls do not. */\n .account.retired { background: #f6f8f5; }\n .account.retired > a, .account.retired > strong, .account.retired > .named > a, .account.retired > .named > strong { color: #6b786f; }\n /* The name, then a dot and the account's creation date. The date wraps under a long name. */\n /* Clipped 4px left of the text, which hides the dot when the date wraps to a new line. */\n .named { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 14px; margin-left: -4px; padding-left: 4px; overflow-x: clip; }\n .since { position: relative; white-space: nowrap; }\n /* The dot sits in the gap before the date. */\n .since::before { content: \"\u00B7\"; position: absolute; left: -7px; transform: translateX(-50%); }\n .account.retired .mark { opacity: .45; }\n .group { margin: 20px 0 0; color: #6b786f; font-size: 11px; font-weight: 550; }\n summary.group { display: flex; align-items: center; gap: 4px; width: fit-content; border-radius: 4px; list-style: none; cursor: pointer; }\n summary::-webkit-details-marker { display: none; }\n .fold { display: flex; align-items: center; gap: 4px; width: auto; height: auto; padding: 2px 8px 2px 4px; margin-left: -4px; border: 0; background: none; font: 550 12px/1.6 system-ui, sans-serif; }\n .fold svg, summary svg { width: 12px; height: 12px; }\n .fold[aria-expanded=\"true\"] svg, details[open] > summary svg { transform: rotate(90deg); }\n @media (prefers-reduced-motion: no-preference) { .fold svg, summary svg { transition: transform .15s; } }\n /* The fold and what the holder may do share a line, until a question needs the whole of it. */\n .bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 10px; }\n .account .bar .row { margin: 0 0 0 auto; }\n .additional { margin-top: 2px; padding-left: 12px; }\n .joiner { display: block; width: 20px; height: 20px; margin: 8px auto -4px; color: #90a096; }\n 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; }\n dt { color: #6b786f; }\n dd { margin: 0; text-align: right; overflow-wrap: anywhere; }\n .explanation { margin-top: 16px; }\n .action { width: auto; height: auto; padding: 9px 14px; font: 600 13px/1.3 system-ui, sans-serif; color: #23312b; text-align: left; }\n .action.primary { border-color: #245f43; background: #245f43; color: #fff; }\n .action.primary:hover { background: #1d4f37; }\n .action:disabled { opacity: .6; cursor: progress; }\n .action.danger:hover { border-color: #b3261e; background: #fdecea; color: #b3261e; }\n .row { display: flex; justify-content: end; gap: 8px; margin-top: 16px; }\n .account .row { align-items: center; justify-content: start; flex-wrap: wrap; margin-top: 12px; }\n .account .action { padding: 5px 10px; font-size: 12px; }\n .account .action.dots { display: grid; place-items: center; align-self: stretch; width: 30px; padding: 0; }\n .dots svg { width: 16px; height: 16px; }\n /* Placed under its button each time it opens, above the dialog and clipped by none of it. */\n .menu { position: fixed; inset: auto; margin: 0; min-width: 170px; padding: 6px; border: 1px solid #dce2de; border-radius: 10px; background: #fff; color: #23312b; box-shadow: 0 12px 40px #10201930; }\n .menu:popover-open { display: grid; }\n .account .menu .action { border: 0; border-radius: 6px; padding: 6px 10px; font-weight: 550; }\n .menu .danger { color: #b3261e; }\n .menu hr { width: 100%; margin: 4px 0; border: 0; border-top: 1px solid #e5e9e3; }\n footer { display: flex; align-items: center; justify-content: start; gap: 6px; margin: 20px 0 -8px; color: #9aa9a0; font-size: 11px; }\n footer a { display: flex; color: inherit; }\n .logo { display: block; height: 13px; }\n \n @media (prefers-reduced-motion: no-preference) {\n .mark.current path:last-child { animation: verily-draw .8s cubic-bezier(.65, 0, .35, 1) .1s backwards; }\n }\n @keyframes verily-draw { from { stroke-dasharray: 0 176; } to { stroke-dasharray: 108 176; } }\n\n";
|
|
5
5
|
export declare function node<K extends keyof HTMLElementTagNameMap>(tag: K, text?: string, className?: string): HTMLElementTagNameMap[K];
|
|
6
6
|
/**
|
|
7
7
|
* Points an anchor out of the dialog. Every link here opens in a new tab: the dialog is
|
|
@@ -26,6 +26,11 @@ export declare function linkMark(): SVGSVGElement;
|
|
|
26
26
|
* expires on its own terms, and a second provider on the same subject would differ.
|
|
27
27
|
*/
|
|
28
28
|
export declare function accountCard(heading: Node[], name: string, reference: string | string[] | undefined, url?: string, ...extra: HTMLElement[]): HTMLElement;
|
|
29
|
+
/**
|
|
30
|
+
* The card for an external account: its name, link and id, with the "Joined <date>" text
|
|
31
|
+
* on the name's line when the creation date is known.
|
|
32
|
+
*/
|
|
33
|
+
export declare function namedCard(heading: Node[], external: ExternalAccount, ...extra: HTMLElement[]): HTMLElement;
|
|
29
34
|
/**
|
|
30
35
|
* A record as a card shows it. Where the card speaks for several records of one account,
|
|
31
36
|
* `links` names every one not yet revoked, whether or not it is shown, so removing the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { attestationLabel, externalId, externalIdGroups, externalLink, externalName, lastProved, localSide, proofTitle, statusLabel, } from '../core/index.js';
|
|
1
|
+
import { attestationLabel, externalId, externalIdGroups, externalLink, externalName, externalSince, 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';
|
|
@@ -40,9 +40,12 @@ export const styles = `
|
|
|
40
40
|
.spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin .7s linear infinite; }
|
|
41
41
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
42
42
|
@media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }
|
|
43
|
-
|
|
44
|
-
.
|
|
45
|
-
.
|
|
43
|
+
/* The "signed" pill after the verifier's name, one size smaller than the line it sits on. */
|
|
44
|
+
.summary .signed { display: inline-block; margin-left: 5px; padding: 0 6px; border: 1px solid #cfdcd3; border-radius: 999px; font-size: 10px; font-weight: 550; line-height: 1.3; text-decoration: none; vertical-align: 1px; }
|
|
45
|
+
.summary a.signed:hover { background: #f2f5f1; }
|
|
46
|
+
.summary .signed.bad { border-color: #b3261e; color: #b3261e; font-weight: 650; cursor: help; }
|
|
47
|
+
.caution { display: inline-block; margin-left: 3px; color: #a15c00; vertical-align: -1.5px; }
|
|
48
|
+
.caution svg { display: block; width: 10px; height: 10px; }
|
|
46
49
|
.account { padding: 14px 16px; border: 1px solid #e0e6df; border-radius: 10px; margin-top: 12px; }
|
|
47
50
|
.account h3 { display: flex; align-items: center; gap: 6px; margin: 0 0 3px; color: #6b786f; font-size: 11px; font-weight: 550; }
|
|
48
51
|
.account a, .account strong { font-weight: 650; font-size: 14px; }
|
|
@@ -58,7 +61,13 @@ export const styles = `
|
|
|
58
61
|
.listing.preferred { border-color: #245f43; color: #245f43; }
|
|
59
62
|
/* Quiet, as its pill is: the surface and the name step back, and the controls do not. */
|
|
60
63
|
.account.retired { background: #f6f8f5; }
|
|
61
|
-
.account.retired > a, .account.retired > strong { color: #6b786f; }
|
|
64
|
+
.account.retired > a, .account.retired > strong, .account.retired > .named > a, .account.retired > .named > strong { color: #6b786f; }
|
|
65
|
+
/* The name, then a dot and the account's creation date. The date wraps under a long name. */
|
|
66
|
+
/* Clipped 4px left of the text, which hides the dot when the date wraps to a new line. */
|
|
67
|
+
.named { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 14px; margin-left: -4px; padding-left: 4px; overflow-x: clip; }
|
|
68
|
+
.since { position: relative; white-space: nowrap; }
|
|
69
|
+
/* The dot sits in the gap before the date. */
|
|
70
|
+
.since::before { content: "·"; position: absolute; left: -7px; transform: translateX(-50%); }
|
|
62
71
|
.account.retired .mark { opacity: .45; }
|
|
63
72
|
.group { margin: 20px 0 0; color: #6b786f; font-size: 11px; font-weight: 550; }
|
|
64
73
|
summary.group { display: flex; align-items: center; gap: 4px; width: fit-content; border-radius: 4px; list-style: none; cursor: pointer; }
|
|
@@ -232,13 +241,34 @@ function moment(time) {
|
|
|
232
241
|
* expires on its own terms, and a second provider on the same subject would differ.
|
|
233
242
|
*/
|
|
234
243
|
export function accountCard(heading, name, reference, url, ...extra) {
|
|
244
|
+
return side(heading, name, reference, url, undefined, extra);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The card for an external account: its name, link and id, with the "Joined <date>" text
|
|
248
|
+
* on the name's line when the creation date is known.
|
|
249
|
+
*/
|
|
250
|
+
export function namedCard(heading, external, ...extra) {
|
|
251
|
+
return side(heading, externalName(external),
|
|
252
|
+
// A mailbox's address or a domain is its name already, so there is no second identifier.
|
|
253
|
+
externalIdGroups(external) ?? externalId(external), externalLink(external),
|
|
254
|
+
// In UTC, so the day matches the one the verifier's own pages print.
|
|
255
|
+
externalSince(external, (time) => new Date(time).toLocaleDateString(undefined, { dateStyle: 'medium', timeZone: 'UTC' })), extra);
|
|
256
|
+
}
|
|
257
|
+
function side(heading, name, reference, url, since, extra) {
|
|
235
258
|
const card = node('section', '', 'account');
|
|
236
259
|
const title = node('h3');
|
|
237
260
|
const value = node(url ? 'a' : 'strong', name);
|
|
238
261
|
title.append(...heading);
|
|
239
262
|
if (value instanceof HTMLAnchorElement)
|
|
240
263
|
outward(value, url);
|
|
241
|
-
|
|
264
|
+
if (since) {
|
|
265
|
+
// The name and the date share a line, and the date wraps under a long name.
|
|
266
|
+
const named = node('div', '', 'named');
|
|
267
|
+
named.append(value, node('span', since, 'muted since'));
|
|
268
|
+
card.append(title, named);
|
|
269
|
+
}
|
|
270
|
+
else
|
|
271
|
+
card.append(title, value);
|
|
242
272
|
if (typeof reference === 'string')
|
|
243
273
|
card.append(node('div', reference, 'muted reference'));
|
|
244
274
|
else if (reference) {
|
|
@@ -420,38 +450,45 @@ function signatures(evidence) {
|
|
|
420
450
|
: signature(source)));
|
|
421
451
|
}
|
|
422
452
|
/**
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
453
|
+
* The "signed" pill shown after the verifier's name. It links to the signature section of
|
|
454
|
+
* the verifier's evidence page, where the signed record can be downloaded.
|
|
455
|
+
*
|
|
456
|
+
* The signature is checked here against the verifier's keys:
|
|
457
|
+
* - valid: the pill says "signed"
|
|
458
|
+
* - not yet checked: the pill carries a warning sign
|
|
459
|
+
* - invalid: a warning replaces the pill
|
|
460
|
+
*
|
|
461
|
+
* `warned` is called every time the invalid warning is drawn, so a folded card can open.
|
|
429
462
|
*/
|
|
430
463
|
function signedMark(evidence, later, warned) {
|
|
431
464
|
const mark = node('span');
|
|
432
|
-
const link = outward(node('a', 'signed'), `${evidence.evidenceUrl}#signed`);
|
|
465
|
+
const link = outward(node('a', 'signed', 'signed'), `${evidence.evidenceUrl}#signed`);
|
|
433
466
|
const see = 'See the signature (opens in a new tab)';
|
|
434
467
|
const settle = (results) => {
|
|
435
468
|
const first = results[0];
|
|
436
469
|
if (results.some((result) => result.state === 'invalid')) {
|
|
437
|
-
|
|
470
|
+
// Not a link: the page it would open is the one this record failed to match.
|
|
471
|
+
const warning = node('span', 'bad signature', 'signed bad');
|
|
438
472
|
warning.setAttribute('role', 'alert');
|
|
439
473
|
// What it means, for a reader who has never met a signature, kept off the card.
|
|
440
474
|
warning.title = `This record does not match the signature ${evidence.verifierName} put on it, so it may have been altered. Do not rely on it.`;
|
|
441
|
-
mark.replaceChildren(
|
|
475
|
+
mark.replaceChildren(warning);
|
|
442
476
|
warned();
|
|
443
477
|
return;
|
|
444
478
|
}
|
|
445
479
|
if (first?.state === 'valid' && results.every((result) => result.state === 'valid')) {
|
|
446
|
-
|
|
447
|
-
//
|
|
480
|
+
// No tick: a signature that checks out is what a reader expects, and the pill is
|
|
481
|
+
// marked only when it did not. Setting the word again clears a warning sign left in it.
|
|
482
|
+
link.textContent = 'signed';
|
|
483
|
+
// Names where the keys came from, which is what the check answers for.
|
|
448
484
|
link.title = `Signature checked in this browser against the keys ${new URL(evidence.evidenceUrl).host} publishes (key ${first.keyId}). ${see}`;
|
|
449
|
-
mark.replaceChildren(
|
|
485
|
+
mark.replaceChildren(link);
|
|
450
486
|
return;
|
|
451
487
|
}
|
|
452
|
-
// No verdict was reached. The record is signed and says so, with a warning
|
|
453
|
-
//
|
|
454
|
-
// or could not read what it needed is made again when
|
|
488
|
+
// No verdict was reached. The record is signed and says so, with a warning sign in the
|
|
489
|
+
// pill, which is all that tells it from one that checked out, and why is said on hover.
|
|
490
|
+
// A check that only ran out of time or could not read what it needed is made again when
|
|
491
|
+
// the dialog next reads.
|
|
455
492
|
const why = results.find((result) => result.state === 'unchecked');
|
|
456
493
|
const reason = why?.state === 'unchecked' && why.why === 'timeout'
|
|
457
494
|
? 'Signature check timed out, retrying.'
|
|
@@ -462,8 +499,9 @@ function signedMark(evidence, later, warned) {
|
|
|
462
499
|
caution.setAttribute('aria-label', reason);
|
|
463
500
|
caution.append(cautionMark());
|
|
464
501
|
link.textContent = 'signed';
|
|
502
|
+
link.append(caution);
|
|
465
503
|
link.title = `${reason} ${see}`;
|
|
466
|
-
mark.replaceChildren(
|
|
504
|
+
mark.replaceChildren(link);
|
|
467
505
|
if (why)
|
|
468
506
|
later(() => void signatures(evidence).then(settle));
|
|
469
507
|
};
|
|
@@ -585,12 +623,10 @@ function externalCard(evidence, manage, later = () => { }, fold, warned = () =>
|
|
|
585
623
|
if (fold)
|
|
586
624
|
bar.append(foldToggle([...methods, dates], fold), ...row);
|
|
587
625
|
const logo = providerMark(evidence.provider, evidence.attestations.external[0].method);
|
|
588
|
-
const card =
|
|
626
|
+
const card = namedCard(
|
|
589
627
|
// The mark and the name, or just the name. A mark that falls back to writing the name
|
|
590
628
|
// would print it twice here, since this heading writes it either way.
|
|
591
|
-
logo ? [logo, document.createTextNode(provider)] : [document.createTextNode(provider)],
|
|
592
|
-
// A mailbox's address or a domain is its name already, so there is no second identifier.
|
|
593
|
-
externalIdGroups(evidence.external) ?? externalId(evidence.external), externalLink(evidence.external),
|
|
629
|
+
logo ? [logo, document.createTextNode(provider)] : [document.createTextNode(provider)], evidence.external,
|
|
594
630
|
// Status first, then how it was shown, then when. The proof explains the state
|
|
595
631
|
// above it, so it cannot sit before that state has been given.
|
|
596
632
|
summary, ...(fold ? [] : methods),
|
package/dist/browser/index.js
CHANGED
|
@@ -649,6 +649,9 @@ function validEvidence(value) {
|
|
|
649
649
|
['label', 'reference'].every((k) => typeof value.local[k] === 'string') &&
|
|
650
650
|
['id', 'handle', 'profileUrl'].every((k) => typeof value.external[k] === 'string') &&
|
|
651
651
|
(value.local.profileUrl === undefined || typeof value.local.profileUrl === 'string') &&
|
|
652
|
+
(value.external.createdAt === undefined ||
|
|
653
|
+
(typeof value.external.createdAt === 'number' &&
|
|
654
|
+
Number.isFinite(value.external.createdAt))) &&
|
|
652
655
|
// A signed record lives beside the record it was made from and nowhere else, since the
|
|
653
656
|
// keys it is checked against are read from where the record lives.
|
|
654
657
|
(value.signedUrl === undefined || value.signedUrl === `${value.evidenceUrl}?format=signed`) &&
|
package/dist/browser/mark.d.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* does not, and `pending` for one nobody has confirmed yet, which claims nothing and is
|
|
4
|
-
* drawn in the surrounding text colour.
|
|
5
|
-
*/
|
|
6
|
-
export type MarkTone = 'current' | 'inactive' | 'pending';
|
|
1
|
+
import { type MarkTone } from '../marks.js';
|
|
2
|
+
export type { MarkTone };
|
|
7
3
|
/**
|
|
8
4
|
* The check draws itself in from its left end once the verification holds: when a pill's
|
|
9
5
|
* mark first becomes `current`, and each time the dialog opens on a current one.
|
package/dist/browser/mark.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
current: ['#D3444C', '#149766'],
|
|
3
|
-
inactive: ['#149766', '#D3444C'],
|
|
4
|
-
pending: ['currentColor', 'currentColor'],
|
|
5
|
-
};
|
|
1
|
+
import { tones, verification } from '../marks.js';
|
|
6
2
|
/**
|
|
7
3
|
* The check draws itself in from its left end once the verification holds: when a pill's
|
|
8
4
|
* mark first becomes `current`, and each time the dialog opens on a current one.
|
|
@@ -24,20 +20,18 @@ export function paintMark(svg, tone) {
|
|
|
24
20
|
export function verificationMark(tone = 'current') {
|
|
25
21
|
const namespace = 'http://www.w3.org/2000/svg';
|
|
26
22
|
const svg = document.createElementNS(namespace, 'svg');
|
|
27
|
-
svg.setAttribute('viewBox',
|
|
23
|
+
svg.setAttribute('viewBox', verification.viewBox);
|
|
28
24
|
svg.setAttribute('fill', 'none');
|
|
29
25
|
svg.setAttribute('aria-hidden', 'true');
|
|
30
26
|
svg.setAttribute('focusable', 'false');
|
|
31
27
|
for (const layer of [0, 1]) {
|
|
32
28
|
const path = document.createElementNS(namespace, 'path');
|
|
33
|
-
path.setAttribute('d',
|
|
29
|
+
path.setAttribute('d', verification.path);
|
|
34
30
|
path.setAttribute('stroke-width', '32');
|
|
35
31
|
path.setAttribute('stroke-linejoin', 'miter');
|
|
36
|
-
if (layer === 1)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
path.setAttribute('stroke-dashoffset', '-54');
|
|
40
|
-
}
|
|
32
|
+
if (layer === 1)
|
|
33
|
+
for (const [name, value] of Object.entries(verification.check))
|
|
34
|
+
path.setAttribute(name, value);
|
|
41
35
|
svg.append(path);
|
|
42
36
|
}
|
|
43
37
|
paintMark(svg, tone);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The mark for a provider, or nothing where there is none. Returning nothing
|
|
3
|
-
* the provider's name keeps this to one job: a caller that also writes the
|
|
4
|
-
* otherwise print it twice, and only the caller knows where the name belongs.
|
|
2
|
+
* The mark for a provider as an element, or nothing where there is none. Returning nothing
|
|
3
|
+
* rather than the provider's name keeps this to one job: a caller that also writes the
|
|
4
|
+
* name would otherwise print it twice, and only the caller knows where the name belongs.
|
|
5
5
|
*/
|
|
6
6
|
export declare function providerMark(provider: string, method?: string): SVGSVGElement | undefined;
|
|
@@ -1,101 +1,26 @@
|
|
|
1
|
+
import { markOf, pathAttributes } from '../marks.js';
|
|
1
2
|
const namespace = 'http://www.w3.org/2000/svg';
|
|
2
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The mark for a provider as an element, or nothing where there is none. Returning nothing
|
|
5
|
+
* rather than the provider's name keeps this to one job: a caller that also writes the
|
|
6
|
+
* name would otherwise print it twice, and only the caller knows where the name belongs.
|
|
7
|
+
*/
|
|
8
|
+
export function providerMark(provider, method) {
|
|
9
|
+
const mark = markOf(provider, method);
|
|
10
|
+
if (!mark)
|
|
11
|
+
return undefined;
|
|
3
12
|
const svg = document.createElementNS(namespace, 'svg');
|
|
4
|
-
svg.setAttribute('viewBox',
|
|
13
|
+
svg.setAttribute('viewBox', mark.viewBox);
|
|
5
14
|
svg.setAttribute('class', 'provider');
|
|
6
15
|
svg.setAttribute('aria-hidden', 'true');
|
|
7
16
|
svg.setAttribute('focusable', 'false');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
/** GitHub mark from Primer Octicons (MIT); license in docs/licenses/octicons.txt. */
|
|
11
|
-
function github() {
|
|
12
|
-
const svg = mark();
|
|
13
|
-
const path = document.createElementNS(namespace, 'path');
|
|
14
|
-
svg.setAttribute('fill', 'currentColor');
|
|
15
|
-
path.setAttribute('d', 'M6.766 11.328c-2.063-.25-3.516-1.734-3.516-3.656 0-.781.281-1.625.75-2.188-.203-.515-.172-1.609.063-2.062.625-.078 1.468.25 1.968.703.594-.187 1.219-.281 1.985-.281.765 0 1.39.094 1.953.265.484-.437 1.344-.765 1.969-.687.218.422.25 1.515.046 2.047.5.593.766 1.39.766 2.203 0 1.922-1.453 3.375-3.547 3.64.531.344.89 1.094.89 1.954v1.625c0 .468.391.734.86.547C13.781 14.359 16 11.53 16 8.03 16 3.61 12.406 0 7.984 0 3.563 0 0 3.61 0 8.031a7.88 7.88 0 0 0 5.172 7.422c.422.156.828-.125.828-.547v-1.25c-.219.094-.5.156-.75.156-1.031 0-1.64-.562-2.078-1.609-.172-.422-.36-.672-.719-.719-.187-.015-.25-.093-.25-.187 0-.188.313-.328.625-.328.453 0 .844.281 1.25.86.313.452.64.655 1.031.655s.641-.14 1-.5c.266-.265.47-.5.657-.656');
|
|
16
|
-
svg.append(path);
|
|
17
|
-
return svg;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Discord mark from Simple Icons (CC0); notice in docs/licenses/simple-icons.txt. The mark
|
|
21
|
-
* itself is Discord's trademark, used here only to say an account is a Discord account.
|
|
22
|
-
*/
|
|
23
|
-
function discord() {
|
|
24
|
-
const svg = mark();
|
|
25
|
-
const path = document.createElementNS(namespace, 'path');
|
|
26
|
-
svg.setAttribute('viewBox', '0 0 24 24');
|
|
27
|
-
svg.setAttribute('fill', 'currentColor');
|
|
28
|
-
path.setAttribute('d', 'M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z');
|
|
29
|
-
svg.append(path);
|
|
30
|
-
return svg;
|
|
31
|
-
}
|
|
32
|
-
/** YouTube mark from Simple Icons (CC0); notice in docs/licenses/simple-icons.txt. */
|
|
33
|
-
function youtube() {
|
|
34
|
-
const svg = mark();
|
|
35
|
-
const path = document.createElementNS(namespace, 'path');
|
|
36
|
-
svg.setAttribute('viewBox', '0 0 24 24');
|
|
37
|
-
svg.setAttribute('fill', 'currentColor');
|
|
38
|
-
path.setAttribute('d', 'M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z');
|
|
39
|
-
svg.append(path);
|
|
40
|
-
return svg;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* A key, drawn here rather than taken from anywhere. It is not the OpenPGP logo: that mark
|
|
44
|
-
* is somebody's to license and this repository does not ship artwork it cannot account for.
|
|
45
|
-
* A key is also the truer picture, since what was proved is control of one, not membership
|
|
46
|
-
* of an organisation.
|
|
47
|
-
*/
|
|
48
|
-
function key() {
|
|
49
|
-
return lines([
|
|
50
|
-
'M7.4 8.6a3.3 3.3 0 1 0-4.7 4.7 3.3 3.3 0 0 0 4.7-4.7Z',
|
|
51
|
-
'M7.4 8.6 14 2',
|
|
52
|
-
'M11.2 4.8l1.6 1.6',
|
|
53
|
-
'M9.4 6.6l1.6 1.6',
|
|
54
|
-
]);
|
|
55
|
-
}
|
|
56
|
-
/** A globe, drawn here like the key, for a page or a domain on the open web. */
|
|
57
|
-
function globe() {
|
|
58
|
-
return lines([
|
|
59
|
-
'M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Z',
|
|
60
|
-
'M1.5 8h13',
|
|
61
|
-
'M8 1.5C6.1 3.3 5.1 5.5 5.1 8s1 4.7 2.9 6.5',
|
|
62
|
-
'M8 1.5c1.9 1.8 2.9 4 2.9 6.5s-1 4.7-2.9 6.5',
|
|
63
|
-
]);
|
|
64
|
-
}
|
|
65
|
-
/** An envelope, drawn here like the key, for a mailbox that read a code sent to it. */
|
|
66
|
-
function envelope() {
|
|
67
|
-
return lines(['M2 3.5h12v9H2z', 'M2.4 4l5.6 4.6L13.6 4']);
|
|
68
|
-
}
|
|
69
|
-
/** A mark drawn as rounded strokes in the text colour. */
|
|
70
|
-
function lines(paths) {
|
|
71
|
-
const svg = mark();
|
|
72
|
-
svg.setAttribute('fill', 'none');
|
|
73
|
-
for (const d of paths) {
|
|
17
|
+
svg.setAttribute('fill', mark.stroke ? 'none' : 'currentColor');
|
|
18
|
+
for (const d of mark.paths) {
|
|
74
19
|
const path = document.createElementNS(namespace, 'path');
|
|
75
20
|
path.setAttribute('d', d);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
path.setAttribute('stroke-linecap', 'round');
|
|
79
|
-
path.setAttribute('stroke-linejoin', 'round');
|
|
21
|
+
for (const [name, value] of pathAttributes(mark))
|
|
22
|
+
path.setAttribute(name, value);
|
|
80
23
|
svg.append(path);
|
|
81
24
|
}
|
|
82
25
|
return svg;
|
|
83
26
|
}
|
|
84
|
-
/**
|
|
85
|
-
* The mark for a provider, or nothing where there is none. Returning nothing rather than
|
|
86
|
-
* the provider's name keeps this to one job: a caller that also writes the name would
|
|
87
|
-
* otherwise print it twice, and only the caller knows where the name belongs.
|
|
88
|
-
*/
|
|
89
|
-
export function providerMark(provider, method) {
|
|
90
|
-
const marks = {
|
|
91
|
-
github,
|
|
92
|
-
discord,
|
|
93
|
-
youtube,
|
|
94
|
-
openpgp: key,
|
|
95
|
-
email: envelope,
|
|
96
|
-
};
|
|
97
|
-
// A site names its own link-back providers, so those fall back on the method's mark,
|
|
98
|
-
// which a domain shown by its DNS or by a file shares.
|
|
99
|
-
return (marks[provider]?.() ??
|
|
100
|
-
(['backlink', 'dns', 'wellknown'].includes(method ?? '') ? globe() : undefined));
|
|
101
|
-
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -45,6 +45,12 @@ export interface ExternalAccount {
|
|
|
45
45
|
handle: string;
|
|
46
46
|
/** Where a reader goes to see the subject. For a mailbox, its `mailto:` address. */
|
|
47
47
|
profileUrl: string;
|
|
48
|
+
/**
|
|
49
|
+
* When the account was created, in milliseconds, as reported by the provider. It lets a
|
|
50
|
+
* reader tell an old account from one opened just to be linked. Only sign-in providers
|
|
51
|
+
* set it. Once a record has it, later proofs by other methods keep it.
|
|
52
|
+
*/
|
|
53
|
+
createdAt?: number;
|
|
48
54
|
}
|
|
49
55
|
/**
|
|
50
56
|
* How control of one side was shown. A provider is who an account belongs to; a method is
|
|
@@ -96,6 +102,13 @@ export interface Attestation {
|
|
|
96
102
|
* is reconfirmed like any other.
|
|
97
103
|
*/
|
|
98
104
|
confirmedAt: number;
|
|
105
|
+
/** When `recheck()` last tried to read the proof, whether or not the read worked. */
|
|
106
|
+
readAt?: number;
|
|
107
|
+
/**
|
|
108
|
+
* When a recheck first read the proof and found it missing. The proof does not count
|
|
109
|
+
* while this is set. Cleared by the next read that finds it.
|
|
110
|
+
*/
|
|
111
|
+
absentAt?: number;
|
|
99
112
|
}
|
|
100
113
|
/** Each side of a link is attested separately, by different parties under different methods. */
|
|
101
114
|
export interface Attestations {
|
|
@@ -137,9 +150,44 @@ export interface Connection {
|
|
|
137
150
|
* last proof: never renewed by another method, never reread, never verified.
|
|
138
151
|
*/
|
|
139
152
|
retiredAt?: number;
|
|
153
|
+
/** The state last delivered to `onChange`. Absent until the first delivery. */
|
|
154
|
+
reported?: Reported;
|
|
155
|
+
/**
|
|
156
|
+
* When the last delivery to `onChange` started, while it has not been confirmed. Set when
|
|
157
|
+
* a delivery starts and cleared when it succeeds, so a record with it set is delivered
|
|
158
|
+
* again even if its state matches `reported`.
|
|
159
|
+
*/
|
|
160
|
+
reportTriedAt?: number;
|
|
161
|
+
/** The revision given to the last delivery. Each delivery of the record adds one. */
|
|
162
|
+
reportRevision?: number;
|
|
163
|
+
}
|
|
164
|
+
/** The parts of a record that `onChange` reports a change in. */
|
|
165
|
+
export interface Reported {
|
|
166
|
+
status: Status;
|
|
167
|
+
visibility: Visibility;
|
|
168
|
+
expiresAt: number;
|
|
169
|
+
mark?: Mark;
|
|
170
|
+
}
|
|
171
|
+
/** One record whose status, visibility, expiry or mark changed, as `onChange` receives it. */
|
|
172
|
+
export interface Change {
|
|
173
|
+
/** The record as it stands now. */
|
|
174
|
+
evidence: Evidence;
|
|
175
|
+
/** The subject the record belongs to, with its private id. */
|
|
176
|
+
local: LocalAccount;
|
|
177
|
+
/** What the last confirmed delivery reported. Absent until one is confirmed. */
|
|
178
|
+
previous?: Reported;
|
|
179
|
+
/**
|
|
180
|
+
* Counts the deliveries of this record, starting at 1. A higher revision always carries
|
|
181
|
+
* a state read later, across processes too. Keep the last one applied for each record
|
|
182
|
+
* and ignore a change whose revision is not higher: a slow or repeated delivery can
|
|
183
|
+
* arrive after a newer one.
|
|
184
|
+
*/
|
|
185
|
+
revision: number;
|
|
140
186
|
}
|
|
141
187
|
export interface Flow {
|
|
142
188
|
id: string;
|
|
189
|
+
/** Saved DNS setup; grants no authority to finish a flow. */
|
|
190
|
+
dnsDraftId?: string;
|
|
143
191
|
stateHash: string;
|
|
144
192
|
bindingHash: string;
|
|
145
193
|
verifier?: string;
|
|
@@ -155,7 +203,8 @@ export interface Flow {
|
|
|
155
203
|
reason?: string;
|
|
156
204
|
/**
|
|
157
205
|
* The string an artifact must contain. Public by design: the holder publishes it. It is
|
|
158
|
-
* unguessable and per-flow
|
|
206
|
+
* normally unguessable and per-flow. DNS setup can retain the same value across
|
|
207
|
+
* fresh flows for the same subject and operation while propagation is pending.
|
|
159
208
|
*/
|
|
160
209
|
expect?: string;
|
|
161
210
|
/** Whether `expect` was minted for a method that had nothing of the subject's to state. */
|
|
@@ -269,7 +318,16 @@ export interface SiteRead {
|
|
|
269
318
|
/** Whether that read got an answer. One that did not is asked again sooner. */
|
|
270
319
|
answered: boolean;
|
|
271
320
|
}
|
|
321
|
+
/** DNS setup retained across short-lived, authenticated flows for seven days. */
|
|
322
|
+
export interface DnsDraft {
|
|
323
|
+
id: string;
|
|
324
|
+
flowId: string;
|
|
325
|
+
expect: string;
|
|
326
|
+
artifact?: string;
|
|
327
|
+
expiresAt: number;
|
|
328
|
+
}
|
|
272
329
|
export interface Records {
|
|
330
|
+
dnsDrafts: DnsDraft;
|
|
273
331
|
connections: Connection;
|
|
274
332
|
flows: Flow;
|
|
275
333
|
shares: Share;
|
|
@@ -349,8 +407,9 @@ export interface ArtifactProvider {
|
|
|
349
407
|
* Told to the holder verbatim. Must name what they are publishing and where. Returned in
|
|
350
408
|
* pieces so a renderer can set a command as one: a command is copied character for
|
|
351
409
|
* character, and prose that reflows is prose a holder cannot safely copy.
|
|
410
|
+
* `artifact` is the last submitted or suggested location, when one is known.
|
|
352
411
|
*/
|
|
353
|
-
instructions(expect: string): Instruction[];
|
|
412
|
+
instructions(expect: string, artifact?: string): Instruction[];
|
|
354
413
|
/**
|
|
355
414
|
* What the holder is asked to hand back, where the method takes something a holder knows
|
|
356
415
|
* by a better name than "an address". `input` says how to ask for it: `text` where it
|
|
@@ -451,6 +510,14 @@ export interface CodeProvider {
|
|
|
451
510
|
*/
|
|
452
511
|
export declare class Refused extends Error {
|
|
453
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* A proof that was read and is not there: the page, file, gist or DNS answer came back and
|
|
515
|
+
* the expected value was missing. Thrown by `verify` or `prove`. On a recheck it makes the
|
|
516
|
+
* proof unconfirmed at once. Any other error means the read itself failed, and the proof
|
|
517
|
+
* keeps counting until `freshnessMs` runs out.
|
|
518
|
+
*/
|
|
519
|
+
export declare class Absent extends Refused {
|
|
520
|
+
}
|
|
454
521
|
/**
|
|
455
522
|
* A piece of what the holder is told: a paragraph, a paragraph with links in it, or
|
|
456
523
|
* something they run or publish as is.
|
|
@@ -525,6 +592,10 @@ export * from './routes.js';
|
|
|
525
592
|
* telling anyone, so a confirmation is a heartbeat rather than a permanent fact.
|
|
526
593
|
*/
|
|
527
594
|
export declare const freshnessMs: number;
|
|
595
|
+
/** Whether a record has reached its deadline, including the exact expiry instant. */
|
|
596
|
+
export declare function expired(record: {
|
|
597
|
+
expiresAt: number;
|
|
598
|
+
}, now: number): boolean;
|
|
528
599
|
/**
|
|
529
600
|
* `freshness` bounds how stale a published proof may be before it stops counting. A
|
|
530
601
|
* backend that never rechecks must leave it unbounded, since an unread proof going stale
|
|
@@ -612,6 +683,11 @@ export declare function externalId(external: ExternalAccount): string | undefine
|
|
|
612
683
|
* between groups. Nothing for any other subject, whose id is written as it stands.
|
|
613
684
|
*/
|
|
614
685
|
export declare function externalIdGroups(external: ExternalAccount): string[] | undefined;
|
|
686
|
+
/**
|
|
687
|
+
* The text for an account's creation date, such as "Joined 2014-03-02", or undefined if
|
|
688
|
+
* the date is unknown. `day` formats the date, to the day.
|
|
689
|
+
*/
|
|
690
|
+
export declare function externalSince(external: ExternalAccount, day: (time: number) => string): string | undefined;
|
|
615
691
|
/**
|
|
616
692
|
* Where a subject's name may link, or nothing where it has no page of its own. A mailbox's
|
|
617
693
|
* address is `mailto:`, which opens a message to it and shows a reader nothing.
|
package/dist/core/index.js
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export class Refused extends Error {
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* A proof that was read and is not there: the page, file, gist or DNS answer came back and
|
|
9
|
+
* the expected value was missing. Thrown by `verify` or `prove`. On a recheck it makes the
|
|
10
|
+
* proof unconfirmed at once. Any other error means the read itself failed, and the proof
|
|
11
|
+
* keeps counting until `freshnessMs` runs out.
|
|
12
|
+
*/
|
|
13
|
+
export class Absent extends Refused {
|
|
14
|
+
}
|
|
7
15
|
/** Only an artifact provider is holder-paced, and only it needs a url handed back. */
|
|
8
16
|
export function isArtifactProvider(provider) {
|
|
9
17
|
return 'verify' in provider;
|
|
@@ -28,6 +36,10 @@ export * from './routes.js';
|
|
|
28
36
|
* telling anyone, so a confirmation is a heartbeat rather than a permanent fact.
|
|
29
37
|
*/
|
|
30
38
|
export const freshnessMs = 7 * 86400000;
|
|
39
|
+
/** Whether a record has reached its deadline, including the exact expiry instant. */
|
|
40
|
+
export function expired(record, now) {
|
|
41
|
+
return record.expiresAt <= now;
|
|
42
|
+
}
|
|
31
43
|
/**
|
|
32
44
|
* `freshness` bounds how stale a published proof may be before it stops counting. A
|
|
33
45
|
* backend that never rechecks must leave it unbounded, since an unread proof going stale
|
|
@@ -39,7 +51,7 @@ export function status(connection, now, freshness = freshnessMs) {
|
|
|
39
51
|
// Frozen at its last proof, so neither its expiry nor its freshness is read again.
|
|
40
52
|
if (connection.retiredAt !== undefined)
|
|
41
53
|
return 'retired';
|
|
42
|
-
if (now
|
|
54
|
+
if (expired(connection, now))
|
|
43
55
|
return 'expired';
|
|
44
56
|
const main = connection.attestations?.external[0];
|
|
45
57
|
if (main && !fresh(main, now, freshness))
|
|
@@ -52,6 +64,9 @@ export function status(connection, now, freshness = freshnessMs) {
|
|
|
52
64
|
* reverses the moment it reads again.
|
|
53
65
|
*/
|
|
54
66
|
export function fresh(attestation, now, freshness = freshnessMs) {
|
|
67
|
+
// Read and found missing, which is not the same as not having been read lately.
|
|
68
|
+
if (attestation.absentAt !== undefined)
|
|
69
|
+
return false;
|
|
55
70
|
return (!attestation.artifactUrl ||
|
|
56
71
|
attestation.hosted === true ||
|
|
57
72
|
freshness === Infinity ||
|
|
@@ -75,7 +90,7 @@ export function statusLabel(evidence, now) {
|
|
|
75
90
|
// Before expiry: a retired record's approval has long run out, and that is not its state.
|
|
76
91
|
if (evidence.status === 'retired')
|
|
77
92
|
return 'Retired';
|
|
78
|
-
if (evidence.status === 'expired' || evidence
|
|
93
|
+
if (evidence.status === 'expired' || expired(evidence, now))
|
|
79
94
|
return 'Expired';
|
|
80
95
|
return evidence.status === 'unconfirmed' ? 'Unconfirmed' : 'Verified';
|
|
81
96
|
}
|
|
@@ -188,6 +203,15 @@ export function externalId(external) {
|
|
|
188
203
|
export function externalIdGroups(external) {
|
|
189
204
|
return external.kind === 'key' ? (external.id.match(/.{1,4}/g) ?? undefined) : undefined;
|
|
190
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* The text for an account's creation date, such as "Joined 2014-03-02", or undefined if
|
|
208
|
+
* the date is unknown. `day` formats the date, to the day.
|
|
209
|
+
*/
|
|
210
|
+
export function externalSince(external, day) {
|
|
211
|
+
return typeof external.createdAt === 'number' && Number.isFinite(external.createdAt)
|
|
212
|
+
? `Joined ${day(external.createdAt)}`
|
|
213
|
+
: undefined;
|
|
214
|
+
}
|
|
191
215
|
/**
|
|
192
216
|
* Where a subject's name may link, or nothing where it has no page of its own. A mailbox's
|
|
193
217
|
* address is `mailto:`, which opens a message to it and shows a reader nothing.
|