@bkazemi/verily 0.5.0 → 0.7.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
@@ -9573,13 +9573,18 @@ var Verily = (() => {
9573
9573
  // src/browser.ts
9574
9574
  var browser_exports = {};
9575
9575
  __export(browser_exports, {
9576
+ Absent: () => Absent,
9576
9577
  Refused: () => Refused,
9578
+ accountsOf: () => accountsOf,
9577
9579
  attestationLabel: () => attestationLabel,
9578
9580
  dialogPath: () => dialogPath,
9579
9581
  dialogRoute: () => dialogRoute,
9582
+ expired: () => expired,
9580
9583
  externalId: () => externalId,
9584
+ externalIdGroups: () => externalIdGroups,
9581
9585
  externalLink: () => externalLink,
9582
9586
  externalName: () => externalName,
9587
+ externalSince: () => externalSince,
9583
9588
  fresh: () => fresh,
9584
9589
  freshnessMs: () => freshnessMs,
9585
9590
  init: () => init,
@@ -9588,6 +9593,7 @@ var Verily = (() => {
9588
9593
  isRedirectProvider: () => isRedirectProvider,
9589
9594
  lastProved: () => lastProved,
9590
9595
  localSide: () => localSide,
9596
+ place: () => place,
9591
9597
  presentConnections: () => presentConnections,
9592
9598
  proofTitle: () => proofTitle,
9593
9599
  providerMethod: () => providerMethod,
@@ -9713,6 +9719,8 @@ var Verily = (() => {
9713
9719
  // src/core/index.ts
9714
9720
  var Refused = class extends Error {
9715
9721
  };
9722
+ var Absent = class extends Refused {
9723
+ };
9716
9724
  function isArtifactProvider(provider) {
9717
9725
  return "verify" in provider;
9718
9726
  }
@@ -9726,15 +9734,19 @@ var Verily = (() => {
9726
9734
  return provider.method ?? "oauth";
9727
9735
  }
9728
9736
  var freshnessMs = 7 * 864e5;
9737
+ function expired(record2, now) {
9738
+ return record2.expiresAt <= now;
9739
+ }
9729
9740
  function status(connection, now, freshness = freshnessMs) {
9730
9741
  if (connection.revokedAt !== void 0) return "revoked";
9731
9742
  if (connection.retiredAt !== void 0) return "retired";
9732
- if (now >= connection.expiresAt) return "expired";
9743
+ if (expired(connection, now)) return "expired";
9733
9744
  const main = connection.attestations?.external[0];
9734
9745
  if (main && !fresh(main, now, freshness)) return "unconfirmed";
9735
9746
  return "verified";
9736
9747
  }
9737
9748
  function fresh(attestation, now, freshness = freshnessMs) {
9749
+ if (attestation.absentAt !== void 0) return false;
9738
9750
  return !attestation.artifactUrl || attestation.hosted === true || freshness === Infinity || now < attestation.confirmedAt + freshness;
9739
9751
  }
9740
9752
  function lastProved(evidence) {
@@ -9744,9 +9756,32 @@ var Verily = (() => {
9744
9756
  function statusLabel(evidence, now) {
9745
9757
  if (evidence.status === "revoked") return "Revoked";
9746
9758
  if (evidence.status === "retired") return "Retired";
9747
- if (evidence.status === "expired" || evidence.expiresAt <= now) return "Expired";
9759
+ if (evidence.status === "expired" || expired(evidence, now)) return "Expired";
9748
9760
  return evidence.status === "unconfirmed" ? "Unconfirmed" : "Verified";
9749
9761
  }
9762
+ function place(account) {
9763
+ if (account.status === "retired") return 3;
9764
+ return account.mark === "preferred" ? 0 : account.mark === "unused" ? 2 : 1;
9765
+ }
9766
+ function accountsOf(records, now) {
9767
+ const when = (e2) => e2.connectedAt ?? e2.approvedAt;
9768
+ const byAccount = /* @__PURE__ */ new Map();
9769
+ for (const record2 of records) {
9770
+ const key2 = JSON.stringify([record2.provider, record2.external.id]);
9771
+ byAccount.set(key2, [...byAccount.get(key2) ?? [], record2]);
9772
+ }
9773
+ return [...byAccount.values()].map((unsorted) => {
9774
+ const held = [...unsorted].sort((a2, b2) => when(a2) - when(b2));
9775
+ const verified = held.filter((e2) => e2.status === "verified" && e2.expiresAt > now);
9776
+ const lead = verified[0] ?? held.reduce((a2, b2) => b2.approvedAt > a2.approvedAt ? b2 : a2);
9777
+ const external = [...lead.attestations.external];
9778
+ for (const other of verified.slice(1))
9779
+ for (const attestation of other.attestations.external)
9780
+ if (!external.some((shown3) => shown3.method === attestation.method))
9781
+ external.push(attestation);
9782
+ return { lead, held, verified, external, connectedAt: when(held[0]) };
9783
+ }).sort((a2, b2) => place(a2.lead) - place(b2.lead) || a2.connectedAt - b2.connectedAt);
9784
+ }
9750
9785
  function localSide(local, fallback) {
9751
9786
  const siteName = local.siteName ?? fallback;
9752
9787
  if (local.kind === "site") return { heading: "Website", value: siteName };
@@ -9776,6 +9811,12 @@ var Verily = (() => {
9776
9811
  function externalId(external) {
9777
9812
  return external.kind === "mailbox" || external.kind === "domain" ? void 0 : external.id;
9778
9813
  }
9814
+ function externalIdGroups(external) {
9815
+ return external.kind === "key" ? external.id.match(/.{1,4}/g) ?? void 0 : void 0;
9816
+ }
9817
+ function externalSince(external, day) {
9818
+ return typeof external.createdAt === "number" && Number.isFinite(external.createdAt) ? `Joined ${day(external.createdAt)}` : void 0;
9819
+ }
9779
9820
  function externalLink(external) {
9780
9821
  return external.kind === "mailbox" ? void 0 : external.profileUrl;
9781
9822
  }
@@ -10067,11 +10108,12 @@ var Verily = (() => {
10067
10108
  function badgeShown(element) {
10068
10109
  return intact(element) && shown.has(element);
10069
10110
  }
10070
- function renderKey(evidence, current, label, more, linked, rows, peek) {
10111
+ function renderKey(evidence, current, label, more, linked, rows, peek, href) {
10071
10112
  return JSON.stringify([
10072
10113
  more,
10073
10114
  linked,
10074
10115
  peek,
10116
+ href,
10075
10117
  rows.map((row) => [row.provider, row.providerName, externalName(row.external)]),
10076
10118
  evidence.provider,
10077
10119
  evidence.providerName,
@@ -10122,14 +10164,15 @@ var Verily = (() => {
10122
10164
  more = 0,
10123
10165
  linked = true,
10124
10166
  rows = [],
10125
- peek = false
10167
+ peek = false,
10168
+ href = evidence.evidenceUrl
10126
10169
  } = {}) {
10127
10170
  const provider = evidence.providerName;
10128
10171
  const current = evidence.status === "verified" && evidence.expiresAt > Date.now();
10129
10172
  const retired = evidence.status === "retired";
10130
10173
  const state = current ? "verified" : retired ? "retired" : evidence.status === "revoked" ? "revoked" : "expired";
10131
10174
  const label = statusLabel(evidence, Date.now());
10132
- const key2 = renderKey(evidence, current, label, more, linked, rows, peek);
10175
+ const key2 = renderKey(evidence, current, label, more, linked, rows, peek, href);
10133
10176
  if (intact(element) && shown.get(element) === key2) return null;
10134
10177
  const handle = externalName(evidence.external);
10135
10178
  const { pill: badge, mark: mark2 } = frame(element, state, !linked);
@@ -10138,7 +10181,7 @@ var Verily = (() => {
10138
10181
  const others = `${named}${unnamed ? ` and ${unnamed} more` : ""}`;
10139
10182
  paintMark(mark2, current ? "current" : retired ? "pending" : "inactive");
10140
10183
  if (linked) {
10141
- badge.href = evidence.evidenceUrl;
10184
+ badge.href = href;
10142
10185
  badge.rel = "noreferrer";
10143
10186
  } else badge.type = "button";
10144
10187
  badge.setAttribute(
@@ -10213,7 +10256,7 @@ var Verily = (() => {
10213
10256
  } catch {
10214
10257
  return;
10215
10258
  }
10216
- place(panel, pill, listed, unnamed);
10259
+ place2(panel, pill, listed, unnamed);
10217
10260
  globalThis.addEventListener?.("scroll", hide, true);
10218
10261
  };
10219
10262
  panel.setAttribute("popover", "manual");
@@ -10235,7 +10278,7 @@ var Verily = (() => {
10235
10278
  if (event.key === "Escape") hide();
10236
10279
  };
10237
10280
  }
10238
- function place(panel, pill, listed, unnamed) {
10281
+ function place2(panel, pill, listed, unnamed) {
10239
10282
  const gap = 6;
10240
10283
  const edge = 8;
10241
10284
  const at2 = pill.getBoundingClientRect();
@@ -10485,7 +10528,7 @@ ${claims(evidence)}`;
10485
10528
  }
10486
10529
 
10487
10530
  // src/version.ts
10488
- var version = "v0.5.0";
10531
+ var version = "v0.7.0";
10489
10532
 
10490
10533
  // src/browser/evidence-dialog.ts
10491
10534
  function stampLink() {
@@ -10502,8 +10545,16 @@ ${claims(evidence)}`;
10502
10545
  h2 { margin: 0; font-size: 18px; font-weight: 650; letter-spacing: -.3px; }
10503
10546
  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
10547
  button:hover { background: #f2f5f1; }
10548
+ .icon { display: grid; place-items: center; padding: 0; }
10549
+ .icon svg { width: 16px; height: 16px; }
10550
+ .title { display: flex; align-items: center; gap: 6px; min-width: 0; }
10551
+ .all { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; color: #52645a; }
10552
+ .all:hover { background: #f2f5f1; }
10553
+ .all svg { width: 16px; height: 16px; }
10505
10554
  a { color: #245f43; text-underline-offset: 3px; overflow-wrap: anywhere; }
10506
- a:focus-visible, button:focus-visible { outline: 2px solid #357ce5; outline-offset: 3px; }
10555
+ a:focus-visible, button:focus-visible, summary:focus-visible { outline: 2px solid #357ce5; outline-offset: 3px; }
10556
+ /* An element given its own display would otherwise go on showing while hidden. */
10557
+ [hidden] { display: none !important; }
10507
10558
  .summary { display: flex; align-items: center; gap: 8px; margin-top: 16px; }
10508
10559
  .mark { width: 36px; height: 36px; flex-shrink: 0; }
10509
10560
  .provider { width: 14px; height: 14px; }
@@ -10515,24 +10566,45 @@ ${claims(evidence)}`;
10515
10566
  .spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin .7s linear infinite; }
10516
10567
  @keyframes spin { to { transform: rotate(360deg); } }
10517
10568
  @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }
10518
- .caution { display: inline-block; margin-left: 4px; color: #a15c00; cursor: help; vertical-align: middle; }
10519
- .caution svg { display: block; width: 12px; height: 12px; }
10520
- .bad { color: #b3261e; font-weight: 650; text-decoration: underline dotted; text-underline-offset: 3px; cursor: help; }
10569
+ /* The "signed" pill after the verifier's name, one size smaller than the line it sits on. */
10570
+ .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; }
10571
+ .summary a.signed:hover { background: #f2f5f1; }
10572
+ .summary .signed.bad { border-color: #b3261e; color: #b3261e; font-weight: 650; cursor: help; }
10573
+ .caution { display: inline-block; margin-left: 3px; color: #a15c00; vertical-align: -1.5px; }
10574
+ .caution svg { display: block; width: 10px; height: 10px; }
10521
10575
  .account { padding: 14px 16px; border: 1px solid #e0e6df; border-radius: 10px; margin-top: 12px; }
10522
10576
  .account h3 { display: flex; align-items: center; gap: 6px; margin: 0 0 3px; color: #6b786f; font-size: 11px; font-weight: 550; }
10523
10577
  .account a, .account strong { font-weight: 650; font-size: 14px; }
10524
10578
  /* The account's own name carries that weight; a link inside a line of prose does not. */
10525
10579
  .summary a, .method a, .additional a { font: inherit; }
10526
10580
  .reference { margin-top: 2px; }
10581
+ /* A fingerprint in fours: it breaks between groups, and copies as one unbroken word. */
10582
+ .fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }
10583
+ .fingerprint span { display: inline-block; margin-right: .5ch; }
10527
10584
  .method { margin-top: 8px; }
10528
10585
  /* In the corner the heading leaves empty, apart from everything that was checked. */
10529
10586
  .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; }
10530
10587
  .listing.preferred { border-color: #245f43; color: #245f43; }
10531
10588
  /* Quiet, as its pill is: the surface and the name step back, and the controls do not. */
10532
10589
  .account.retired { background: #f6f8f5; }
10533
- .account.retired > a, .account.retired > strong { color: #6b786f; }
10590
+ .account.retired > a, .account.retired > strong, .account.retired > .named > a, .account.retired > .named > strong { color: #6b786f; }
10591
+ /* The name, then a dot and the account's creation date. The date wraps under a long name. */
10592
+ /* Clipped 4px left of the text, which hides the dot when the date wraps to a new line. */
10593
+ .named { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 14px; margin-left: -4px; padding-left: 4px; overflow-x: clip; }
10594
+ .since { position: relative; white-space: nowrap; }
10595
+ /* The dot sits in the gap before the date. */
10596
+ .since::before { content: "\xB7"; position: absolute; left: -7px; transform: translateX(-50%); }
10534
10597
  .account.retired .mark { opacity: .45; }
10535
10598
  .group { margin: 20px 0 0; color: #6b786f; font-size: 11px; font-weight: 550; }
10599
+ summary.group { display: flex; align-items: center; gap: 4px; width: fit-content; border-radius: 4px; list-style: none; cursor: pointer; }
10600
+ summary::-webkit-details-marker { display: none; }
10601
+ .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; }
10602
+ .fold svg, summary svg { width: 12px; height: 12px; }
10603
+ .fold[aria-expanded="true"] svg, details[open] > summary svg { transform: rotate(90deg); }
10604
+ @media (prefers-reduced-motion: no-preference) { .fold svg, summary svg { transition: transform .15s; } }
10605
+ /* The fold and what the holder may do share a line, until a question needs the whole of it. */
10606
+ .bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
10607
+ .account .bar .row { margin: 0 0 0 auto; }
10536
10608
  .additional { margin-top: 2px; padding-left: 12px; }
10537
10609
  .joiner { display: block; width: 20px; height: 20px; margin: 8px auto -4px; color: #90a096; }
10538
10610
  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 +10619,14 @@ ${claims(evidence)}`;
10547
10619
  .row { display: flex; justify-content: end; gap: 8px; margin-top: 16px; }
10548
10620
  .account .row { align-items: center; justify-content: start; flex-wrap: wrap; margin-top: 12px; }
10549
10621
  .account .action { padding: 5px 10px; font-size: 12px; }
10622
+ .account .action.dots { display: grid; place-items: center; align-self: stretch; width: 30px; padding: 0; }
10623
+ .dots svg { width: 16px; height: 16px; }
10624
+ /* Placed under its button each time it opens, above the dialog and clipped by none of it. */
10625
+ .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; }
10626
+ .menu:popover-open { display: grid; }
10627
+ .account .menu .action { border: 0; border-radius: 6px; padding: 6px 10px; font-weight: 550; }
10628
+ .menu .danger { color: #b3261e; }
10629
+ .menu hr { width: 100%; margin: 4px 0; border: 0; border-top: 1px solid #e5e9e3; }
10550
10630
  footer { display: flex; align-items: center; justify-content: start; gap: 6px; margin: 20px 0 -8px; color: #9aa9a0; font-size: 11px; }
10551
10631
  footer a { display: flex; color: inherit; }
10552
10632
  .logo { display: block; height: 13px; }
@@ -10564,6 +10644,58 @@ ${claims(evidence)}`;
10564
10644
  anchor.target = "_blank";
10565
10645
  return anchor;
10566
10646
  }
10647
+ function subjectUrl(e2) {
10648
+ const url = new URL(e2.evidenceUrl);
10649
+ const path = url.pathname.replace(/\/connections\/[^/]+$/, "/published");
10650
+ if (path === url.pathname) return e2.evidenceUrl;
10651
+ url.pathname = path;
10652
+ url.search = `site=${encodeURIComponent(e2.siteName)}&reference=${encodeURIComponent(e2.local.reference)}`;
10653
+ return url.href;
10654
+ }
10655
+ function awayMark() {
10656
+ const namespace2 = "http://www.w3.org/2000/svg";
10657
+ const svg = document.createElementNS(namespace2, "svg");
10658
+ svg.setAttribute("viewBox", "0 0 24 24");
10659
+ svg.setAttribute("fill", "none");
10660
+ svg.setAttribute("aria-hidden", "true");
10661
+ svg.setAttribute("focusable", "false");
10662
+ for (const d2 of [
10663
+ "M14 5h5v5",
10664
+ "M19 5l-8 8",
10665
+ "M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"
10666
+ ]) {
10667
+ const path = document.createElementNS(namespace2, "path");
10668
+ path.setAttribute("d", d2);
10669
+ path.setAttribute("stroke", "currentColor");
10670
+ path.setAttribute("stroke-width", "2");
10671
+ path.setAttribute("stroke-linecap", "round");
10672
+ path.setAttribute("stroke-linejoin", "round");
10673
+ svg.append(path);
10674
+ }
10675
+ return svg;
10676
+ }
10677
+ function strokes(box, width, paths) {
10678
+ const namespace2 = "http://www.w3.org/2000/svg";
10679
+ const svg = document.createElementNS(namespace2, "svg");
10680
+ svg.setAttribute("viewBox", `0 0 ${box} ${box}`);
10681
+ svg.setAttribute("fill", "none");
10682
+ svg.setAttribute("aria-hidden", "true");
10683
+ svg.setAttribute("focusable", "false");
10684
+ for (const d2 of paths) {
10685
+ const path = document.createElementNS(namespace2, "path");
10686
+ path.setAttribute("d", d2);
10687
+ path.setAttribute("stroke", "currentColor");
10688
+ path.setAttribute("stroke-width", String(width));
10689
+ path.setAttribute("stroke-linecap", "round");
10690
+ path.setAttribute("stroke-linejoin", "round");
10691
+ svg.append(path);
10692
+ }
10693
+ return svg;
10694
+ }
10695
+ var foldMark = () => strokes(16, 1.8, ["M6 3.5 10.5 8 6 12.5"]);
10696
+ var moreMark = () => strokes(16, 2.4, ["M3 8h.01", "M8 8h.01", "M13 8h.01"]);
10697
+ var closeMark = () => strokes(16, 1.8, ["M4 4l8 8", "M12 4l-8 8"]);
10698
+ var backMark = () => strokes(16, 1.8, ["M10 3.5 5.5 8 10 12.5"]);
10567
10699
  function linkMark() {
10568
10700
  const namespace2 = "http://www.w3.org/2000/svg";
10569
10701
  const svg = document.createElementNS(namespace2, "svg");
@@ -10603,18 +10735,91 @@ ${claims(evidence)}`;
10603
10735
  return new Date(time).toLocaleString(void 0, { dateStyle: "medium", timeStyle: "short" });
10604
10736
  }
10605
10737
  function accountCard(heading, name, reference, url, ...extra) {
10738
+ return side(heading, name, reference, url, void 0, extra);
10739
+ }
10740
+ function namedCard(heading, external, ...extra) {
10741
+ return side(
10742
+ heading,
10743
+ externalName(external),
10744
+ // A mailbox's address or a domain is its name already, so there is no second identifier.
10745
+ externalIdGroups(external) ?? externalId(external),
10746
+ externalLink(external),
10747
+ // In UTC, so the day matches the one the verifier's own pages print.
10748
+ externalSince(
10749
+ external,
10750
+ (time) => new Date(time).toLocaleDateString(void 0, { dateStyle: "medium", timeZone: "UTC" })
10751
+ ),
10752
+ extra
10753
+ );
10754
+ }
10755
+ function side(heading, name, reference, url, since, extra) {
10606
10756
  const card = node("section", "", "account");
10607
10757
  const title = node("h3");
10608
10758
  const value = node(url ? "a" : "strong", name);
10609
10759
  title.append(...heading);
10610
10760
  if (value instanceof HTMLAnchorElement) outward(value, url);
10611
- card.append(title, value);
10612
- if (reference) card.append(node("div", reference, "muted reference"));
10761
+ if (since) {
10762
+ const named = node("div", "", "named");
10763
+ named.append(value, node("span", since, "muted since"));
10764
+ card.append(title, named);
10765
+ } else card.append(title, value);
10766
+ if (typeof reference === "string") card.append(node("div", reference, "muted reference"));
10767
+ else if (reference) {
10768
+ const groups = node("div", "", "muted reference fingerprint");
10769
+ groups.append(...reference.map((group) => node("span", group)));
10770
+ card.append(groups);
10771
+ }
10613
10772
  card.append(...extra);
10614
10773
  return card;
10615
10774
  }
10616
10775
  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
10776
  var removable = (evidence) => evidence.links ?? (evidence.status === "revoked" ? [] : [evidence.id]);
10777
+ function actionsMenu(more, choices) {
10778
+ const menu = node("div", "", "menu");
10779
+ if (typeof menu.showPopover !== "function") return void 0;
10780
+ const label = "More actions";
10781
+ more.className = "action dots";
10782
+ more.title = label;
10783
+ more.setAttribute("aria-label", label);
10784
+ more.setAttribute("aria-haspopup", "menu");
10785
+ more.append(moreMark());
10786
+ more.popoverTargetElement = menu;
10787
+ menu.setAttribute("popover", "auto");
10788
+ menu.setAttribute("role", "menu");
10789
+ for (const choice of choices) choice.setAttribute("role", "menuitem");
10790
+ menu.append(...choices.slice(0, -1), node("hr"), choices.at(-1));
10791
+ const shut = () => {
10792
+ try {
10793
+ if (menu.matches(":popover-open")) menu.hidePopover();
10794
+ } catch {
10795
+ }
10796
+ };
10797
+ const gap = 4;
10798
+ const edge = 8;
10799
+ menu.addEventListener("beforetoggle", (event) => {
10800
+ if (event.newState !== "open") return;
10801
+ const at2 = more.getBoundingClientRect();
10802
+ menu.style.top = `${at2.bottom + gap}px`;
10803
+ menu.style.right = `${document.documentElement.clientWidth - at2.right}px`;
10804
+ });
10805
+ menu.addEventListener("toggle", (event) => {
10806
+ if (event.newState !== "open") return;
10807
+ const { height, bottom } = menu.getBoundingClientRect();
10808
+ if (bottom > innerHeight - edge)
10809
+ menu.style.top = `${Math.max(edge, more.getBoundingClientRect().top - gap - height)}px`;
10810
+ menu.closest("dialog")?.addEventListener("scroll", shut, { once: true });
10811
+ choices[0].focus();
10812
+ });
10813
+ menu.addEventListener("click", shut);
10814
+ menu.addEventListener("keydown", (event) => {
10815
+ const step = { ArrowDown: 1, ArrowUp: -1 }[event.key];
10816
+ if (!step) return;
10817
+ event.preventDefault();
10818
+ const at2 = choices.indexOf(event.target);
10819
+ choices[(at2 + step + choices.length) % choices.length].focus();
10820
+ });
10821
+ return menu;
10822
+ }
10618
10823
  function manageRow(evidence, manage) {
10619
10824
  const row = node("div", "", "row");
10620
10825
  const act = (label, danger = false) => {
@@ -10657,7 +10862,11 @@ ${claims(evidence)}`;
10657
10862
  retire
10658
10863
  );
10659
10864
  }
10660
- row.replaceChildren(renew, ...listing, remove, ...note ? [node("span", note, "muted")] : []);
10865
+ const said = note ? [node("span", note, "muted")] : [];
10866
+ const more = act("");
10867
+ const menu = listing.length ? actionsMenu(more, [...listing, remove]) : void 0;
10868
+ if (menu) row.replaceChildren(renew, more, menu, ...said);
10869
+ else row.replaceChildren(renew, ...listing, remove, ...said);
10661
10870
  };
10662
10871
  const retiring = () => {
10663
10872
  const cancel = act("Cancel");
@@ -10700,23 +10909,24 @@ ${claims(evidence)}`;
10700
10909
  )
10701
10910
  );
10702
10911
  }
10703
- function signedMark(evidence, later) {
10912
+ function signedMark(evidence, later, warned) {
10704
10913
  const mark2 = node("span");
10705
- const link = outward(node("a", "signed"), `${evidence.evidenceUrl}#signed`);
10914
+ const link = outward(node("a", "signed", "signed"), `${evidence.evidenceUrl}#signed`);
10706
10915
  const see = "See the signature (opens in a new tab)";
10707
10916
  const settle = (results) => {
10708
10917
  const first = results[0];
10709
10918
  if (results.some((result) => result.state === "invalid")) {
10710
- const warning = node("span", "invalid signature", "bad");
10919
+ const warning = node("span", "bad signature", "signed bad");
10711
10920
  warning.setAttribute("role", "alert");
10712
10921
  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
- mark2.replaceChildren(document.createTextNode(" \xB7 "), warning);
10922
+ mark2.replaceChildren(warning);
10923
+ warned();
10714
10924
  return;
10715
10925
  }
10716
10926
  if (first?.state === "valid" && results.every((result) => result.state === "valid")) {
10717
- link.textContent = "signed \u2713";
10927
+ link.textContent = "signed";
10718
10928
  link.title = `Signature checked in this browser against the keys ${new URL(evidence.evidenceUrl).host} publishes (key ${first.keyId}). ${see}`;
10719
- mark2.replaceChildren(document.createTextNode(" \xB7 "), link);
10929
+ mark2.replaceChildren(link);
10720
10930
  return;
10721
10931
  }
10722
10932
  const why = results.find((result) => result.state === "unchecked");
@@ -10727,8 +10937,9 @@ ${claims(evidence)}`;
10727
10937
  caution.setAttribute("aria-label", reason);
10728
10938
  caution.append(cautionMark());
10729
10939
  link.textContent = "signed";
10940
+ link.append(caution);
10730
10941
  link.title = `${reason} ${see}`;
10731
- mark2.replaceChildren(document.createTextNode(" \xB7 "), link, caution);
10942
+ mark2.replaceChildren(link);
10732
10943
  if (why) later(() => void signatures(evidence).then(settle));
10733
10944
  };
10734
10945
  void signatures(evidence).then(settle);
@@ -10752,7 +10963,25 @@ ${claims(evidence)}`;
10752
10963
  }
10753
10964
  return svg;
10754
10965
  }
10966
+ function foldToggle(folded, fold) {
10967
+ const toggle = node("button", "", "fold");
10968
+ let shown3 = fold.open;
10969
+ const set = (open) => {
10970
+ shown3 = open;
10971
+ for (const part of folded) part.hidden = !open;
10972
+ toggle.setAttribute("aria-expanded", String(open));
10973
+ };
10974
+ toggle.type = "button";
10975
+ toggle.append(foldMark(), document.createTextNode("Details"));
10976
+ set(fold.open);
10977
+ toggle.onclick = () => {
10978
+ set(!shown3);
10979
+ fold.toggle(shown3);
10980
+ };
10981
+ return toggle;
10982
+ }
10755
10983
  function externalCard(evidence, manage, later = () => {
10984
+ }, fold, warned = () => {
10756
10985
  }) {
10757
10986
  const current = evidence.status === "verified" && evidence.expiresAt > Date.now();
10758
10987
  const status2 = statusLabel(evidence, Date.now());
@@ -10765,7 +10994,7 @@ ${claims(evidence)}`;
10765
10994
  verifier.title = `View this record at ${evidence.verifierName} (opens in a new tab)`;
10766
10995
  attribution.append(verifier);
10767
10996
  } else attribution.append(document.createTextNode(evidence.verifierName));
10768
- if (evidence.signedUrl) attribution.append(signedMark(evidence, later));
10997
+ if (evidence.signedUrl) attribution.append(signedMark(evidence, later, warned));
10769
10998
  copy.append(node("div", status2, "state"), attribution);
10770
10999
  const retired = evidence.status === "retired";
10771
11000
  summary.append(verificationMark(current ? "current" : retired ? "pending" : "inactive"), copy);
@@ -10788,23 +11017,25 @@ ${claims(evidence)}`;
10788
11017
  dates.append(node("dt", label), node("dd", moment(time)));
10789
11018
  }
10790
11019
  const names2 = { site: evidence.siteName, provider };
11020
+ const row = manage && removable(evidence).length ? [manageRow(evidence, manage)] : [];
11021
+ const bar = node("div", "", "bar");
11022
+ const methods = [
11023
+ ...attestationNote(main, names2),
11024
+ ...evidence.attestations.external.slice(1).flatMap((a2) => attestationNote(a2, names2, true))
11025
+ ];
11026
+ if (fold) bar.append(foldToggle([...methods, dates], fold), ...row);
10791
11027
  const logo2 = providerMark(evidence.provider, evidence.attestations.external[0].method);
10792
- const card = accountCard(
11028
+ const card = namedCard(
10793
11029
  // The mark and the name, or just the name. A mark that falls back to writing the name
10794
11030
  // would print it twice here, since this heading writes it either way.
10795
11031
  logo2 ? [logo2, document.createTextNode(provider)] : [document.createTextNode(provider)],
10796
- externalName(evidence.external),
10797
- // A mailbox's address or a domain is its name already, so there is no second identifier.
10798
- externalId(evidence.external),
10799
- externalLink(evidence.external),
11032
+ evidence.external,
10800
11033
  // Status first, then how it was shown, then when. The proof explains the state
10801
11034
  // above it, so it cannot sit before that state has been given.
10802
11035
  summary,
10803
- ...attestationNote(main, names2),
10804
- ...evidence.attestations.external.slice(1).flatMap((a2) => attestationNote(a2, names2, true)),
11036
+ ...fold ? [] : methods,
10805
11037
  ...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)] : []
11038
+ ...fold ? [bar, ...methods, dates] : [dates, ...row]
10808
11039
  );
10809
11040
  const surface = retired ? "account retired" : "account";
10810
11041
  card.className = surface;
@@ -10832,7 +11063,7 @@ ${claims(evidence)}`;
10832
11063
  row.append(add);
10833
11064
  return row;
10834
11065
  }
10835
- function render(content, records, manage, later) {
11066
+ function render(content, records, manage, later, folds = { open: false, cards: /* @__PURE__ */ new Set() }) {
10836
11067
  const [first] = records;
10837
11068
  const local = localSide(first.local, first.siteName);
10838
11069
  const localCard = accountCard(
@@ -10849,15 +11080,40 @@ ${claims(evidence)}`;
10849
11080
  })
10850
11081
  );
10851
11082
  const retired = records.filter((record2) => record2.status === "retired");
11083
+ const listed = records.filter((record2) => record2.status !== "retired");
11084
+ const cards = (among, warned) => among.map((record2) => {
11085
+ const key2 = `${record2.provider}
11086
+ ${record2.external.id}`;
11087
+ return externalCard(
11088
+ record2,
11089
+ manage,
11090
+ later,
11091
+ among.length > 1 ? {
11092
+ open: folds.cards.has(key2) !== folds.open,
11093
+ toggle: (open) => void (open === folds.open ? folds.cards.delete(key2) : folds.cards.add(key2))
11094
+ } : void 0,
11095
+ warned
11096
+ );
11097
+ });
11098
+ const history = node("details", "", "history");
11099
+ const count = node("summary", "", "group");
11100
+ count.append(foldMark(), document.createTextNode(`Retired accounts (${retired.length})`));
11101
+ history.append(
11102
+ count,
11103
+ ...cards(retired, () => {
11104
+ history.open = true;
11105
+ })
11106
+ );
11107
+ history.open = folds.retired ?? !listed.length;
11108
+ count.addEventListener("click", () => {
11109
+ folds.retired = !history.open;
11110
+ });
10852
11111
  content.replaceChildren(
10853
11112
  localCard,
10854
11113
  linkMark(),
10855
- ...records.filter((record2) => record2.status !== "retired").map((record2) => externalCard(record2, manage, later)),
11114
+ ...cards(listed),
10856
11115
  ...manage ? [addRow(manage)] : [],
10857
- ...retired.length ? [
10858
- node("h3", "Retired accounts", "group"),
10859
- ...retired.map((record2) => externalCard(record2, manage, later))
10860
- ] : [],
11116
+ ...retired.length ? [history] : [],
10861
11117
  // Nothing below the cards may name a provider. Approval, method and dates belong to
10862
11118
  // the card they came from, and a second provider on this subject gets its own card.
10863
11119
  node(
@@ -10867,7 +11123,7 @@ ${claims(evidence)}`;
10867
11123
  )
10868
11124
  );
10869
11125
  }
10870
- function openEvidenceDialog(opener, load2, opened, arrange2 = (records) => records, manage) {
11126
+ function openEvidenceDialog(opener, load2, opened, arrange2 = (records) => records, manage, detailsOpen = false) {
10871
11127
  const existing = openDialogs.get(opener);
10872
11128
  if (existing?.open) {
10873
11129
  existing.focus();
@@ -10880,7 +11136,9 @@ ${claims(evidence)}`;
10880
11136
  root.adoptedStyleSheets = [sheet2];
10881
11137
  const dialog = node("dialog");
10882
11138
  const heading = node("h2", "Verification details");
10883
- const close = node("button", "\xD7");
11139
+ const close = node("button", "", "icon");
11140
+ const all = node("a", "", "all");
11141
+ const title = node("div", "", "title");
10884
11142
  const header = node("header");
10885
11143
  const stamp = node("footer");
10886
11144
  const content = node("div", "Checking verification\u2026");
@@ -10890,13 +11148,18 @@ ${claims(evidence)}`;
10890
11148
  close.setAttribute("aria-label", "Close verification details");
10891
11149
  content.setAttribute("aria-live", "polite");
10892
11150
  stamp.append(stampLink(), node("span", version));
10893
- header.append(heading, close);
11151
+ all.append(awayMark());
11152
+ close.append(closeMark());
11153
+ close.autofocus = true;
11154
+ title.append(heading);
11155
+ header.append(title, close);
10894
11156
  dialog.append(header, content, stamp);
10895
11157
  const removed = /* @__PURE__ */ new Map();
10896
11158
  const listed = [];
10897
11159
  const listing = (record2) => JSON.stringify([record2.status, record2.mark, record2.retiredAt, record2.approvedAt]);
10898
11160
  const listings = (given) => new Map([given].flat().map((record2) => [record2.id, listing(record2)]));
10899
11161
  let held;
11162
+ const folds = { open: detailsOpen, cards: /* @__PURE__ */ new Set() };
10900
11163
  const relisted = (evidence) => {
10901
11164
  let record2 = evidence;
10902
11165
  for (const { ids, as: as2, at: at2 } of listed) {
@@ -10965,11 +11228,19 @@ ${claims(evidence)}`;
10965
11228
  (e2) => removed.has(e2.id) && e2.status !== "revoked" ? { ...e2, status: "revoked", revokedAt: removed.get(e2.id) } : relisted(standing(e2))
10966
11229
  )
10967
11230
  );
11231
+ const listable = records.filter((e2) => e2.visibility === "public" && e2.status !== "revoked");
11232
+ title.replaceChildren(heading, ...listable.length > 1 ? [all] : []);
11233
+ if (listable.length > 1) {
11234
+ const label = `View all on ${listable[0].verifierName} (opens in a new tab)`;
11235
+ outward(all, subjectUrl(listable[0]));
11236
+ all.title = label;
11237
+ all.setAttribute("aria-label", label);
11238
+ }
10968
11239
  const key2 = JSON.stringify(records.map((e2) => [e2, statusLabel(e2, Date.now())]));
10969
11240
  if (key2 === drawn) return;
10970
11241
  drawn = key2;
10971
11242
  retries.clear();
10972
- render(content, records, actions, (retry) => retries.add(retry));
11243
+ render(content, records, actions, (retry) => retries.add(retry), folds);
10973
11244
  };
10974
11245
  const watch = (given) => {
10975
11246
  clearTimeout(expiryTimer);
@@ -11048,13 +11319,15 @@ ${claims(evidence)}`;
11048
11319
  /* The block scrolls and its frame does not, so the copy button stays in its corner. */
11049
11320
  .code { position: relative; margin: 10px 0; }
11050
11321
  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; }
11322
+ .code .action { position: absolute; top: 9px; right: 9px; padding: 3px 8px; font-size: 11px; }
11052
11323
  label { display: grid; gap: 4px; margin-top: 12px; font-weight: 600; }
11053
11324
  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
11325
  textarea { font-family: ui-monospace, monospace; font-size: 12px; resize: vertical; }
11055
11326
  fieldset { margin: 16px 0 0; padding: 10px 14px; border: 1px solid #e0e6df; border-radius: 10px; }
11056
11327
  legend { padding: 0 4px; color: #6b786f; font-size: 11px; font-weight: 550; }
11057
11328
  fieldset label { display: flex; align-items: baseline; gap: 8px; margin-top: 4px; font-weight: 500; }
11329
+ /* A radio has no baseline to speak of, so it is set in the middle of the label's first line. */
11330
+ 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
11331
  .error { color: #9b2c33; }
11059
11332
  `;
11060
11333
  function codeBlock(code) {
@@ -11116,7 +11389,7 @@ ${claims(evidence)}`;
11116
11389
  root.adoptedStyleSheets = [sheet2];
11117
11390
  const dialog = node("dialog");
11118
11391
  const heading = node("h2", "Verify an account");
11119
- const close = node("button", "\xD7");
11392
+ const close = node("button", "", "icon");
11120
11393
  const header = node("header");
11121
11394
  const stamp = node("footer");
11122
11395
  const content = node("div", "Loading\u2026", "steps");
@@ -11124,6 +11397,7 @@ ${claims(evidence)}`;
11124
11397
  dialog.setAttribute("aria-labelledby", heading.id);
11125
11398
  close.type = "button";
11126
11399
  close.setAttribute("aria-label", "Close");
11400
+ close.append(closeMark());
11127
11401
  content.setAttribute("aria-live", "polite");
11128
11402
  stamp.append(stampLink(), node("span", version));
11129
11403
  header.append(heading, close);
@@ -11133,9 +11407,10 @@ ${claims(evidence)}`;
11133
11407
  openDialogs2.set(opener, dialog);
11134
11408
  let returning = false;
11135
11409
  if (back) {
11136
- const before = node("button", "\u2039");
11410
+ const before = node("button", "", "icon");
11137
11411
  before.type = "button";
11138
11412
  before.setAttribute("aria-label", "Back");
11413
+ before.append(backMark());
11139
11414
  header.className = "backed";
11140
11415
  header.replaceChildren(before, heading, close);
11141
11416
  before.onclick = () => {
@@ -11407,11 +11682,9 @@ ${claims(evidence)}`;
11407
11682
  }
11408
11683
  function approve(flow) {
11409
11684
  const logo2 = providerMark(flow.provider.id, flow.provider.method);
11410
- const external = accountCard(
11685
+ const external = namedCard(
11411
11686
  logo2 ? [logo2, document.createTextNode(flow.provider.name)] : [document.createTextNode(flow.provider.name)],
11412
- externalName(flow.external),
11413
- externalId(flow.external),
11414
- externalLink(flow.external)
11687
+ flow.external
11415
11688
  );
11416
11689
  const parts = [subjectCard(flow.local), linkMark(), external];
11417
11690
  const choice = node("fieldset");
@@ -11561,15 +11834,19 @@ ${claims(evidence)}`;
11561
11834
  },
11562
11835
  latestGroup.get(element),
11563
11836
  accounts,
11564
- managers.get(element)
11837
+ managers.get(element),
11838
+ openHosts.has(element)
11565
11839
  );
11566
11840
  }
11567
11841
  }
11568
11842
  var stackedHosts = /* @__PURE__ */ new WeakSet();
11569
11843
  var quietHosts = /* @__PURE__ */ new WeakSet();
11844
+ var openHosts = /* @__PURE__ */ new WeakSet();
11570
11845
  var stackedAccounts = 4;
11571
11846
  var peekedAccounts = 8;
11572
- function arrange(element, { stacked, peek }) {
11847
+ function arrange(element, { stacked, peek, detailsOpen }) {
11848
+ if (detailsOpen) openHosts.add(element);
11849
+ else openHosts.delete(element);
11573
11850
  if (stacked) stackedHosts.add(element);
11574
11851
  else stackedHosts.delete(element);
11575
11852
  if (peek === false) quietHosts.add(element);
@@ -11577,34 +11854,14 @@ ${claims(evidence)}`;
11577
11854
  }
11578
11855
  var live = (evidence) => evidence.status === "verified" && evidence.expiresAt > Date.now();
11579
11856
  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;
11857
+ return accountsOf(records, Date.now()).map(({ lead, held, verified, external, connectedAt }) => ({
11858
+ ...lead,
11859
+ connectedAt,
11860
+ attestations: { ...lead.attestations, external },
11861
+ links: held.filter((e2) => e2.status !== "revoked").map((e2) => e2.id),
11862
+ // Every record the card speaks for, so its signature mark answers for all of them.
11863
+ sources: verified.length ? verified : [lead]
11864
+ }));
11608
11865
  }
11609
11866
  function presentGroup(element, records, load2) {
11610
11867
  const ordered = accounts(records.map(standing));
@@ -11617,6 +11874,8 @@ ${claims(evidence)}`;
11617
11874
  const badge = renderBadge(element, lead, {
11618
11875
  more: Math.max(verified.length - 1, 0),
11619
11876
  linked: lead.visibility === "public",
11877
+ // A modified click follows the link, and must reach what a plain one shows: every account.
11878
+ ...ordered.length > 1 ? { href: subjectUrl(lead) } : {},
11620
11879
  rows: stacked ? verified.slice(1, stackedAccounts) : peek ? verified.slice(1, peekedAccounts) : [],
11621
11880
  peek
11622
11881
  });
@@ -11972,7 +12231,7 @@ ${claims(evidence)}`;
11972
12231
  (k2) => typeof value.local[k2] === "string"
11973
12232
  ) && ["id", "handle", "profileUrl"].every(
11974
12233
  (k2) => typeof value.external[k2] === "string"
11975
- ) && (value.local.profileUrl === void 0 || typeof value.local.profileUrl === "string") && // A signed record lives beside the record it was made from and nowhere else, since the
12234
+ ) && (value.local.profileUrl === void 0 || typeof value.local.profileUrl === "string") && (value.external.createdAt === void 0 || typeof value.external.createdAt === "number" && Number.isFinite(value.external.createdAt)) && // A signed record lives beside the record it was made from and nowhere else, since the
11976
12235
  // keys it is checked against are read from where the record lives.
11977
12236
  (value.signedUrl === void 0 || value.signedUrl === `${value.evidenceUrl}?format=signed`) && validAttestations(value.attestations) && (value.local.kind === void 0 || typeof value.local.kind === "string") && (value.external.kind === void 0 || ["account", "key", "page", "mailbox", "domain"].includes(String(value.external.kind))) && (value.revokedAt === void 0 || typeof value.revokedAt === "number" && Number.isFinite(value.revokedAt)) && ["verified", "unconfirmed", "expired", "retired", "revoked"].includes(String(value.status)) && // A retired record stands on its dates, so one that does not say when is not one.
11978
12237
  (value.status === "retired" ? typeof value.retiredAt === "number" && Number.isFinite(value.retiredAt) : value.retiredAt === void 0) && (value.mark === void 0 || ["preferred", "unused"].includes(String(value.mark))) && ["public", "unlisted"].includes(String(value.visibility)) && (value.connectedAt === void 0 || typeof value.connectedAt === "number" && Number.isFinite(value.connectedAt)) && ["authenticatedAt", "approvedAt", "expiresAt"].every(
@@ -12017,7 +12276,8 @@ ${claims(evidence)}`;
12017
12276
  "handoff-url",
12018
12277
  "connect",
12019
12278
  "stacked",
12020
- "peek"
12279
+ "peek",
12280
+ "details-open"
12021
12281
  ];
12022
12282
  handed;
12023
12283
  /**
@@ -12071,7 +12331,8 @@ ${claims(evidence)}`;
12071
12331
  const handoffUrl = this.getAttribute("handoff-url");
12072
12332
  const arrangement = {
12073
12333
  stacked: this.getAttribute("stacked") !== null,
12074
- peek: this.getAttribute("peek") !== "off"
12334
+ peek: this.getAttribute("peek") !== "off",
12335
+ detailsOpen: this.getAttribute("details-open") !== null
12075
12336
  };
12076
12337
  if (backendUrl && (handoffUrl !== null || this.getAttribute("connect") !== null)) {
12077
12338
  const client = init({ backendUrl, handoffUrl: handoffUrl ?? void 0 });