@bkazemi/verily 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/verily.js CHANGED
@@ -9574,10 +9574,12 @@ var Verily = (() => {
9574
9574
  var browser_exports = {};
9575
9575
  __export(browser_exports, {
9576
9576
  Refused: () => Refused,
9577
+ accountsOf: () => accountsOf,
9577
9578
  attestationLabel: () => attestationLabel,
9578
9579
  dialogPath: () => dialogPath,
9579
9580
  dialogRoute: () => dialogRoute,
9580
9581
  externalId: () => externalId,
9582
+ externalIdGroups: () => externalIdGroups,
9581
9583
  externalLink: () => externalLink,
9582
9584
  externalName: () => externalName,
9583
9585
  fresh: () => fresh,
@@ -9588,6 +9590,7 @@ var Verily = (() => {
9588
9590
  isRedirectProvider: () => isRedirectProvider,
9589
9591
  lastProved: () => lastProved,
9590
9592
  localSide: () => localSide,
9593
+ place: () => place,
9591
9594
  presentConnections: () => presentConnections,
9592
9595
  proofTitle: () => proofTitle,
9593
9596
  providerMethod: () => providerMethod,
@@ -9747,6 +9750,29 @@ var Verily = (() => {
9747
9750
  if (evidence.status === "expired" || evidence.expiresAt <= now) return "Expired";
9748
9751
  return evidence.status === "unconfirmed" ? "Unconfirmed" : "Verified";
9749
9752
  }
9753
+ function place(account) {
9754
+ if (account.status === "retired") return 3;
9755
+ return account.mark === "preferred" ? 0 : account.mark === "unused" ? 2 : 1;
9756
+ }
9757
+ function accountsOf(records, now) {
9758
+ const when = (e2) => e2.connectedAt ?? e2.approvedAt;
9759
+ const byAccount = /* @__PURE__ */ new Map();
9760
+ for (const record2 of records) {
9761
+ const key2 = JSON.stringify([record2.provider, record2.external.id]);
9762
+ byAccount.set(key2, [...byAccount.get(key2) ?? [], record2]);
9763
+ }
9764
+ return [...byAccount.values()].map((unsorted) => {
9765
+ const held = [...unsorted].sort((a2, b2) => when(a2) - when(b2));
9766
+ const verified = held.filter((e2) => e2.status === "verified" && e2.expiresAt > now);
9767
+ const lead = verified[0] ?? held.reduce((a2, b2) => b2.approvedAt > a2.approvedAt ? b2 : a2);
9768
+ const external = [...lead.attestations.external];
9769
+ for (const other of verified.slice(1))
9770
+ for (const attestation of other.attestations.external)
9771
+ if (!external.some((shown3) => shown3.method === attestation.method))
9772
+ external.push(attestation);
9773
+ return { lead, held, verified, external, connectedAt: when(held[0]) };
9774
+ }).sort((a2, b2) => place(a2.lead) - place(b2.lead) || a2.connectedAt - b2.connectedAt);
9775
+ }
9750
9776
  function localSide(local, fallback) {
9751
9777
  const siteName = local.siteName ?? fallback;
9752
9778
  if (local.kind === "site") return { heading: "Website", value: siteName };
@@ -9776,6 +9802,9 @@ var Verily = (() => {
9776
9802
  function externalId(external) {
9777
9803
  return external.kind === "mailbox" || external.kind === "domain" ? void 0 : external.id;
9778
9804
  }
9805
+ function externalIdGroups(external) {
9806
+ return external.kind === "key" ? external.id.match(/.{1,4}/g) ?? void 0 : void 0;
9807
+ }
9779
9808
  function externalLink(external) {
9780
9809
  return external.kind === "mailbox" ? void 0 : external.profileUrl;
9781
9810
  }
@@ -10067,11 +10096,12 @@ var Verily = (() => {
10067
10096
  function badgeShown(element) {
10068
10097
  return intact(element) && shown.has(element);
10069
10098
  }
10070
- function renderKey(evidence, current, label, more, linked, rows, peek) {
10099
+ function renderKey(evidence, current, label, more, linked, rows, peek, href) {
10071
10100
  return JSON.stringify([
10072
10101
  more,
10073
10102
  linked,
10074
10103
  peek,
10104
+ href,
10075
10105
  rows.map((row) => [row.provider, row.providerName, externalName(row.external)]),
10076
10106
  evidence.provider,
10077
10107
  evidence.providerName,
@@ -10122,14 +10152,15 @@ var Verily = (() => {
10122
10152
  more = 0,
10123
10153
  linked = true,
10124
10154
  rows = [],
10125
- peek = false
10155
+ peek = false,
10156
+ href = evidence.evidenceUrl
10126
10157
  } = {}) {
10127
10158
  const provider = evidence.providerName;
10128
10159
  const current = evidence.status === "verified" && evidence.expiresAt > Date.now();
10129
10160
  const retired = evidence.status === "retired";
10130
10161
  const state = current ? "verified" : retired ? "retired" : evidence.status === "revoked" ? "revoked" : "expired";
10131
10162
  const label = statusLabel(evidence, Date.now());
10132
- const key2 = renderKey(evidence, current, label, more, linked, rows, peek);
10163
+ const key2 = renderKey(evidence, current, label, more, linked, rows, peek, href);
10133
10164
  if (intact(element) && shown.get(element) === key2) return null;
10134
10165
  const handle = externalName(evidence.external);
10135
10166
  const { pill: badge, mark: mark2 } = frame(element, state, !linked);
@@ -10138,7 +10169,7 @@ var Verily = (() => {
10138
10169
  const others = `${named}${unnamed ? ` and ${unnamed} more` : ""}`;
10139
10170
  paintMark(mark2, current ? "current" : retired ? "pending" : "inactive");
10140
10171
  if (linked) {
10141
- badge.href = evidence.evidenceUrl;
10172
+ badge.href = href;
10142
10173
  badge.rel = "noreferrer";
10143
10174
  } else badge.type = "button";
10144
10175
  badge.setAttribute(
@@ -10213,7 +10244,7 @@ var Verily = (() => {
10213
10244
  } catch {
10214
10245
  return;
10215
10246
  }
10216
- place(panel, pill, listed, unnamed);
10247
+ place2(panel, pill, listed, unnamed);
10217
10248
  globalThis.addEventListener?.("scroll", hide, true);
10218
10249
  };
10219
10250
  panel.setAttribute("popover", "manual");
@@ -10235,7 +10266,7 @@ var Verily = (() => {
10235
10266
  if (event.key === "Escape") hide();
10236
10267
  };
10237
10268
  }
10238
- function place(panel, pill, listed, unnamed) {
10269
+ function place2(panel, pill, listed, unnamed) {
10239
10270
  const gap = 6;
10240
10271
  const edge = 8;
10241
10272
  const at2 = pill.getBoundingClientRect();
@@ -10485,7 +10516,7 @@ ${claims(evidence)}`;
10485
10516
  }
10486
10517
 
10487
10518
  // src/version.ts
10488
- var version = "v0.5.0";
10519
+ var version = "v0.6.0";
10489
10520
 
10490
10521
  // src/browser/evidence-dialog.ts
10491
10522
  function stampLink() {
@@ -10502,8 +10533,16 @@ ${claims(evidence)}`;
10502
10533
  h2 { margin: 0; font-size: 18px; font-weight: 650; letter-spacing: -.3px; }
10503
10534
  button { width: 32px; height: 32px; border: 1px solid #dce2de; border-radius: 8px; background: #fff; color: #52645a; font: 20px system-ui, sans-serif; cursor: pointer; }
10504
10535
  button:hover { background: #f2f5f1; }
10536
+ .icon { display: grid; place-items: center; padding: 0; }
10537
+ .icon svg { width: 16px; height: 16px; }
10538
+ .title { display: flex; align-items: center; gap: 6px; min-width: 0; }
10539
+ .all { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; color: #52645a; }
10540
+ .all:hover { background: #f2f5f1; }
10541
+ .all svg { width: 16px; height: 16px; }
10505
10542
  a { color: #245f43; text-underline-offset: 3px; overflow-wrap: anywhere; }
10506
- a:focus-visible, button:focus-visible { outline: 2px solid #357ce5; outline-offset: 3px; }
10543
+ a:focus-visible, button:focus-visible, summary:focus-visible { outline: 2px solid #357ce5; outline-offset: 3px; }
10544
+ /* An element given its own display would otherwise go on showing while hidden. */
10545
+ [hidden] { display: none !important; }
10507
10546
  .summary { display: flex; align-items: center; gap: 8px; margin-top: 16px; }
10508
10547
  .mark { width: 36px; height: 36px; flex-shrink: 0; }
10509
10548
  .provider { width: 14px; height: 14px; }
@@ -10515,7 +10554,7 @@ ${claims(evidence)}`;
10515
10554
  .spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin .7s linear infinite; }
10516
10555
  @keyframes spin { to { transform: rotate(360deg); } }
10517
10556
  @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }
10518
- .caution { display: inline-block; margin-left: 4px; color: #a15c00; cursor: help; vertical-align: middle; }
10557
+ .caution { display: inline-block; margin-left: 4px; color: #a15c00; cursor: help; vertical-align: -1.5px; }
10519
10558
  .caution svg { display: block; width: 12px; height: 12px; }
10520
10559
  .bad { color: #b3261e; font-weight: 650; text-decoration: underline dotted; text-underline-offset: 3px; cursor: help; }
10521
10560
  .account { padding: 14px 16px; border: 1px solid #e0e6df; border-radius: 10px; margin-top: 12px; }
@@ -10524,6 +10563,9 @@ ${claims(evidence)}`;
10524
10563
  /* The account's own name carries that weight; a link inside a line of prose does not. */
10525
10564
  .summary a, .method a, .additional a { font: inherit; }
10526
10565
  .reference { margin-top: 2px; }
10566
+ /* A fingerprint in fours: it breaks between groups, and copies as one unbroken word. */
10567
+ .fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }
10568
+ .fingerprint span { display: inline-block; margin-right: .5ch; }
10527
10569
  .method { margin-top: 8px; }
10528
10570
  /* In the corner the heading leaves empty, apart from everything that was checked. */
10529
10571
  .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; }
@@ -10533,6 +10575,15 @@ ${claims(evidence)}`;
10533
10575
  .account.retired > a, .account.retired > strong { color: #6b786f; }
10534
10576
  .account.retired .mark { opacity: .45; }
10535
10577
  .group { margin: 20px 0 0; color: #6b786f; font-size: 11px; font-weight: 550; }
10578
+ summary.group { display: flex; align-items: center; gap: 4px; width: fit-content; border-radius: 4px; list-style: none; cursor: pointer; }
10579
+ summary::-webkit-details-marker { display: none; }
10580
+ .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; }
10581
+ .fold svg, summary svg { width: 12px; height: 12px; }
10582
+ .fold[aria-expanded="true"] svg, details[open] > summary svg { transform: rotate(90deg); }
10583
+ @media (prefers-reduced-motion: no-preference) { .fold svg, summary svg { transition: transform .15s; } }
10584
+ /* The fold and what the holder may do share a line, until a question needs the whole of it. */
10585
+ .bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
10586
+ .account .bar .row { margin: 0 0 0 auto; }
10536
10587
  .additional { margin-top: 2px; padding-left: 12px; }
10537
10588
  .joiner { display: block; width: 20px; height: 20px; margin: 8px auto -4px; color: #90a096; }
10538
10589
  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; }
@@ -10547,6 +10598,14 @@ ${claims(evidence)}`;
10547
10598
  .row { display: flex; justify-content: end; gap: 8px; margin-top: 16px; }
10548
10599
  .account .row { align-items: center; justify-content: start; flex-wrap: wrap; margin-top: 12px; }
10549
10600
  .account .action { padding: 5px 10px; font-size: 12px; }
10601
+ .account .action.dots { display: grid; place-items: center; align-self: stretch; width: 30px; padding: 0; }
10602
+ .dots svg { width: 16px; height: 16px; }
10603
+ /* Placed under its button each time it opens, above the dialog and clipped by none of it. */
10604
+ .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; }
10605
+ .menu:popover-open { display: grid; }
10606
+ .account .menu .action { border: 0; border-radius: 6px; padding: 6px 10px; font-weight: 550; }
10607
+ .menu .danger { color: #b3261e; }
10608
+ .menu hr { width: 100%; margin: 4px 0; border: 0; border-top: 1px solid #e5e9e3; }
10550
10609
  footer { display: flex; align-items: center; justify-content: start; gap: 6px; margin: 20px 0 -8px; color: #9aa9a0; font-size: 11px; }
10551
10610
  footer a { display: flex; color: inherit; }
10552
10611
  .logo { display: block; height: 13px; }
@@ -10564,6 +10623,58 @@ ${claims(evidence)}`;
10564
10623
  anchor.target = "_blank";
10565
10624
  return anchor;
10566
10625
  }
10626
+ function subjectUrl(e2) {
10627
+ const url = new URL(e2.evidenceUrl);
10628
+ const path = url.pathname.replace(/\/connections\/[^/]+$/, "/published");
10629
+ if (path === url.pathname) return e2.evidenceUrl;
10630
+ url.pathname = path;
10631
+ url.search = `site=${encodeURIComponent(e2.siteName)}&reference=${encodeURIComponent(e2.local.reference)}`;
10632
+ return url.href;
10633
+ }
10634
+ function awayMark() {
10635
+ const namespace2 = "http://www.w3.org/2000/svg";
10636
+ const svg = document.createElementNS(namespace2, "svg");
10637
+ svg.setAttribute("viewBox", "0 0 24 24");
10638
+ svg.setAttribute("fill", "none");
10639
+ svg.setAttribute("aria-hidden", "true");
10640
+ svg.setAttribute("focusable", "false");
10641
+ for (const d2 of [
10642
+ "M14 5h5v5",
10643
+ "M19 5l-8 8",
10644
+ "M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"
10645
+ ]) {
10646
+ const path = document.createElementNS(namespace2, "path");
10647
+ path.setAttribute("d", d2);
10648
+ path.setAttribute("stroke", "currentColor");
10649
+ path.setAttribute("stroke-width", "2");
10650
+ path.setAttribute("stroke-linecap", "round");
10651
+ path.setAttribute("stroke-linejoin", "round");
10652
+ svg.append(path);
10653
+ }
10654
+ return svg;
10655
+ }
10656
+ function strokes(box, width, paths) {
10657
+ const namespace2 = "http://www.w3.org/2000/svg";
10658
+ const svg = document.createElementNS(namespace2, "svg");
10659
+ svg.setAttribute("viewBox", `0 0 ${box} ${box}`);
10660
+ svg.setAttribute("fill", "none");
10661
+ svg.setAttribute("aria-hidden", "true");
10662
+ svg.setAttribute("focusable", "false");
10663
+ for (const d2 of paths) {
10664
+ const path = document.createElementNS(namespace2, "path");
10665
+ path.setAttribute("d", d2);
10666
+ path.setAttribute("stroke", "currentColor");
10667
+ path.setAttribute("stroke-width", String(width));
10668
+ path.setAttribute("stroke-linecap", "round");
10669
+ path.setAttribute("stroke-linejoin", "round");
10670
+ svg.append(path);
10671
+ }
10672
+ return svg;
10673
+ }
10674
+ var foldMark = () => strokes(16, 1.8, ["M6 3.5 10.5 8 6 12.5"]);
10675
+ var moreMark = () => strokes(16, 2.4, ["M3 8h.01", "M8 8h.01", "M13 8h.01"]);
10676
+ var closeMark = () => strokes(16, 1.8, ["M4 4l8 8", "M12 4l-8 8"]);
10677
+ var backMark = () => strokes(16, 1.8, ["M10 3.5 5.5 8 10 12.5"]);
10567
10678
  function linkMark() {
10568
10679
  const namespace2 = "http://www.w3.org/2000/svg";
10569
10680
  const svg = document.createElementNS(namespace2, "svg");
@@ -10609,12 +10720,63 @@ ${claims(evidence)}`;
10609
10720
  title.append(...heading);
10610
10721
  if (value instanceof HTMLAnchorElement) outward(value, url);
10611
10722
  card.append(title, value);
10612
- if (reference) card.append(node("div", reference, "muted reference"));
10723
+ if (typeof reference === "string") card.append(node("div", reference, "muted reference"));
10724
+ else if (reference) {
10725
+ const groups = node("div", "", "muted reference fingerprint");
10726
+ groups.append(...reference.map((group) => node("span", group)));
10727
+ card.append(groups);
10728
+ }
10613
10729
  card.append(...extra);
10614
10730
  return card;
10615
10731
  }
10616
10732
  var retiringNote = "Retiring keeps this record as history. It will no longer read as verified, and who can read it cannot be changed afterwards.";
10617
10733
  var removable = (evidence) => evidence.links ?? (evidence.status === "revoked" ? [] : [evidence.id]);
10734
+ function actionsMenu(more, choices) {
10735
+ const menu = node("div", "", "menu");
10736
+ if (typeof menu.showPopover !== "function") return void 0;
10737
+ const label = "More actions";
10738
+ more.className = "action dots";
10739
+ more.title = label;
10740
+ more.setAttribute("aria-label", label);
10741
+ more.setAttribute("aria-haspopup", "menu");
10742
+ more.append(moreMark());
10743
+ more.popoverTargetElement = menu;
10744
+ menu.setAttribute("popover", "auto");
10745
+ menu.setAttribute("role", "menu");
10746
+ for (const choice of choices) choice.setAttribute("role", "menuitem");
10747
+ menu.append(...choices.slice(0, -1), node("hr"), choices.at(-1));
10748
+ const shut = () => {
10749
+ try {
10750
+ if (menu.matches(":popover-open")) menu.hidePopover();
10751
+ } catch {
10752
+ }
10753
+ };
10754
+ const gap = 4;
10755
+ const edge = 8;
10756
+ menu.addEventListener("beforetoggle", (event) => {
10757
+ if (event.newState !== "open") return;
10758
+ const at2 = more.getBoundingClientRect();
10759
+ menu.style.top = `${at2.bottom + gap}px`;
10760
+ menu.style.right = `${document.documentElement.clientWidth - at2.right}px`;
10761
+ });
10762
+ menu.addEventListener("toggle", (event) => {
10763
+ if (event.newState !== "open") return;
10764
+ const { height, bottom } = menu.getBoundingClientRect();
10765
+ if (bottom > innerHeight - edge)
10766
+ menu.style.top = `${Math.max(edge, more.getBoundingClientRect().top - gap - height)}px`;
10767
+ menu.closest("dialog")?.addEventListener("scroll", shut, { once: true });
10768
+ choices[0].focus();
10769
+ });
10770
+ menu.addEventListener("click", shut);
10771
+ menu.addEventListener("keydown", (event) => {
10772
+ const step = { ArrowDown: 1, ArrowUp: -1 }[event.key];
10773
+ if (!step) return;
10774
+ event.preventDefault();
10775
+ const at2 = choices.indexOf(event.target);
10776
+ choices[(at2 + step + choices.length) % choices.length].focus();
10777
+ });
10778
+ return menu;
10779
+ }
10618
10780
  function manageRow(evidence, manage) {
10619
10781
  const row = node("div", "", "row");
10620
10782
  const act = (label, danger = false) => {
@@ -10657,7 +10819,11 @@ ${claims(evidence)}`;
10657
10819
  retire
10658
10820
  );
10659
10821
  }
10660
- row.replaceChildren(renew, ...listing, remove, ...note ? [node("span", note, "muted")] : []);
10822
+ const said = note ? [node("span", note, "muted")] : [];
10823
+ const more = act("");
10824
+ const menu = listing.length ? actionsMenu(more, [...listing, remove]) : void 0;
10825
+ if (menu) row.replaceChildren(renew, more, menu, ...said);
10826
+ else row.replaceChildren(renew, ...listing, remove, ...said);
10661
10827
  };
10662
10828
  const retiring = () => {
10663
10829
  const cancel = act("Cancel");
@@ -10700,7 +10866,7 @@ ${claims(evidence)}`;
10700
10866
  )
10701
10867
  );
10702
10868
  }
10703
- function signedMark(evidence, later) {
10869
+ function signedMark(evidence, later, warned) {
10704
10870
  const mark2 = node("span");
10705
10871
  const link = outward(node("a", "signed"), `${evidence.evidenceUrl}#signed`);
10706
10872
  const see = "See the signature (opens in a new tab)";
@@ -10711,6 +10877,7 @@ ${claims(evidence)}`;
10711
10877
  warning.setAttribute("role", "alert");
10712
10878
  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.`;
10713
10879
  mark2.replaceChildren(document.createTextNode(" \xB7 "), warning);
10880
+ warned();
10714
10881
  return;
10715
10882
  }
10716
10883
  if (first?.state === "valid" && results.every((result) => result.state === "valid")) {
@@ -10752,7 +10919,25 @@ ${claims(evidence)}`;
10752
10919
  }
10753
10920
  return svg;
10754
10921
  }
10922
+ function foldToggle(folded, fold) {
10923
+ const toggle = node("button", "", "fold");
10924
+ let shown3 = fold.open;
10925
+ const set = (open) => {
10926
+ shown3 = open;
10927
+ for (const part of folded) part.hidden = !open;
10928
+ toggle.setAttribute("aria-expanded", String(open));
10929
+ };
10930
+ toggle.type = "button";
10931
+ toggle.append(foldMark(), document.createTextNode("Details"));
10932
+ set(fold.open);
10933
+ toggle.onclick = () => {
10934
+ set(!shown3);
10935
+ fold.toggle(shown3);
10936
+ };
10937
+ return toggle;
10938
+ }
10755
10939
  function externalCard(evidence, manage, later = () => {
10940
+ }, fold, warned = () => {
10756
10941
  }) {
10757
10942
  const current = evidence.status === "verified" && evidence.expiresAt > Date.now();
10758
10943
  const status2 = statusLabel(evidence, Date.now());
@@ -10765,7 +10950,7 @@ ${claims(evidence)}`;
10765
10950
  verifier.title = `View this record at ${evidence.verifierName} (opens in a new tab)`;
10766
10951
  attribution.append(verifier);
10767
10952
  } else attribution.append(document.createTextNode(evidence.verifierName));
10768
- if (evidence.signedUrl) attribution.append(signedMark(evidence, later));
10953
+ if (evidence.signedUrl) attribution.append(signedMark(evidence, later, warned));
10769
10954
  copy.append(node("div", status2, "state"), attribution);
10770
10955
  const retired = evidence.status === "retired";
10771
10956
  summary.append(verificationMark(current ? "current" : retired ? "pending" : "inactive"), copy);
@@ -10788,6 +10973,13 @@ ${claims(evidence)}`;
10788
10973
  dates.append(node("dt", label), node("dd", moment(time)));
10789
10974
  }
10790
10975
  const names2 = { site: evidence.siteName, provider };
10976
+ const row = manage && removable(evidence).length ? [manageRow(evidence, manage)] : [];
10977
+ const bar = node("div", "", "bar");
10978
+ const methods = [
10979
+ ...attestationNote(main, names2),
10980
+ ...evidence.attestations.external.slice(1).flatMap((a2) => attestationNote(a2, names2, true))
10981
+ ];
10982
+ if (fold) bar.append(foldToggle([...methods, dates], fold), ...row);
10791
10983
  const logo2 = providerMark(evidence.provider, evidence.attestations.external[0].method);
10792
10984
  const card = accountCard(
10793
10985
  // The mark and the name, or just the name. A mark that falls back to writing the name
@@ -10795,16 +10987,14 @@ ${claims(evidence)}`;
10795
10987
  logo2 ? [logo2, document.createTextNode(provider)] : [document.createTextNode(provider)],
10796
10988
  externalName(evidence.external),
10797
10989
  // A mailbox's address or a domain is its name already, so there is no second identifier.
10798
- externalId(evidence.external),
10990
+ externalIdGroups(evidence.external) ?? externalId(evidence.external),
10799
10991
  externalLink(evidence.external),
10800
10992
  // Status first, then how it was shown, then when. The proof explains the state
10801
10993
  // above it, so it cannot sit before that state has been given.
10802
10994
  summary,
10803
- ...attestationNote(main, names2),
10804
- ...evidence.attestations.external.slice(1).flatMap((a2) => attestationNote(a2, names2, true)),
10995
+ ...fold ? [] : methods,
10805
10996
  ...evidence.status === "revoked" || retired ? [] : evidence.mark === "preferred" ? [node("div", "Preferred", "listing preferred")] : evidence.mark === "unused" ? [node("div", "No longer used", "listing")] : [],
10806
- dates,
10807
- ...manage && removable(evidence).length ? [manageRow(evidence, manage)] : []
10997
+ ...fold ? [bar, ...methods, dates] : [dates, ...row]
10808
10998
  );
10809
10999
  const surface = retired ? "account retired" : "account";
10810
11000
  card.className = surface;
@@ -10832,7 +11022,7 @@ ${claims(evidence)}`;
10832
11022
  row.append(add);
10833
11023
  return row;
10834
11024
  }
10835
- function render(content, records, manage, later) {
11025
+ function render(content, records, manage, later, folds = { open: false, cards: /* @__PURE__ */ new Set() }) {
10836
11026
  const [first] = records;
10837
11027
  const local = localSide(first.local, first.siteName);
10838
11028
  const localCard = accountCard(
@@ -10849,15 +11039,40 @@ ${claims(evidence)}`;
10849
11039
  })
10850
11040
  );
10851
11041
  const retired = records.filter((record2) => record2.status === "retired");
11042
+ const listed = records.filter((record2) => record2.status !== "retired");
11043
+ const cards = (among, warned) => among.map((record2) => {
11044
+ const key2 = `${record2.provider}
11045
+ ${record2.external.id}`;
11046
+ return externalCard(
11047
+ record2,
11048
+ manage,
11049
+ later,
11050
+ among.length > 1 ? {
11051
+ open: folds.cards.has(key2) !== folds.open,
11052
+ toggle: (open) => void (open === folds.open ? folds.cards.delete(key2) : folds.cards.add(key2))
11053
+ } : void 0,
11054
+ warned
11055
+ );
11056
+ });
11057
+ const history = node("details", "", "history");
11058
+ const count = node("summary", "", "group");
11059
+ count.append(foldMark(), document.createTextNode(`Retired accounts (${retired.length})`));
11060
+ history.append(
11061
+ count,
11062
+ ...cards(retired, () => {
11063
+ history.open = true;
11064
+ })
11065
+ );
11066
+ history.open = folds.retired ?? !listed.length;
11067
+ count.addEventListener("click", () => {
11068
+ folds.retired = !history.open;
11069
+ });
10852
11070
  content.replaceChildren(
10853
11071
  localCard,
10854
11072
  linkMark(),
10855
- ...records.filter((record2) => record2.status !== "retired").map((record2) => externalCard(record2, manage, later)),
11073
+ ...cards(listed),
10856
11074
  ...manage ? [addRow(manage)] : [],
10857
- ...retired.length ? [
10858
- node("h3", "Retired accounts", "group"),
10859
- ...retired.map((record2) => externalCard(record2, manage, later))
10860
- ] : [],
11075
+ ...retired.length ? [history] : [],
10861
11076
  // Nothing below the cards may name a provider. Approval, method and dates belong to
10862
11077
  // the card they came from, and a second provider on this subject gets its own card.
10863
11078
  node(
@@ -10867,7 +11082,7 @@ ${claims(evidence)}`;
10867
11082
  )
10868
11083
  );
10869
11084
  }
10870
- function openEvidenceDialog(opener, load2, opened, arrange2 = (records) => records, manage) {
11085
+ function openEvidenceDialog(opener, load2, opened, arrange2 = (records) => records, manage, detailsOpen = false) {
10871
11086
  const existing = openDialogs.get(opener);
10872
11087
  if (existing?.open) {
10873
11088
  existing.focus();
@@ -10880,7 +11095,9 @@ ${claims(evidence)}`;
10880
11095
  root.adoptedStyleSheets = [sheet2];
10881
11096
  const dialog = node("dialog");
10882
11097
  const heading = node("h2", "Verification details");
10883
- const close = node("button", "\xD7");
11098
+ const close = node("button", "", "icon");
11099
+ const all = node("a", "", "all");
11100
+ const title = node("div", "", "title");
10884
11101
  const header = node("header");
10885
11102
  const stamp = node("footer");
10886
11103
  const content = node("div", "Checking verification\u2026");
@@ -10890,13 +11107,18 @@ ${claims(evidence)}`;
10890
11107
  close.setAttribute("aria-label", "Close verification details");
10891
11108
  content.setAttribute("aria-live", "polite");
10892
11109
  stamp.append(stampLink(), node("span", version));
10893
- header.append(heading, close);
11110
+ all.append(awayMark());
11111
+ close.append(closeMark());
11112
+ close.autofocus = true;
11113
+ title.append(heading);
11114
+ header.append(title, close);
10894
11115
  dialog.append(header, content, stamp);
10895
11116
  const removed = /* @__PURE__ */ new Map();
10896
11117
  const listed = [];
10897
11118
  const listing = (record2) => JSON.stringify([record2.status, record2.mark, record2.retiredAt, record2.approvedAt]);
10898
11119
  const listings = (given) => new Map([given].flat().map((record2) => [record2.id, listing(record2)]));
10899
11120
  let held;
11121
+ const folds = { open: detailsOpen, cards: /* @__PURE__ */ new Set() };
10900
11122
  const relisted = (evidence) => {
10901
11123
  let record2 = evidence;
10902
11124
  for (const { ids, as: as2, at: at2 } of listed) {
@@ -10965,11 +11187,19 @@ ${claims(evidence)}`;
10965
11187
  (e2) => removed.has(e2.id) && e2.status !== "revoked" ? { ...e2, status: "revoked", revokedAt: removed.get(e2.id) } : relisted(standing(e2))
10966
11188
  )
10967
11189
  );
11190
+ const listable = records.filter((e2) => e2.visibility === "public" && e2.status !== "revoked");
11191
+ title.replaceChildren(heading, ...listable.length > 1 ? [all] : []);
11192
+ if (listable.length > 1) {
11193
+ const label = `View all on ${listable[0].verifierName} (opens in a new tab)`;
11194
+ outward(all, subjectUrl(listable[0]));
11195
+ all.title = label;
11196
+ all.setAttribute("aria-label", label);
11197
+ }
10968
11198
  const key2 = JSON.stringify(records.map((e2) => [e2, statusLabel(e2, Date.now())]));
10969
11199
  if (key2 === drawn) return;
10970
11200
  drawn = key2;
10971
11201
  retries.clear();
10972
- render(content, records, actions, (retry) => retries.add(retry));
11202
+ render(content, records, actions, (retry) => retries.add(retry), folds);
10973
11203
  };
10974
11204
  const watch = (given) => {
10975
11205
  clearTimeout(expiryTimer);
@@ -11048,13 +11278,15 @@ ${claims(evidence)}`;
11048
11278
  /* The block scrolls and its frame does not, so the copy button stays in its corner. */
11049
11279
  .code { position: relative; margin: 10px 0; }
11050
11280
  pre { margin: 0; padding: 10px 64px 10px 12px; border: 1px solid #e0e6df; border-radius: 8px; background: #f6f8f5; font: 12px/1.5 ui-monospace, monospace; white-space: pre; overflow-x: auto; }
11051
- .code .action { position: absolute; top: 6px; right: 6px; padding: 3px 8px; font-size: 11px; }
11281
+ .code .action { position: absolute; top: 9px; right: 9px; padding: 3px 8px; font-size: 11px; }
11052
11282
  label { display: grid; gap: 4px; margin-top: 12px; font-weight: 600; }
11053
11283
  input[type=url], input[type=email], input[type=tel], input[type=text], textarea { width: 100%; padding: 8px 10px; border: 1px solid #cfd8d2; border-radius: 8px; font: 13px/1.4 system-ui, sans-serif; color: inherit; background: #fff; }
11054
11284
  textarea { font-family: ui-monospace, monospace; font-size: 12px; resize: vertical; }
11055
11285
  fieldset { margin: 16px 0 0; padding: 10px 14px; border: 1px solid #e0e6df; border-radius: 10px; }
11056
11286
  legend { padding: 0 4px; color: #6b786f; font-size: 11px; font-weight: 550; }
11057
11287
  fieldset label { display: flex; align-items: baseline; gap: 8px; margin-top: 4px; font-weight: 500; }
11288
+ /* A radio has no baseline to speak of, so it is set in the middle of the label's first line. */
11289
+ fieldset input { flex-shrink: 0; align-self: flex-start; width: 13px; height: 13px; font: inherit; margin-top: 4px; margin-top: calc((1lh - 13px) / 2); margin-bottom: 0; }
11058
11290
  .error { color: #9b2c33; }
11059
11291
  `;
11060
11292
  function codeBlock(code) {
@@ -11116,7 +11348,7 @@ ${claims(evidence)}`;
11116
11348
  root.adoptedStyleSheets = [sheet2];
11117
11349
  const dialog = node("dialog");
11118
11350
  const heading = node("h2", "Verify an account");
11119
- const close = node("button", "\xD7");
11351
+ const close = node("button", "", "icon");
11120
11352
  const header = node("header");
11121
11353
  const stamp = node("footer");
11122
11354
  const content = node("div", "Loading\u2026", "steps");
@@ -11124,6 +11356,7 @@ ${claims(evidence)}`;
11124
11356
  dialog.setAttribute("aria-labelledby", heading.id);
11125
11357
  close.type = "button";
11126
11358
  close.setAttribute("aria-label", "Close");
11359
+ close.append(closeMark());
11127
11360
  content.setAttribute("aria-live", "polite");
11128
11361
  stamp.append(stampLink(), node("span", version));
11129
11362
  header.append(heading, close);
@@ -11133,9 +11366,10 @@ ${claims(evidence)}`;
11133
11366
  openDialogs2.set(opener, dialog);
11134
11367
  let returning = false;
11135
11368
  if (back) {
11136
- const before = node("button", "\u2039");
11369
+ const before = node("button", "", "icon");
11137
11370
  before.type = "button";
11138
11371
  before.setAttribute("aria-label", "Back");
11372
+ before.append(backMark());
11139
11373
  header.className = "backed";
11140
11374
  header.replaceChildren(before, heading, close);
11141
11375
  before.onclick = () => {
@@ -11410,7 +11644,7 @@ ${claims(evidence)}`;
11410
11644
  const external = accountCard(
11411
11645
  logo2 ? [logo2, document.createTextNode(flow.provider.name)] : [document.createTextNode(flow.provider.name)],
11412
11646
  externalName(flow.external),
11413
- externalId(flow.external),
11647
+ externalIdGroups(flow.external) ?? externalId(flow.external),
11414
11648
  externalLink(flow.external)
11415
11649
  );
11416
11650
  const parts = [subjectCard(flow.local), linkMark(), external];
@@ -11561,15 +11795,19 @@ ${claims(evidence)}`;
11561
11795
  },
11562
11796
  latestGroup.get(element),
11563
11797
  accounts,
11564
- managers.get(element)
11798
+ managers.get(element),
11799
+ openHosts.has(element)
11565
11800
  );
11566
11801
  }
11567
11802
  }
11568
11803
  var stackedHosts = /* @__PURE__ */ new WeakSet();
11569
11804
  var quietHosts = /* @__PURE__ */ new WeakSet();
11805
+ var openHosts = /* @__PURE__ */ new WeakSet();
11570
11806
  var stackedAccounts = 4;
11571
11807
  var peekedAccounts = 8;
11572
- function arrange(element, { stacked, peek }) {
11808
+ function arrange(element, { stacked, peek, detailsOpen }) {
11809
+ if (detailsOpen) openHosts.add(element);
11810
+ else openHosts.delete(element);
11573
11811
  if (stacked) stackedHosts.add(element);
11574
11812
  else stackedHosts.delete(element);
11575
11813
  if (peek === false) quietHosts.add(element);
@@ -11577,34 +11815,14 @@ ${claims(evidence)}`;
11577
11815
  }
11578
11816
  var live = (evidence) => evidence.status === "verified" && evidence.expiresAt > Date.now();
11579
11817
  function accounts(records) {
11580
- const when = (e2) => e2.connectedAt ?? e2.approvedAt;
11581
- const byAccount = /* @__PURE__ */ new Map();
11582
- for (const record2 of records) {
11583
- const key2 = JSON.stringify([record2.provider, record2.external.id]);
11584
- byAccount.set(key2, [...byAccount.get(key2) ?? [], record2]);
11585
- }
11586
- return [...byAccount.values()].map((held) => {
11587
- const sorted = [...held].sort((a2, b2) => when(a2) - when(b2));
11588
- const verified = sorted.filter(live);
11589
- const base = verified[0] ?? sorted.reduce((a2, b2) => b2.approvedAt > a2.approvedAt ? b2 : a2);
11590
- const external = [...base.attestations.external];
11591
- for (const other of verified.slice(1))
11592
- for (const attestation of other.attestations.external)
11593
- if (!external.some((shown3) => shown3.method === attestation.method))
11594
- external.push(attestation);
11595
- return {
11596
- ...base,
11597
- connectedAt: when(sorted[0]),
11598
- attestations: { ...base.attestations, external },
11599
- links: sorted.filter((e2) => e2.status !== "revoked").map((e2) => e2.id),
11600
- // Every record the card speaks for, so its signature mark answers for all of them.
11601
- sources: verified.length ? verified : [base]
11602
- };
11603
- }).sort((a2, b2) => place2(a2) - place2(b2) || a2.connectedAt - b2.connectedAt);
11604
- }
11605
- function place2(account) {
11606
- if (account.status === "retired") return 3;
11607
- return account.mark === "preferred" ? 0 : account.mark === "unused" ? 2 : 1;
11818
+ return accountsOf(records, Date.now()).map(({ lead, held, verified, external, connectedAt }) => ({
11819
+ ...lead,
11820
+ connectedAt,
11821
+ attestations: { ...lead.attestations, external },
11822
+ links: held.filter((e2) => e2.status !== "revoked").map((e2) => e2.id),
11823
+ // Every record the card speaks for, so its signature mark answers for all of them.
11824
+ sources: verified.length ? verified : [lead]
11825
+ }));
11608
11826
  }
11609
11827
  function presentGroup(element, records, load2) {
11610
11828
  const ordered = accounts(records.map(standing));
@@ -11617,6 +11835,8 @@ ${claims(evidence)}`;
11617
11835
  const badge = renderBadge(element, lead, {
11618
11836
  more: Math.max(verified.length - 1, 0),
11619
11837
  linked: lead.visibility === "public",
11838
+ // A modified click follows the link, and must reach what a plain one shows: every account.
11839
+ ...ordered.length > 1 ? { href: subjectUrl(lead) } : {},
11620
11840
  rows: stacked ? verified.slice(1, stackedAccounts) : peek ? verified.slice(1, peekedAccounts) : [],
11621
11841
  peek
11622
11842
  });
@@ -12017,7 +12237,8 @@ ${claims(evidence)}`;
12017
12237
  "handoff-url",
12018
12238
  "connect",
12019
12239
  "stacked",
12020
- "peek"
12240
+ "peek",
12241
+ "details-open"
12021
12242
  ];
12022
12243
  handed;
12023
12244
  /**
@@ -12071,7 +12292,8 @@ ${claims(evidence)}`;
12071
12292
  const handoffUrl = this.getAttribute("handoff-url");
12072
12293
  const arrangement = {
12073
12294
  stacked: this.getAttribute("stacked") !== null,
12074
- peek: this.getAttribute("peek") !== "off"
12295
+ peek: this.getAttribute("peek") !== "off",
12296
+ detailsOpen: this.getAttribute("details-open") !== null
12075
12297
  };
12076
12298
  if (backendUrl && (handoffUrl !== null || this.getAttribute("connect") !== null)) {
12077
12299
  const client = init({ backendUrl, handoffUrl: handoffUrl ?? void 0 });