@bkazemi/verily 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -168,7 +168,7 @@ import { init } from '@bkazemi/verily';
168
168
  const client = init({ backendUrl: '/api/verily' });
169
169
  ```
170
170
 
171
- Without one, serve `node_modules/@bkazemi/verily/dist/verily.js` yourself, or load it from a CDN such as `https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.8.0/dist/verily.js`. The script defines a global `Verily`.
171
+ Without one, serve `node_modules/@bkazemi/verily/dist/verily.js` yourself, or load it from a CDN such as `https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.8.1/dist/verily.js`. The script defines a global `Verily`.
172
172
 
173
173
  Then place the badge wherever the account appears:
174
174
 
@@ -370,7 +370,7 @@ A static site only needs the script and the badge markup; the backend runs somew
370
370
 
371
371
  ```html
372
372
  <script
373
- src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.8.0/dist/verily.js"
373
+ src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.8.1/dist/verily.js"
374
374
  defer
375
375
  ></script>
376
376
  <verily-badge
@@ -201,6 +201,19 @@ function heldNames(providers) {
201
201
  const names = [...new Set(providers.map(held))];
202
202
  return names.length > 1 ? `${names.slice(0, -1).join(', ')} or ${names.at(-1)}` : names[0];
203
203
  }
204
+ /** What proving control is for on each kind of /verify page. */
205
+ function purpose(kind, held, noun, several) {
206
+ const ways = {
207
+ connect: several
208
+ ? `Choose an account to link to this ${noun}. You show you control it, then review the link before it's made.`
209
+ : `Show you control your ${held} to link it to this ${noun}. You review the link before it's made.`,
210
+ renew: `A connection lapses unless it's renewed. Show you still control your ${held} to extend it. It keeps its ID, so anywhere that shows it keeps working.`,
211
+ visibility: `Only someone who controls both accounts can change this. Show you still control your ${held}, then choose.`,
212
+ revoke: `Show you control the ${held} on this connection, and you can then remove it.`,
213
+ 'share-revoke': `Show you control the ${held} on this connection, and you can then revoke its sharing link. The connection itself stays.`,
214
+ };
215
+ return ways[kind];
216
+ }
204
217
  /** Provider setup with the flow’s return instructions, shared by HTML and the dialog. */
205
218
  function proofInstructions(provider, flow) {
206
219
  return [
@@ -272,6 +285,7 @@ function evidencePage(e, base, report, keyId, named) {
272
285
  // site's own reference would be a third line saying the same thing. The provider's
273
286
  // identifier stays: that one is the provider's word, not the site's own wording.
274
287
  card(local.heading, local.value, e.local.profileUrl, undefined, attestationNote(e.attestations.local, names)) +
288
+ joiner +
275
289
  externalCard(marked(names.provider, e.provider, e.attestations.external[0].method), e.external, externalNotes(e.attestations, names)) +
276
290
  times(e.status === 'retired'
277
291
  ? [
@@ -302,9 +316,9 @@ function evidencePage(e, base, report, keyId, named) {
302
316
  `${e.linkExpiresAt ? '<p class="fine">Anyone with this link can view and forward it.</p>' : ''}
303
317
  ${e.signedUrl ? `<p class="signed" id="signed"><strong>Signed by ${escape(e.verifierName)}</strong>${keyId ? `<br>${keyLine(base, keyId, named)}` : ''}<br><a href="${escape(safeUrl(e.signedUrl))}">Download signed record</a> · <a href="${escape(base)}/check">Check a record</a></p>` : ''}
304
318
  <p class="fine">This connection does not establish legal identity, trustworthiness, content authorship, or permanent ownership.</p>
305
- <p class="fine"><a href="${escape(base)}/external-revoke/${escape(e.id)}">Remove this connection using your external account</a></p>
306
- ${e.visibility === 'unlisted' ? `<p class="fine"><a href="${escape(base)}/external-share-revoke/${escape(e.id)}">Revoke only this sharing link using your external account</a></p>` : ''}
307
- <p class="fine"><a href="${escape(report)}" rel="noreferrer">Report an incorrect record</a></p>`, '', '', verdict(toneOf(e, Date.now())));
319
+ <footer><p class="fine">Is this your account? <a href="${escape(base)}/external-revoke/${escape(e.id)}?provider=${encodeURIComponent(e.provider)}">Remove this connection</a></p>
320
+ ${e.visibility === 'unlisted' ? `<p class="fine"><a href="${escape(base)}/external-share-revoke/${escape(e.id)}?provider=${encodeURIComponent(e.provider)}">Revoke only this sharing link using your external account</a></p>` : ''}
321
+ <p class="fine"><a href="${escape(report)}" rel="noreferrer">Report an incorrect record</a></p></footer>`, '', '', verdict(toneOf(e, Date.now()), true));
308
322
  }
309
323
  /** Joins a subject's card to its accounts: the link itself, drawn as the dialog draws it. */
310
324
  const joiner = '<svg class="joiner" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">' +
@@ -392,6 +406,7 @@ function checkedPage(d, keyId, base, named) {
392
406
  const retired = d.version === 2;
393
407
  return page(base, retired ? 'Retired connection' : 'Signed record', `<p>${escape(d.verifierName)} signed this record. It shows what stood when it was signed, not what stands now.</p>` +
394
408
  card(local.heading, local.value, d.local.profileUrl, undefined, attestationNote(d.attestations.local, names)) +
409
+ joiner +
395
410
  externalCard(marked(names.provider, d.provider, d.attestations.external[0].method), d.external, externalNotes(d.attestations, names)) +
396
411
  times(retired
397
412
  ? [
@@ -687,22 +702,33 @@ export function createVerily(options) {
687
702
  // A new link started from this page always ends on its approval, as it did when
688
703
  // this form was a GET, so it says so: `/verify` is an address any site can send a
689
704
  // browser to, and the approval is where the holder sees what they are linking.
705
+ // the provider's mark where it has one, then what the button does
706
+ const label = (text, p) => {
707
+ const heading = marked(text, p.id, providerMethod(p));
708
+ return typeof heading === 'string'
709
+ ? escape(heading)
710
+ : `${heading.mark}<span>${escape(heading.text)}</span>`;
711
+ };
690
712
  const forms = offered
691
- .map((p) => `<form method="post" action="${escape(prefix)}/sessions"><input type="hidden" name="kind" value="${kind}">${kind === 'connect' ? '<input type="hidden" name="unattended" value="1">' : ''}<input type="hidden" name="connectionId" value="${escape(id)}"><input type="hidden" name="provider" value="${escape(p.id)}"><input type="hidden" name="method" value="${escape(providerMethod(p))}"><button>${escape(offered.length > 1 ? methodAction(p) : `Continue with ${p.name}`)}</button></form>`)
713
+ .map((p) => `<form method="post" action="${escape(prefix)}/sessions"><input type="hidden" name="kind" value="${kind}">${kind === 'connect' ? '<input type="hidden" name="unattended" value="1">' : ''}<input type="hidden" name="connectionId" value="${escape(id)}"><input type="hidden" name="provider" value="${escape(p.id)}"><input type="hidden" name="method" value="${escape(providerMethod(p))}"><button>${label(offered.length > 1 ? methodAction(p) : `Continue with ${p.name}`, p)}</button></form>`)
692
714
  .join('');
715
+ // several methods stack as one list of equal buttons, as in the dialog
716
+ const choices = offered.length > 1 ? `<div class="choices">${forms}</div>` : forms;
693
717
  return html(page(prefix, external
694
- ? 'Remove a connection'
718
+ ? kind === 'share-revoke'
719
+ ? 'Revoke a sharing link'
720
+ : 'Remove a connection'
695
721
  : kind === 'renew'
696
722
  ? 'Renew this connection'
697
- : several
698
- ? 'Verify an account'
699
- : `Verify with ${providerNames(offered)}`, `
700
- ${user
701
- ? card(subject(user).heading, subject(user).value, user.profileUrl)
702
- : '<p>Authenticate with the matching external account to review and remove this link.</p>'}
703
- <p>Confirm the connection between this ${escape(subjectNoun(user?.kind) ?? 'site')} and your ${escape(heldNames(offered))}.</p>
723
+ : kind === 'visibility'
724
+ ? 'Change who can see this connection'
725
+ : several
726
+ ? 'Verify an account'
727
+ : `Verify with ${providerNames(offered)}`, `
728
+ ${user ? card(subject(user).heading, subject(user).value, user.profileUrl) : ''}
729
+ <p>${escape(purpose(kind, heldNames(offered), subjectNoun(user?.kind) ?? 'site', several))}</p>
704
730
  ${kind === 'renew' && offered.length > 1 ? '<p class="fine">Renewing another way adds that method beneath the one this connection was first shown by.</p>' : ''}
705
- ${forms}`));
731
+ ${choices}`));
706
732
  }
707
733
  if (path === '/callback') {
708
734
  const state = url.searchParams.get('state');
@@ -785,6 +811,7 @@ export function createVerily(options) {
785
811
  ? 'Add to connection'
786
812
  : 'Confirm connection', `
787
813
  ${card(subject(flow.local).heading, subject(flow.local).value, flow.local.profileUrl)}
814
+ ${joiner}
788
815
  ${externalCard(marked(provider.name, provider.id, providerMethod(provider)), flow.external)}
789
816
  ${joined ? `<p>This account is already linked here. Confirming adds this method to that connection, beneath the one it was first shown by, and it stays ${escape(joined.visibility)}.</p>` : ''}
790
817
  ${found ? `<p>${escape(found)}</p>` : ''}
@@ -8,7 +8,7 @@
8
8
  * leaves. So the styling does the two things that helps with, a measured column and a
9
9
  * legible hierarchy, and nothing that would make a record look like a product.
10
10
  */
11
- export declare const stylesheet = ":root {\n --ink: #23312b;\n --muted: #6b786f;\n --line: #dce2de;\n --surface: #fff;\n --raised: #f7f9f7;\n --accent: #245f43;\n --warn: #a4442b;\n color-scheme: light dark;\n accent-color: var(--accent);\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --ink: #dde5e0;\n --muted: #96a39b;\n --line: #2f3b35;\n --surface: #161a18;\n --raised: #1d2220;\n --accent: #7fc4a2;\n --warn: #e59a82;\n }\n}\n\n* { box-sizing: border-box; }\n\nbody {\n margin: 0;\n padding: 0 1rem 4rem;\n background: var(--surface);\n color: var(--ink);\n font: 16px/1.65 system-ui, -apple-system, 'Segoe UI', sans-serif;\n text-rendering: optimizeLegibility;\n}\n\nmain {\n max-width: 34rem;\n margin: 0 auto;\n padding-top: 3rem;\n}\n\n/*\n * The logotype, above the page's own heading, on its own dark plate. Sized so the word is\n * the height it was without the plate, whose margin makes the box that much taller. The\n * word is white in either scheme: the plate is what it is read against, not the page.\n */\n.logo {\n display: block;\n height: 2.5rem;\n margin-bottom: 1.5rem;\n}\n\nh1 {\n margin: 0 0 1.75rem;\n padding-bottom: 1rem;\n border-bottom: 1px solid var(--line);\n font-size: 1.5rem;\n font-weight: 620;\n letter-spacing: -.02em;\n line-height: 1.25;\n}\n\n/*\n * A page holding one thing needs no rule under its heading. The rule divides a heading\n * from the record beneath it, and a page that only asks for a key has no record to divide.\n */\nmain.single h1 { margin-bottom: 1.25rem; padding-bottom: 0; border-bottom: none; }\n\n/*\n * A page holding one thing sits in the middle of the window rather than at its top,\n * where it would leave most of the window empty below it.\n */\nbody:has(main.single) { display: flex; min-height: 100vh; min-height: 100dvh; }\nmain.single { width: 100%; max-width: 24rem; margin: auto; padding-bottom: 3rem; }\n\nh2 { margin: 2rem 0 .75rem; font-size: 1.125rem; font-weight: 620; letter-spacing: -.01em; }\n\nh3 { margin: 0 0 .5rem; font-size: 1rem; font-weight: 620; }\n\np { margin: 0 0 1rem; overflow-wrap: break-word; }\n\na { color: var(--accent); text-underline-offset: 3px; overflow-wrap: anywhere; }\n\na:hover { text-decoration-thickness: 2px; }\n\n/*\n * Something to be run or published exactly as written. Its own line breaks are kept and a\n * long line is soft-wrapped rather than cut off: a soft wrap is drawn, not inserted, so\n * what is copied is still character for character what was written. Selecting inside the\n * block takes all of it, since half a command is worse than none.\n */\npre {\n position: relative;\n margin: 0 0 1rem;\n /* Room along the top line for the copy button, which sits over the block rather than in it. */\n padding: .75rem 5.25rem .75rem .875rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--raised);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n user-select: all;\n}\n\ncode { font: .8125rem/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }\n\n/* Made by a script, so it exists only where it works. Quiet: the block is the point. */\n.copy {\n position: absolute;\n top: .5rem;\n right: .5rem;\n padding: .1875rem .5rem;\n border: 1px solid var(--line);\n border-radius: 5px;\n background: var(--surface);\n color: var(--muted);\n font: 500 .75rem/1.5 system-ui, sans-serif;\n user-select: none;\n}\n\n.copy:hover { color: var(--ink); border-color: var(--muted); }\n\n/*\n * The two sides of the link, which is what a reader came to read. Each fact about a side\n * gets its own line: what it is, what it is called, the identifier behind that, and how it\n * was shown. Everything else on the page qualifies the pair, so it is set quieter.\n */\n.side {\n margin-bottom: .75rem;\n padding: .875rem 1rem;\n border: 1px solid var(--line);\n border-radius: 8px;\n background: var(--raised);\n}\n\n/*\n * A subject's accounts are peers, so where there is room they sit two across. The page\n * that lists them is wider for it, and a narrow one keeps them in a single column.\n */\nmain:has(.accounts) { max-width: 54rem; }\n\n.accounts {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));\n gap: .75rem;\n margin-bottom: .75rem;\n}\n\n.accounts .side { position: relative; display: flex; flex-direction: column; margin-bottom: 0; }\n\n/* How the holder lists the account, in the card's corner and out of the lines below. */\n.listing {\n position: absolute;\n top: .75rem;\n right: .875rem;\n padding: .0625rem .5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n color: var(--muted);\n font-size: .6875rem;\n font-weight: 550;\n}\n\n.listing.preferred { border-color: var(--accent); color: var(--accent); }\n\n/* The times sit at the foot of the card, so they line up along a row of uneven cards. */\n.accounts .side dl { margin-top: auto; }\n.accounts .side p:last-of-type { margin-bottom: .625rem; }\n\n/* Between a subject and its accounts on the page that lists them. */\n.joiner { display: block; width: 1.25rem; height: 1.25rem; margin: 0 auto .75rem; color: var(--muted); }\n\n.who {\n margin: 0 0 .125rem;\n color: var(--muted);\n font-size: .75rem;\n font-weight: 550;\n letter-spacing: .02em;\n}\n\n/* A provider's mark before its name, at the height of the letters. */\n.who .provider { display: inline-block; width: .875rem; height: .875rem; margin-right: .375rem; vertical-align: -.125rem; }\n\n/* Clipped 4px left of the text, which hides the date's dot when the date wraps to a new line. */\n.name { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 1rem; margin: 0 0 0 -4px; padding-left: 4px; overflow-x: clip; font-size: 1.0625rem; font-weight: 620; letter-spacing: -.01em; }\n\n/* The account's creation date, after the name and a dot. It wraps under a long name. */\n.name .since { position: relative; color: var(--muted); font-size: .8125rem; font-weight: 400; letter-spacing: 0; white-space: nowrap; }\n\n/* The dot sits in the gap before the date. */\n.name .since::before { content: \"\u00B7\"; position: absolute; left: -.5rem; transform: translateX(-50%); }\n\n/* The name is the card's subject before it is a link, so the rule is drawn on hover. */\n.name a { text-decoration: none; }\n\n.name a:hover { text-decoration: underline; }\n\n.reference, .how { margin: 0; color: var(--muted); font-size: .8125rem; line-height: 1.5; }\n\n.name + .how, .reference + .how { margin-top: .5rem; }\n.fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }\n.fingerprint span { display: inline-block; margin-right: .5ch; }\n.how.additional { padding-left: .75rem; }\n/* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n.how .signed-mark { display: inline-block; margin-left: .3125rem; padding: 0 .375rem; border: 1px solid var(--line); border-radius: 999px; font-size: .625rem; font-weight: 550; line-height: 1.3; text-decoration: none; vertical-align: .0625rem; }\n\n.fine { color: var(--muted); font-size: .8125rem; line-height: 1.55; margin-bottom: .625rem; }\n\n/* That the record is signed is a fact about it, so it is set as one and not as fine print. */\n.signed { font-size: .875rem; line-height: 1.55; margin-bottom: .875rem; }\n/* Arrived at from a badge's \"signed\" link, so the reader is shown where they landed. */\n.signed:target { margin-inline: -.75rem; padding: .625rem .75rem; border-radius: 8px; background: var(--raised); }\n\n/* Every time on the record in one column, so they can be compared rather than hunted for. */\ndl {\n display: grid;\n grid-template-columns: auto 1fr;\n gap: .3rem 1.5rem;\n margin: 1.25rem 0;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n font-size: .8125rem;\n}\n\ndt { color: var(--muted); }\n\ndd { margin: 0; font-variant-numeric: tabular-nums; }\n\n/* Inside a card the rule above it is the card's own edge, so the list needs none. */\nsection dl { margin: .5rem 0 1rem; padding-top: 0; border-top: 0; }\n.side dl { margin: .625rem 0 0; padding-top: .625rem; }\n\nsection {\n margin: 0 0 1.25rem;\n padding: 1.25rem 1.375rem;\n border: 1px solid var(--line);\n border-radius: 10px;\n}\n\nsection > :last-child { margin-bottom: 0; }\n\nform { margin: 0 0 1.5rem; }\n\nlabel { display: block; margin: 0 0 .75rem; color: var(--muted); font-size: .8125rem; }\n\ninput, textarea {\n display: block;\n width: 100%;\n margin-top: .375rem;\n padding: .5rem .625rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--surface);\n color: var(--ink);\n font: .875rem/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;\n}\n\n/* Height comes from the rows attribute, which differs per box. */\ntextarea { resize: vertical; }\n\ninput:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }\n\nbutton {\n padding: .5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font: 500 .9375rem/1.4 system-ui, -apple-system, 'Segoe UI', sans-serif;\n cursor: pointer;\n}\n\n/* A link that starts something, drawn as a button. */\na.button {\n display: inline-block;\n padding: .5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font-weight: 500;\n font-size: .9375rem;\n line-height: 1.4;\n text-decoration: none;\n}\n\nbutton + button { margin-left: .5rem; border-color: var(--line); background: none; color: var(--ink); }\n\n/* The other choice beside a page's one action, such as Cancel. */\na.button.secondary { border-color: var(--line); background: none; color: var(--ink); }\n\n/* A page's action and its alternative, side by side. */\n.choice { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1.5rem; }\n.choice form { margin: 0; }\n\n/* A control that ends something. Filled where it is the page's one action. */\nbutton.danger { border-color: var(--warn); background: var(--warn); }\n\n/*\n * A connection on the settings page: the account's card, then what can be done with it.\n * Its state sits in the card's corner, on the line that names the method.\n */\n.connection { position: relative; margin-bottom: 1rem; }\n\n.state {\n position: absolute;\n top: .75rem;\n right: .875rem;\n display: flex;\n align-items: center;\n gap: .375rem;\n height: 1.625rem;\n padding: 0 .75rem 0 .5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--surface);\n color: var(--ink);\n font-size: .8125rem;\n font-weight: 550;\n}\n\n/*\n * The V the badge draws, in the tone that says how the record stands. A record that claims\n * nothing is drawn faint in the text colour, as the badge draws it.\n */\n.verdict { flex-shrink: 0; width: 1rem; height: 1rem; }\n.verdict.pending { opacity: .45; }\n.how .verdict { width: .875rem; height: .875rem; margin-right: .3125rem; vertical-align: -.125rem; }\nh1 .verdict { width: 1.25rem; height: 1.25rem; margin-right: .5rem; vertical-align: -.0625rem; }\n\n/*\n * In a state pill the V is cropped to its strokes and fills the pill's height, so its arms\n * and its foot meet the border. The pill's left padding keeps the arms clear of its curve.\n */\n.state .verdict { width: auto; height: 100%; }\n\n/*\n * A pill's line includes room for descenders below the letters, which leaves its text\n * sitting high. Trimmed to the letters, the padding alone sets the space above and below.\n * In a state pill the word alone is trimmed, so it centres on the V by its letters.\n */\n@supports (text-box: trim-both cap alphabetic) {\n .listing { padding: .3125rem .5rem; text-box: trim-both cap alphabetic; }\n .state > span { text-box: trim-both cap alphabetic; }\n}\n\n/* Links that sit together on one line, wrapping where there is no room. */\n.links { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1.25rem; font-size: .875rem; }\n\n/* Under the account's id, with the facts about it rather than the controls. */\n.evidence { margin: .375rem 0 0; font-size: .8125rem; }\n\n.connection dl { margin-bottom: .75rem; }\n.connection .side > :last-child { margin-bottom: 0; }\n.connection pre { margin: .75rem 0; background: var(--surface); }\n.connection .actions { margin-top: .875rem; }\n\n/* The buttons for one connection in a row, wrapping where there is no room. */\n.actions { display: flex; flex-wrap: wrap; gap: .5rem; }\n.actions form { display: contents; }\n\n.actions button, .actions a.button {\n margin: 0;\n padding: .3125rem .75rem;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n font-size: .8125rem;\n}\n\n.actions button:hover, .actions a.button:hover { border-color: var(--muted); }\n\n/* The controls that cannot be undone, together at the foot of the card. */\n.ending { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem; margin-top: .75rem; padding-top: .75rem; border-top: 1px solid var(--line); }\n.ending form { margin: 0; }\n\n.ending button.danger {\n padding: .3125rem .75rem;\n background: var(--surface);\n color: var(--warn);\n font-size: .8125rem;\n}\n\nfieldset { margin: 0 0 1.25rem; padding: 1rem 1.125rem .25rem; border: 1px solid var(--line); border-radius: 8px; }\n\nlegend { padding: 0 .375rem; color: var(--muted); font-size: .8125rem; }\n\nfieldset label { display: flex; align-items: baseline; gap: .5rem; color: var(--ink); font-size: .9375rem; }\n\nfieldset input { width: auto; margin: 0; }\n\nfieldset p { margin: .25rem 0 .875rem 1.5rem; color: var(--muted); font-size: .8125rem; }\n";
11
+ export declare const stylesheet = ":root {\n --ink: #23312b;\n --muted: #6b786f;\n --line: #dce2de;\n --surface: #fff;\n --raised: #f7f9f7;\n --accent: #245f43;\n --warn: #a4442b;\n color-scheme: light dark;\n accent-color: var(--accent);\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --ink: #dde5e0;\n --muted: #96a39b;\n --line: #2f3b35;\n --surface: #161a18;\n --raised: #1d2220;\n --accent: #7fc4a2;\n --warn: #e59a82;\n }\n}\n\n* { box-sizing: border-box; }\n\nbody {\n margin: 0;\n padding: 0 1rem 4rem;\n background: var(--surface);\n color: var(--ink);\n font: 16px/1.65 system-ui, -apple-system, 'Segoe UI', sans-serif;\n text-rendering: optimizeLegibility;\n}\n\nmain {\n max-width: 34rem;\n margin: 0 auto;\n padding-top: 3rem;\n}\n\n/*\n * The logotype, above the page's own heading, on its own dark plate. Sized so the word is\n * the height it was without the plate, whose margin makes the box that much taller. The\n * word is white in either scheme: the plate is what it is read against, not the page.\n */\n.logo {\n display: block;\n height: 2.5rem;\n margin-bottom: 1.5rem;\n}\n\nh1 {\n margin: 0 0 1.75rem;\n padding-bottom: 1rem;\n border-bottom: 1px solid var(--line);\n font-size: 1.5rem;\n font-weight: 620;\n letter-spacing: -.02em;\n line-height: 1.25;\n}\n\n/*\n * A page holding one thing needs no rule under its heading. The rule divides a heading\n * from the record beneath it, and a page that only asks for a key has no record to divide.\n */\nmain.single h1 { margin-bottom: 1.25rem; padding-bottom: 0; border-bottom: none; }\n\n/*\n * A page holding one thing sits in the middle of the window rather than at its top,\n * where it would leave most of the window empty below it.\n */\nbody:has(main.single) { display: flex; min-height: 100vh; min-height: 100dvh; }\nmain.single { width: 100%; max-width: 24rem; margin: auto; padding-bottom: 3rem; }\n\nh2 { margin: 2rem 0 .75rem; font-size: 1.125rem; font-weight: 620; letter-spacing: -.01em; }\n\nh3 { margin: 0 0 .5rem; font-size: 1rem; font-weight: 620; }\n\np { margin: 0 0 1rem; overflow-wrap: break-word; }\n\na { color: var(--accent); text-underline-offset: 3px; overflow-wrap: anywhere; }\n\na:hover { text-decoration-thickness: 2px; }\n\n/*\n * Something to be run or published exactly as written. Its own line breaks are kept and a\n * long line is soft-wrapped rather than cut off: a soft wrap is drawn, not inserted, so\n * what is copied is still character for character what was written. Selecting inside the\n * block takes all of it, since half a command is worse than none.\n */\npre {\n position: relative;\n margin: 0 0 1rem;\n /* Room along the top line for the copy button, which sits over the block rather than in it. */\n padding: .75rem 5.25rem .75rem .875rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--raised);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n user-select: all;\n}\n\ncode { font: .8125rem/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }\n\n/* Made by a script, so it exists only where it works. Quiet: the block is the point. */\n.copy {\n position: absolute;\n top: .5rem;\n right: .5rem;\n padding: .1875rem .5rem;\n border: 1px solid var(--line);\n border-radius: 5px;\n background: var(--surface);\n color: var(--muted);\n font: 500 .75rem/1.5 system-ui, sans-serif;\n user-select: none;\n}\n\n.copy:hover { color: var(--ink); border-color: var(--muted); }\n\n/*\n * The two sides of the link, which is what a reader came to read. Each fact about a side\n * gets its own line: what it is, what it is called, the identifier behind that, and how it\n * was shown. Everything else on the page qualifies the pair, so it is set quieter.\n */\n.side {\n margin-bottom: .75rem;\n padding: .875rem 1rem;\n border: 1px solid var(--line);\n border-radius: 8px;\n background: var(--raised);\n}\n\n/*\n * A subject's accounts are peers, so where there is room they sit two across. The page\n * that lists them is wider for it, and a narrow one keeps them in a single column.\n */\nmain:has(.accounts) { max-width: 54rem; }\n\n.accounts {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));\n gap: .75rem;\n margin-bottom: .75rem;\n}\n\n.accounts .side { position: relative; display: flex; flex-direction: column; margin-bottom: 0; }\n\n/* How the holder lists the account, in the card's corner and out of the lines below. */\n.listing {\n position: absolute;\n top: .75rem;\n right: .875rem;\n padding: .0625rem .5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n color: var(--muted);\n font-size: .6875rem;\n font-weight: 550;\n}\n\n.listing.preferred { border-color: var(--accent); color: var(--accent); }\n\n/* The times sit at the foot of the card, so they line up along a row of uneven cards. */\n.accounts .side dl { margin-top: auto; }\n.accounts .side p:last-of-type { margin-bottom: .625rem; }\n\n/* Between a subject and its accounts on the page that lists them. */\n.joiner { display: block; width: 1.25rem; height: 1.25rem; margin: 0 auto .75rem; color: var(--muted); }\n\n.who {\n margin: 0 0 .125rem;\n color: var(--muted);\n font-size: .75rem;\n font-weight: 550;\n letter-spacing: .02em;\n}\n\n/* A provider's mark before its name, at the height of the letters. */\n.who .provider { display: inline-block; width: .875rem; height: .875rem; margin-right: .375rem; vertical-align: -.125rem; }\n\n/* Clipped 4px left of the text, which hides the date's dot when the date wraps to a new line. */\n.name { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 1rem; margin: 0 0 0 -4px; padding-left: 4px; overflow-x: clip; font-size: 1.0625rem; font-weight: 620; letter-spacing: -.01em; }\n\n/* The account's creation date, after the name and a dot. It wraps under a long name. */\n.name .since { position: relative; color: var(--muted); font-size: .8125rem; font-weight: 400; letter-spacing: 0; white-space: nowrap; }\n\n/* The dot sits in the gap before the date. */\n.name .since::before { content: \"\u00B7\"; position: absolute; left: -.5rem; transform: translateX(-50%); }\n\n/* The name is the card's subject before it is a link, so the rule is drawn on hover. */\n.name a { text-decoration: none; }\n\n.name a:hover { text-decoration: underline; }\n\n.reference, .how { margin: 0; color: var(--muted); font-size: .8125rem; line-height: 1.5; }\n\n.name + .how, .reference + .how { margin-top: .5rem; }\n.fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }\n.fingerprint span { display: inline-block; margin-right: .5ch; }\n.how.additional { padding-left: .75rem; }\n/* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n.how .signed-mark { display: inline-block; margin-left: .3125rem; padding: 0 .375rem; border: 1px solid var(--line); border-radius: 999px; font-size: .625rem; font-weight: 550; line-height: 1.3; text-decoration: none; vertical-align: .0625rem; }\n\n.fine { color: var(--muted); font-size: .8125rem; line-height: 1.55; margin-bottom: .625rem; }\n\n/* That the record is signed is a fact about it, so it is set as one and not as fine print. */\n.signed { font-size: .875rem; line-height: 1.55; margin-bottom: .875rem; }\n/* Arrived at from a badge's \"signed\" link, so the reader is shown where they landed. */\n.signed:target { margin-inline: -.75rem; padding: .625rem .75rem; border-radius: 8px; background: var(--raised); }\n\n/* Every time on the record in one column, so they can be compared rather than hunted for. */\ndl {\n display: grid;\n grid-template-columns: auto 1fr;\n gap: .3rem 1.5rem;\n margin: 1.25rem 0;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n font-size: .8125rem;\n}\n\ndt { color: var(--muted); }\n\ndd { margin: 0; font-variant-numeric: tabular-nums; }\n\n/* Inside a card the rule above it is the card's own edge, so the list needs none. */\nsection dl { margin: .5rem 0 1rem; padding-top: 0; border-top: 0; }\n.side dl { margin: .625rem 0 0; padding-top: .625rem; }\n\n/* The links under a record, ruled off from the record and its disclaimer. */\nfooter { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--line); }\nfooter > :last-child { margin-bottom: 0; }\n\nsection {\n margin: 0 0 1.25rem;\n padding: 1.25rem 1.375rem;\n border: 1px solid var(--line);\n border-radius: 10px;\n}\n\nsection > :last-child { margin-bottom: 0; }\n\nform { margin: 0 0 1.5rem; }\n\nlabel { display: block; margin: 0 0 .75rem; color: var(--muted); font-size: .8125rem; }\n\ninput, textarea {\n display: block;\n width: 100%;\n margin-top: .375rem;\n padding: .5rem .625rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--surface);\n color: var(--ink);\n font: .875rem/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;\n}\n\n/* Height comes from the rows attribute, which differs per box. */\ntextarea { resize: vertical; }\n\ninput:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }\n\nbutton {\n padding: .5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font: 500 .9375rem/1.4 system-ui, -apple-system, 'Segoe UI', sans-serif;\n cursor: pointer;\n}\n\n/* A link that starts something, drawn as a button. */\na.button {\n display: inline-block;\n padding: .5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font-weight: 500;\n font-size: .9375rem;\n line-height: 1.4;\n text-decoration: none;\n}\n\nbutton + button { margin-left: .5rem; border-color: var(--line); background: none; color: var(--ink); }\n\n/* The other choice beside a page's one action, such as Cancel. */\na.button.secondary { border-color: var(--line); background: none; color: var(--ink); }\n\n/* A page's action and its alternative, side by side. */\n.choice { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1.5rem; }\n.choice form { margin: 0; }\n\n/* Ways to verify: one full-width button per method, none filled over the others. */\n.choices { display: grid; gap: .5rem; margin-bottom: 1.5rem; }\n.choices form { margin: 0; }\n\n.choices button {\n width: 100%;\n text-align: left;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n}\n\n.choices button:hover { border-color: var(--muted); }\n\n/* A provider's mark before a button's label, at the height of the letters. */\nbutton .provider { width: 1rem; height: 1rem; margin-right: .5rem; vertical-align: -.1875rem; }\n\n/* A control that ends something. Filled where it is the page's one action. */\nbutton.danger { border-color: var(--warn); background: var(--warn); }\n\n/*\n * A connection on the settings page: the account's card, then what can be done with it.\n * Its state sits in the card's corner, on the line that names the method.\n */\n.connection { position: relative; margin-bottom: 1rem; }\n\n.state {\n position: absolute;\n top: .75rem;\n right: .875rem;\n display: flex;\n align-items: center;\n gap: .375rem;\n height: 1.625rem;\n padding: 0 .75rem 0 .5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--surface);\n color: var(--ink);\n font-size: .8125rem;\n font-weight: 550;\n}\n\n/*\n * The V the badge draws, in the tone that says how the record stands. A record that claims\n * nothing is drawn faint in the text colour, as the badge draws it.\n */\n.verdict { flex-shrink: 0; width: 1rem; height: 1rem; }\n.verdict.pending { opacity: .45; }\n.how .verdict { width: .875rem; height: .875rem; margin-right: .3125rem; vertical-align: -.125rem; }\n/* Cropped to its strokes and taller than the heading's letters, centred beside them. */\nh1:has(.verdict) { display: flex; align-items: center; gap: .75rem; }\nh1 .verdict { width: auto; height: 2.25rem; }\n\n/*\n * In a state pill the V is cropped to its strokes and fills the pill's height, so its arms\n * and its foot meet the border. The pill's left padding keeps the arms clear of its curve.\n */\n.state .verdict { width: auto; height: 100%; }\n\n/*\n * A pill's line includes room for descenders below the letters, which leaves its text\n * sitting high. Trimmed to the letters, the padding alone sets the space above and below.\n * In a state pill the word alone is trimmed, so it centres on the V by its letters.\n */\n@supports (text-box: trim-both cap alphabetic) {\n .listing { padding: .3125rem .5rem; text-box: trim-both cap alphabetic; }\n .state > span { text-box: trim-both cap alphabetic; }\n}\n\n/* Links that sit together on one line, wrapping where there is no room. */\n.links { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1.25rem; font-size: .875rem; }\n\n/* Under the account's id, with the facts about it rather than the controls. */\n.evidence { margin: .375rem 0 0; font-size: .8125rem; }\n\n.connection dl { margin-bottom: .75rem; }\n.connection .side > :last-child { margin-bottom: 0; }\n.connection pre { margin: .75rem 0; background: var(--surface); }\n.connection .actions { margin-top: .875rem; }\n\n/* The buttons for one connection in a row, wrapping where there is no room. */\n.actions { display: flex; flex-wrap: wrap; gap: .5rem; }\n.actions form { display: contents; }\n\n.actions button, .actions a.button {\n margin: 0;\n padding: .3125rem .75rem;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n font-size: .8125rem;\n}\n\n.actions button:hover, .actions a.button:hover { border-color: var(--muted); }\n\n/* The controls that cannot be undone, together at the foot of the card. */\n.ending { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem; margin-top: .75rem; padding-top: .75rem; border-top: 1px solid var(--line); }\n.ending form { margin: 0; }\n\n.ending button.danger {\n padding: .3125rem .75rem;\n background: var(--surface);\n color: var(--warn);\n font-size: .8125rem;\n}\n\nfieldset { margin: 0 0 1.25rem; padding: 1rem 1.125rem .25rem; border: 1px solid var(--line); border-radius: 8px; }\n\nlegend { padding: 0 .375rem; color: var(--muted); font-size: .8125rem; }\n\nfieldset label { display: flex; align-items: baseline; gap: .5rem; color: var(--ink); font-size: .9375rem; }\n\nfieldset input { width: auto; margin: 0; }\n\nfieldset p { margin: .25rem 0 .875rem 1.5rem; color: var(--muted); font-size: .8125rem; }\n";
12
12
  /**
13
13
  * A short digest of the sheet above, put in the link's query so a release that changes the
14
14
  * styling changes its address. Without that the only safe cache is no cache: a reader who
@@ -240,6 +240,10 @@ dd { margin: 0; font-variant-numeric: tabular-nums; }
240
240
  section dl { margin: .5rem 0 1rem; padding-top: 0; border-top: 0; }
241
241
  .side dl { margin: .625rem 0 0; padding-top: .625rem; }
242
242
 
243
+ /* The links under a record, ruled off from the record and its disclaimer. */
244
+ footer { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--line); }
245
+ footer > :last-child { margin-bottom: 0; }
246
+
243
247
  section {
244
248
  margin: 0 0 1.25rem;
245
249
  padding: 1.25rem 1.375rem;
@@ -303,6 +307,23 @@ a.button.secondary { border-color: var(--line); background: none; color: var(--i
303
307
  .choice { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1.5rem; }
304
308
  .choice form { margin: 0; }
305
309
 
310
+ /* Ways to verify: one full-width button per method, none filled over the others. */
311
+ .choices { display: grid; gap: .5rem; margin-bottom: 1.5rem; }
312
+ .choices form { margin: 0; }
313
+
314
+ .choices button {
315
+ width: 100%;
316
+ text-align: left;
317
+ border-color: var(--line);
318
+ background: var(--surface);
319
+ color: var(--ink);
320
+ }
321
+
322
+ .choices button:hover { border-color: var(--muted); }
323
+
324
+ /* A provider's mark before a button's label, at the height of the letters. */
325
+ button .provider { width: 1rem; height: 1rem; margin-right: .5rem; vertical-align: -.1875rem; }
326
+
306
327
  /* A control that ends something. Filled where it is the page's one action. */
307
328
  button.danger { border-color: var(--warn); background: var(--warn); }
308
329
 
@@ -336,7 +357,9 @@ button.danger { border-color: var(--warn); background: var(--warn); }
336
357
  .verdict { flex-shrink: 0; width: 1rem; height: 1rem; }
337
358
  .verdict.pending { opacity: .45; }
338
359
  .how .verdict { width: .875rem; height: .875rem; margin-right: .3125rem; vertical-align: -.125rem; }
339
- h1 .verdict { width: 1.25rem; height: 1.25rem; margin-right: .5rem; vertical-align: -.0625rem; }
360
+ /* Cropped to its strokes and taller than the heading's letters, centred beside them. */
361
+ h1:has(.verdict) { display: flex; align-items: center; gap: .75rem; }
362
+ h1 .verdict { width: auto; height: 2.25rem; }
340
363
 
341
364
  /*
342
365
  * In a state pill the V is cropped to its strokes and fills the pill's height, so its arms
package/dist/verily.js CHANGED
@@ -10520,7 +10520,7 @@ ${claims(evidence)}`;
10520
10520
  }
10521
10521
 
10522
10522
  // src/version.ts
10523
- var version = "v0.8.0";
10523
+ var version = "v0.8.1";
10524
10524
 
10525
10525
  // src/browser/evidence-dialog.ts
10526
10526
  function stampLink() {
package/dist/version.d.ts CHANGED
@@ -7,4 +7,4 @@
7
7
  * output stays what tsc emitted and nothing has to be substituted into it. A version bump
8
8
  * changes this line with package.json.
9
9
  */
10
- export declare const version = "v0.8.0";
10
+ export declare const version = "v0.8.1";
package/dist/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  * output stays what tsc emitted and nothing has to be substituted into it. A version bump
8
8
  * changes this line with package.json.
9
9
  */
10
- export const version = 'v0.8.0';
10
+ export const version = 'v0.8.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bkazemi/verily",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Self-hosted account-to-account verification for Node.js",
5
5
  "type": "module",
6
6
  "license": "MIT",