@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.
Files changed (62) hide show
  1. package/README.md +197 -91
  2. package/dist/browser/connect-dialog.js +3 -4
  3. package/dist/browser/evidence-dialog.d.ts +7 -2
  4. package/dist/browser/evidence-dialog.js +62 -26
  5. package/dist/browser/index.js +3 -0
  6. package/dist/browser/mark.d.ts +2 -6
  7. package/dist/browser/mark.js +6 -12
  8. package/dist/browser/provider-mark.d.ts +3 -3
  9. package/dist/browser/provider-mark.js +15 -90
  10. package/dist/core/index.d.ts +78 -2
  11. package/dist/core/index.js +26 -2
  12. package/dist/instance/auth.d.ts +14 -0
  13. package/dist/instance/auth.js +80 -0
  14. package/dist/instance/bin.d.ts +2 -0
  15. package/dist/instance/bin.js +36 -0
  16. package/dist/instance/cloudflare.d.ts +57 -0
  17. package/dist/instance/cloudflare.js +71 -0
  18. package/dist/instance/env.d.ts +51 -0
  19. package/dist/instance/env.js +84 -0
  20. package/dist/instance/index.d.ts +136 -0
  21. package/dist/instance/index.js +777 -0
  22. package/dist/instance/init.d.ts +6 -0
  23. package/dist/instance/init.js +28 -0
  24. package/dist/instance/limits.d.ts +30 -0
  25. package/dist/instance/limits.js +45 -0
  26. package/dist/instance/node.d.ts +13 -0
  27. package/dist/instance/node.js +43 -0
  28. package/dist/instance/page.d.ts +18 -0
  29. package/dist/instance/page.js +30 -0
  30. package/dist/instance/postgres.d.ts +19 -0
  31. package/dist/instance/postgres.js +83 -0
  32. package/dist/instance/routes.d.ts +7 -0
  33. package/dist/instance/routes.js +22 -0
  34. package/dist/instance/serve.d.ts +31 -0
  35. package/dist/instance/serve.js +49 -0
  36. package/dist/instance/sites.d.ts +169 -0
  37. package/dist/instance/sites.js +413 -0
  38. package/dist/instance/starter.d.ts +6 -0
  39. package/dist/instance/starter.js +99 -0
  40. package/dist/instance/store.d.ts +20 -0
  41. package/dist/instance/store.js +11 -0
  42. package/dist/instance/undici.d.ts +8 -0
  43. package/dist/instance/undici.js +9 -0
  44. package/dist/marks.d.ts +44 -0
  45. package/dist/marks.js +98 -0
  46. package/dist/server/discord.js +3 -0
  47. package/dist/server/domain.js +37 -10
  48. package/dist/server/github-gist.js +16 -4
  49. package/dist/server/github.js +3 -0
  50. package/dist/server/index.js +20 -47
  51. package/dist/server/link.js +9 -4
  52. package/dist/server/render.d.ts +49 -0
  53. package/dist/server/render.js +104 -0
  54. package/dist/server/service.d.ts +44 -1
  55. package/dist/server/service.js +225 -19
  56. package/dist/server/style.d.ts +1 -1
  57. package/dist/server/style.js +132 -2
  58. package/dist/server/youtube.js +5 -0
  59. package/dist/verily.js +155 -124
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.js +1 -1
  62. package/package.json +30 -2
@@ -1,5 +1,4 @@
1
- import { externalId, externalIdGroups, externalLink, externalName } from '../core/index.js';
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 = accountCard(logo
437
+ const external = namedCard(logo
439
438
  ? [logo, document.createTextNode(flow.provider.name)]
440
- : [document.createTextNode(flow.provider.name)], externalName(flow.external), externalIdGroups(flow.external) ?? externalId(flow.external), externalLink(flow.external));
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 .caution { display: inline-block; margin-left: 4px; color: #a15c00; cursor: help; vertical-align: -1.5px; }\n .caution svg { display: block; width: 12px; height: 12px; }\n .bad { color: #b3261e; font-weight: 650; text-decoration: underline dotted; text-underline-offset: 3px; cursor: help; }\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 { color: #6b786f; }\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";
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
- .caution { display: inline-block; margin-left: 4px; color: #a15c00; cursor: help; vertical-align: -1.5px; }
44
- .caution svg { display: block; width: 12px; height: 12px; }
45
- .bad { color: #b3261e; font-weight: 650; text-decoration: underline dotted; text-underline-offset: 3px; cursor: help; }
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
- card.append(title, value);
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
- * Says a record is signed, in a word beside its verifier. The word opens the verifier's
424
- * own page for the record at what it says of the signature, where the signed record is to be
425
- * had: nothing is downloaded from the card. The signed record is checked here against the
426
- * verifier's keys: a tick once it has checked, and a warning in the word's place if it
427
- * fails, the one thing on a card that is a warning. `warned` is told each time that warning
428
- * is drawn, on the first check or on a later one, so whatever folds the card away opens.
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
- const warning = node('span', 'invalid signature', 'bad');
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(document.createTextNode(' · '), warning);
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
- link.textContent = 'signed ✓';
447
- // Names where the keys came from, which is what the tick answers for.
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(document.createTextNode(' · '), link);
485
+ mark.replaceChildren(link);
450
486
  return;
451
487
  }
452
- // No verdict was reached. The record is signed and says so, with a warning beside the
453
- // word in place of a tick, and why is said on hover. A check that only ran out of time
454
- // or could not read what it needed is made again when the dialog next reads.
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(document.createTextNode(' · '), link, caution);
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 = accountCard(
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)], externalName(evidence.external),
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),
@@ -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`) &&
@@ -1,9 +1,5 @@
1
- /**
2
- * How sure the mark is: `current` for a verification that holds, `inactive` for one that
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.
@@ -1,8 +1,4 @@
1
- const tones = {
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', '-4 -4 264 264');
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', 'M40 36 128 220 216 36');
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
- path.setAttribute('pathLength', '176');
38
- path.setAttribute('stroke-dasharray', '108 176');
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 rather than
3
- * the provider's name keeps this to one job: a caller that also writes the name would
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
- function mark() {
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', '0 0 16 16');
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
- return svg;
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
- path.setAttribute('stroke', 'currentColor');
77
- path.setAttribute('stroke-width', '1.6');
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
- }
@@ -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, so an artifact made for one flow cannot complete another.
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.
@@ -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 >= connection.expiresAt)
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.expiresAt <= now)
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.