@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/README.md +25 -6
- package/dist/browser/badge.d.ts +4 -1
- package/dist/browser/badge.js +7 -4
- package/dist/browser/connect-dialog.js +10 -7
- package/dist/browser/evidence-dialog.d.ts +23 -4
- package/dist/browser/evidence-dialog.js +335 -49
- package/dist/browser/index.d.ts +7 -1
- package/dist/browser/index.js +27 -42
- package/dist/core/index.d.ts +106 -2
- package/dist/core/index.js +74 -2
- package/dist/server/discord.js +3 -0
- package/dist/server/domain.js +37 -10
- package/dist/server/github-gist.js +16 -4
- package/dist/server/github.js +3 -0
- package/dist/server/index.js +108 -11
- package/dist/server/link.js +9 -4
- package/dist/server/openpgp.d.ts +4 -0
- package/dist/server/openpgp.js +10 -1
- package/dist/server/service.d.ts +59 -2
- package/dist/server/service.js +240 -22
- package/dist/server/style.d.ts +1 -1
- package/dist/server/style.js +50 -1
- package/dist/server/youtube.js +5 -0
- package/dist/verily.js +345 -84
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -145,7 +145,7 @@ import { init } from '@bkazemi/verily'; // also registers <verily-badge>
|
|
|
145
145
|
const client = init({ backendUrl: '/api/verily' });
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
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.
|
|
148
|
+
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.7.0/dist/verily.js`. The script defines a global `Verily`.
|
|
149
149
|
|
|
150
150
|
Then place the badge wherever the account appears:
|
|
151
151
|
|
|
@@ -184,6 +184,8 @@ Add `stacked` to name the accounts in the badge itself, one to a row:
|
|
|
184
184
|
|
|
185
185
|
It lists up to four verified accounts in the order they were connected, then counts the rest as `+2 more`. It is still one badge that opens the one set of details. It is as tall as its rows, so give it a line of its own. With one verified account it is the ordinary badge. `mountBadges` and `presentConnections` take the same choices as `{ stacked: true }` and `{ peek: false }`.
|
|
186
186
|
|
|
187
|
+
In the details of several accounts, each account is a short card: its name, its status and who verified it. How it was shown and its dates sit behind a Details toggle on the card, and retired accounts are folded into a group at the foot. Add `details-open` to the badge, or pass `{ detailsOpen: true }`, to start every card open where your readers come for the proof. A reader can still open or shut each one. A single account always shows everything.
|
|
188
|
+
|
|
187
189
|
The badge refreshes every 30 seconds, and only ever reads public evidence. The client can also start and end connections, and draw a badge into an element of your own:
|
|
188
190
|
|
|
189
191
|
```js
|
|
@@ -229,6 +231,8 @@ import {
|
|
|
229
231
|
|
|
230
232
|
All methods except the sign-ins and the mailed code let the user publish the proof in their own time and come back. A proof that does not check out can be put right and handed back on the same step, up to five times, so a mistyped address never means publishing or signing again. Gists, link-backs, DNS records and files can be taken down later, so Verily re-reads them on a schedule. A PGP signature is kept by Verily and published at `<baseUrl>/connections/<id>/proof`.
|
|
231
233
|
|
|
234
|
+
**How old an account is.** A link shows who controls an account, not how long it has existed. Signing in with GitHub, Discord or YouTube also reads when the account was made: GitHub's `created_at`, the time inside a Discord id, and a YouTube channel's `publishedAt`. It is kept on the record as `external.createdAt`, in milliseconds, shown as "Joined" beside the account's name on evidence and in the badge's dialog, and covered by a signed record. The other methods read an address or a proof and learn no date, so a record first shown by one of them has none until its holder also signs in. A provider of your own sets it by returning `createdAt` from `authenticate()`.
|
|
235
|
+
|
|
232
236
|
**Several at once.** List more than one, and `/verify` offers each method as its own button:
|
|
233
237
|
|
|
234
238
|
```ts
|
|
@@ -299,7 +303,7 @@ A static site only needs the script and the badge markup; the backend runs somew
|
|
|
299
303
|
4. Add the script and the badge to your site:
|
|
300
304
|
|
|
301
305
|
```html
|
|
302
|
-
<script src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.
|
|
306
|
+
<script src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.7.0/dist/verily.js" defer></script>
|
|
303
307
|
<verily-badge
|
|
304
308
|
backend-url="https://your-backend.example"
|
|
305
309
|
connection-id="CONNECTION_ID"
|
|
@@ -391,7 +395,7 @@ To show them, hand the records to the badge. It draws them as given and fetches
|
|
|
391
395
|
|
|
392
396
|
From script, set the property instead: `badge.connections = records`. Send a page only the records its reader may see: whatever is in the page, the reader has. Several records become one badge, as under [Show the badge](#show-the-badge), and an unlisted one opens its details without linking anywhere.
|
|
393
397
|
|
|
394
|
-
Each record has the provider, the external account's handle and profile address, the visibility and the expiry. Unlisted records are included, and only your site can read them, so you can show a link to your own users without making it public. `connections()` takes any number of ids, so read a whole page of users in one call.
|
|
398
|
+
Each record has the provider, the external account's handle and profile address, the visibility and the expiry. Unlisted records are included, and only your site can read them, so you can show a link to your own users without making it public. `connections()` takes any number of ids, so read a whole page of users in one call. Every call reads the instance, so a removed or expired link stops showing on the next page load. Passing `cacheMs` to the client reuses an answer for that long, and a connection revoked in that time still shows as verified until it runs out, so leave it unset or keep it to a few seconds. `{ fresh: true }` makes one call read anyway.
|
|
395
399
|
|
|
396
400
|
If you would rather your user ids never left your site, give the client a secret:
|
|
397
401
|
|
|
@@ -443,7 +447,7 @@ With a key set:
|
|
|
443
447
|
- **`GET <baseUrl>/connections/<id>?format=signed`** returns the signed file for a public record that is currently verified: the record as JSON under an OpenPGP cleartext signature. It is signed when asked for, so it says the record stood at that moment. An unlisted, revoked or expired record has no signed form. A public retired record has one, signed as `version: 2` with `status: "retired"` and `retiredAt`, so a reader that knows only version 1 refuses it and never takes it for a record that stood.
|
|
444
448
|
- **`GET <baseUrl>/keys.asc`** is the public key as `gpg --import` reads it. **`GET <baseUrl>/keys`** lists the same keys as JSON, each with its fingerprint. The Cloudflare Worker also serves that list at `/.well-known/verily-keys.json`.
|
|
445
449
|
- **`<baseUrl>/check`** is a page where a saved file can be pasted and read back.
|
|
446
|
-
- The evidence page says the record is signed and links to the signed record. The badge's dialog checks the signature in the browser with OpenPGP.js, which it starts only when a record is signed, marks the record `signed
|
|
450
|
+
- The evidence page says the record is signed and links to the signed record. The badge's dialog checks the signature in the browser with OpenPGP.js, which it starts only when a record is signed, marks the record with a `signed` pill after its verifier, and links to that page. A record whose signature fails is shown as unconfirmed, with `bad signature` in the pill's place.
|
|
447
451
|
- The approval step tells the holder that a saved signed record outlives removal.
|
|
448
452
|
|
|
449
453
|
To check a file yourself:
|
|
@@ -507,6 +511,8 @@ The Cloudflare Worker sets `dns: true`.
|
|
|
507
511
|
|
|
508
512
|
## Operations
|
|
509
513
|
|
|
514
|
+
DNS setup is saved for seven days from its first attempt. If propagation takes longer than a flow’s ten minutes, sign in and choose DNS again to restore the same TXT value and the last domain entered (or restart the same renewal). Each return creates a fresh flow and reads the proof again; the saved setup grants no verification on its own. Only the most recent domain entered is remembered for that operation. Keep the TXT record after verification for ongoing checks. A proof covers the exact domain entered; verify parent domains and subdomains separately.
|
|
515
|
+
|
|
510
516
|
Verification lasts 30 days, flows 10 minutes, and sharing links 7 days. Change them with `validityMs`, `flowTtlMs` and `shareTtlMs`. Evidence is never cached.
|
|
511
517
|
|
|
512
518
|
Run the upkeep on a schedule, for example hourly:
|
|
@@ -525,8 +531,21 @@ setInterval(
|
|
|
525
531
|
);
|
|
526
532
|
```
|
|
527
533
|
|
|
528
|
-
- **`prune()`** deletes expired flows, and deletes expired or revoked records after 90 days. A retired record is kept until it is removed.
|
|
529
|
-
- **`recheck()`** re-reads gists, link-backs, DNS records and files that are due, up to 5 per run (its `budget` argument). Each proof is re-read every 24 hours (`recheckMs`) and stays current for 7 days after its last successful read (`freshnessMs`)
|
|
534
|
+
- **`prune()`** deletes expired flows and DNS drafts, and deletes expired or revoked records after 90 days. A retired record is kept until it is removed.
|
|
535
|
+
- **`recheck()`** re-reads gists, link-backs, DNS records and files that are due, up to 5 per provider per run (its `budget` argument). Each proof is re-read every 24 hours (`recheckMs`). A read that finds the proof gone, such as a deleted gist, a page without the link or a DNS answer without the record, makes the connection unconfirmed at once. A read that fails, such as a timeout or a server error, changes nothing, and the proof stays current for 7 days after its last successful read (`freshnessMs`). Either way the proof is read again after a quarter of `recheckMs`, and the connection is verified again as soon as a read finds the proof. A provider of your own says a proof is gone by throwing `Absent` from `verify`; any other error is a failed read. For PGP, it asks the keyserver whether the key has been revoked, and revokes the connection if so.
|
|
536
|
+
|
|
537
|
+
- **`onChange`** is an option, not a call. Set it to hear when a record's status, visibility, expiry or mark changes, so your site can update its own copy without polling:
|
|
538
|
+
|
|
539
|
+
```ts
|
|
540
|
+
createVerily({
|
|
541
|
+
// ...
|
|
542
|
+
onChange: async ({ evidence, local, revision }) => {
|
|
543
|
+
await db.saveConnectionIfNewer(local.id, evidence, revision);
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
`evidence` is the record as it stands now, `local` is your subject with its private id, `previous` is the status, visibility, expiry and mark last confirmed as delivered, and `revision` counts the deliveries of that record. Keep the `revision` you last applied for each record and ignore a change that is not higher: a call that timed out is not cancelled and can finish after a newer one. A change made by a request is delivered before that request returns, and the call is awaited, so keep it quick. A record that expires or goes unconfirmed changes with time alone, so those are delivered by `recheck()`, or by `service.announce()` if you do not recheck. Delivery is at least once: a call that throws is made again on the next run, after the records that have not failed, and turning the option on reports every existing record once, 50 per run. A record removed by `prune()` is not reported.
|
|
530
549
|
|
|
531
550
|
**If you never call `recheck()`, set `freshnessMs: Infinity`.** Otherwise connections proved by gist, link-back or PGP lapse after a week.
|
|
532
551
|
|
package/dist/browser/badge.d.ts
CHANGED
|
@@ -15,17 +15,20 @@ export declare function renderBadgeMessage(element: HTMLElement, message: string
|
|
|
15
15
|
* `more` is how many other accounts of the same subject stand behind the one shown, said
|
|
16
16
|
* as a count beside it. `linked` is whether the record has a page of its own a reader can
|
|
17
17
|
* open: an unlisted one has none, so its pill is a button and never a link to nowhere.
|
|
18
|
+
* `href` is that page, where it is not the record's own: a pill standing for several
|
|
19
|
+
* accounts links to all of them.
|
|
18
20
|
*
|
|
19
21
|
* `rows` are those of the other accounts to name, in order. Stacked, each has a row of
|
|
20
22
|
* its own in the pill, which is then as tall as its rows. Otherwise the pill stays short
|
|
21
23
|
* and, with `peek`, names them in a panel that opens beside it while a pointer rests on it
|
|
22
24
|
* or the keyboard is on it. Either way the accounts past the rows are a count.
|
|
23
25
|
*/
|
|
24
|
-
export declare function renderBadge(element: HTMLElement, evidence: Evidence, { more, linked, rows, peek, }?: {
|
|
26
|
+
export declare function renderBadge(element: HTMLElement, evidence: Evidence, { more, linked, rows, peek, href, }?: {
|
|
25
27
|
more?: number;
|
|
26
28
|
linked?: boolean;
|
|
27
29
|
rows?: Evidence[];
|
|
28
30
|
peek?: boolean;
|
|
31
|
+
href?: string;
|
|
29
32
|
}): HTMLElement | null;
|
|
30
33
|
/**
|
|
31
34
|
* Shuts a host's panel and lets go of what it was listening for. For a host leaving the
|
package/dist/browser/badge.js
CHANGED
|
@@ -155,11 +155,12 @@ export function badgeShown(element) {
|
|
|
155
155
|
return intact(element) && shown.has(element);
|
|
156
156
|
}
|
|
157
157
|
/** Everything the pill draws: equal keys mean an identical pill. */
|
|
158
|
-
function renderKey(evidence, current, label, more, linked, rows, peek) {
|
|
158
|
+
function renderKey(evidence, current, label, more, linked, rows, peek, href) {
|
|
159
159
|
return JSON.stringify([
|
|
160
160
|
more,
|
|
161
161
|
linked,
|
|
162
162
|
peek,
|
|
163
|
+
href,
|
|
163
164
|
rows.map((row) => [row.provider, row.providerName, externalName(row.external)]),
|
|
164
165
|
evidence.provider,
|
|
165
166
|
evidence.providerName,
|
|
@@ -220,13 +221,15 @@ export function renderBadgeMessage(element, message) {
|
|
|
220
221
|
* `more` is how many other accounts of the same subject stand behind the one shown, said
|
|
221
222
|
* as a count beside it. `linked` is whether the record has a page of its own a reader can
|
|
222
223
|
* open: an unlisted one has none, so its pill is a button and never a link to nowhere.
|
|
224
|
+
* `href` is that page, where it is not the record's own: a pill standing for several
|
|
225
|
+
* accounts links to all of them.
|
|
223
226
|
*
|
|
224
227
|
* `rows` are those of the other accounts to name, in order. Stacked, each has a row of
|
|
225
228
|
* its own in the pill, which is then as tall as its rows. Otherwise the pill stays short
|
|
226
229
|
* and, with `peek`, names them in a panel that opens beside it while a pointer rests on it
|
|
227
230
|
* or the keyboard is on it. Either way the accounts past the rows are a count.
|
|
228
231
|
*/
|
|
229
|
-
export function renderBadge(element, evidence, { more = 0, linked = true, rows = [], peek = false, } = {}) {
|
|
232
|
+
export function renderBadge(element, evidence, { more = 0, linked = true, rows = [], peek = false, href = evidence.evidenceUrl, } = {}) {
|
|
230
233
|
const provider = evidence.providerName;
|
|
231
234
|
const current = evidence.status === 'verified' && evidence.expiresAt > Date.now();
|
|
232
235
|
const retired = evidence.status === 'retired';
|
|
@@ -238,7 +241,7 @@ export function renderBadge(element, evidence, { more = 0, linked = true, rows =
|
|
|
238
241
|
? 'revoked'
|
|
239
242
|
: 'expired';
|
|
240
243
|
const label = statusLabel(evidence, Date.now());
|
|
241
|
-
const key = renderKey(evidence, current, label, more, linked, rows, peek);
|
|
244
|
+
const key = renderKey(evidence, current, label, more, linked, rows, peek, href);
|
|
242
245
|
if (intact(element) && shown.get(element) === key)
|
|
243
246
|
return null;
|
|
244
247
|
const handle = externalName(evidence.external);
|
|
@@ -250,7 +253,7 @@ export function renderBadge(element, evidence, { more = 0, linked = true, rows =
|
|
|
250
253
|
// is: in the page's text colour, and not as a verification that failed.
|
|
251
254
|
paintMark(mark, current ? 'current' : retired ? 'pending' : 'inactive');
|
|
252
255
|
if (linked) {
|
|
253
|
-
badge.href =
|
|
256
|
+
badge.href = href;
|
|
254
257
|
badge.rel = 'noreferrer';
|
|
255
258
|
}
|
|
256
259
|
else
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { accountCard, linkMark, node, outward, stampLink, styles } from './evidence-dialog.js';
|
|
1
|
+
import { accountCard, backMark, closeMark, linkMark, node, namedCard, outward, stampLink, styles, } from './evidence-dialog.js';
|
|
3
2
|
import { providerMark } from './provider-mark.js';
|
|
4
3
|
import { version } from '../version.js';
|
|
5
4
|
const connectStyles = `
|
|
@@ -14,13 +13,15 @@ const connectStyles = `
|
|
|
14
13
|
/* The block scrolls and its frame does not, so the copy button stays in its corner. */
|
|
15
14
|
.code { position: relative; margin: 10px 0; }
|
|
16
15
|
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; }
|
|
17
|
-
.code .action { position: absolute; top:
|
|
16
|
+
.code .action { position: absolute; top: 9px; right: 9px; padding: 3px 8px; font-size: 11px; }
|
|
18
17
|
label { display: grid; gap: 4px; margin-top: 12px; font-weight: 600; }
|
|
19
18
|
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; }
|
|
20
19
|
textarea { font-family: ui-monospace, monospace; font-size: 12px; resize: vertical; }
|
|
21
20
|
fieldset { margin: 16px 0 0; padding: 10px 14px; border: 1px solid #e0e6df; border-radius: 10px; }
|
|
22
21
|
legend { padding: 0 4px; color: #6b786f; font-size: 11px; font-weight: 550; }
|
|
23
22
|
fieldset label { display: flex; align-items: baseline; gap: 8px; margin-top: 4px; font-weight: 500; }
|
|
23
|
+
/* A radio has no baseline to speak of, so it is set in the middle of the label's first line. */
|
|
24
|
+
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; }
|
|
24
25
|
.error { color: #9b2c33; }
|
|
25
26
|
`;
|
|
26
27
|
/** Puts a copy control on a block the holder must reproduce exactly, as the flow page does. */
|
|
@@ -110,7 +111,7 @@ renew) {
|
|
|
110
111
|
root.adoptedStyleSheets = [sheet];
|
|
111
112
|
const dialog = node('dialog');
|
|
112
113
|
const heading = node('h2', 'Verify an account');
|
|
113
|
-
const close = node('button', '
|
|
114
|
+
const close = node('button', '', 'icon');
|
|
114
115
|
const header = node('header');
|
|
115
116
|
const stamp = node('footer');
|
|
116
117
|
const content = node('div', 'Loading…', 'steps');
|
|
@@ -118,6 +119,7 @@ renew) {
|
|
|
118
119
|
dialog.setAttribute('aria-labelledby', heading.id);
|
|
119
120
|
close.type = 'button';
|
|
120
121
|
close.setAttribute('aria-label', 'Close');
|
|
122
|
+
close.append(closeMark());
|
|
121
123
|
content.setAttribute('aria-live', 'polite');
|
|
122
124
|
stamp.append(stampLink(), node('span', version));
|
|
123
125
|
header.append(heading, close);
|
|
@@ -127,9 +129,10 @@ renew) {
|
|
|
127
129
|
openDialogs.set(opener, dialog);
|
|
128
130
|
let returning = false;
|
|
129
131
|
if (back) {
|
|
130
|
-
const before = node('button', '
|
|
132
|
+
const before = node('button', '', 'icon');
|
|
131
133
|
before.type = 'button';
|
|
132
134
|
before.setAttribute('aria-label', 'Back');
|
|
135
|
+
before.append(backMark());
|
|
133
136
|
header.className = 'backed';
|
|
134
137
|
header.replaceChildren(before, heading, close);
|
|
135
138
|
before.onclick = () => {
|
|
@@ -431,9 +434,9 @@ renew) {
|
|
|
431
434
|
}
|
|
432
435
|
function approve(flow) {
|
|
433
436
|
const logo = providerMark(flow.provider.id, flow.provider.method);
|
|
434
|
-
const external =
|
|
437
|
+
const external = namedCard(logo
|
|
435
438
|
? [logo, document.createTextNode(flow.provider.name)]
|
|
436
|
-
: [document.createTextNode(flow.provider.name)],
|
|
439
|
+
: [document.createTextNode(flow.provider.name)], flow.external);
|
|
437
440
|
const parts = [subjectCard(flow.local), linkMark(), external];
|
|
438
441
|
const choice = node('fieldset');
|
|
439
442
|
const visibilityText = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type Evidence } from '../core/index.js';
|
|
1
|
+
import { type Evidence, type ExternalAccount } from '../core/index.js';
|
|
2
2
|
/** The logotype as a link to the project, as both dialogs carry it in their footer. */
|
|
3
3
|
export declare function stampLink(): HTMLAnchorElement;
|
|
4
|
-
export declare const styles = "\n * { box-sizing: border-box; }\n dialog { width: min(460px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); margin: auto; padding: 24px; border: 1px solid #dce2de; border-radius: 16px; background: #fff; color: #23312b; box-shadow: 0 24px 90px #10201935; font: 13px/1.6 system-ui, sans-serif; }\n dialog::backdrop { background: #15271f66; }\n header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }\n h2 { margin: 0; font-size: 18px; font-weight: 650; letter-spacing: -.3px; }\n button { width: 32px; height: 32px; border: 1px solid #dce2de; border-radius: 8px; background: #fff; color: #52645a; font: 20px system-ui, sans-serif; cursor: pointer; }\n button:hover { background: #f2f5f1; }\n a { color: #245f43; text-underline-offset: 3px; overflow-wrap: anywhere; }\n a:focus-visible, button:focus-visible { outline: 2px solid #357ce5; outline-offset: 3px; }\n .summary { display: flex; align-items: center; gap: 8px; margin-top: 16px; }\n .mark { width: 36px; height: 36px; flex-shrink: 0; }\n .provider { width: 14px; height: 14px; }\n .state { font-size: 14px; font-weight: 650; line-height: 1.4; }\n .muted { color: #6b786f; font-size: 12px; }\n .account { position: relative; }\n .veiled > :not(.checking) { filter: blur(4px); opacity: .55; user-select: none; }\n .checking { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 8px; color: #23312b; font-size: 13px; font-weight: 600; }\n .spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin .7s linear infinite; }\n @keyframes spin { to { transform: rotate(360deg); } }\n @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }\n .
|
|
4
|
+
export declare const styles = "\n * { box-sizing: border-box; }\n dialog { width: min(460px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); margin: auto; padding: 24px; border: 1px solid #dce2de; border-radius: 16px; background: #fff; color: #23312b; box-shadow: 0 24px 90px #10201935; font: 13px/1.6 system-ui, sans-serif; }\n dialog::backdrop { background: #15271f66; }\n header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }\n h2 { margin: 0; font-size: 18px; font-weight: 650; letter-spacing: -.3px; }\n button { width: 32px; height: 32px; border: 1px solid #dce2de; border-radius: 8px; background: #fff; color: #52645a; font: 20px system-ui, sans-serif; cursor: pointer; }\n button:hover { background: #f2f5f1; }\n .icon { display: grid; place-items: center; padding: 0; }\n .icon svg { width: 16px; height: 16px; }\n .title { display: flex; align-items: center; gap: 6px; min-width: 0; }\n .all { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; color: #52645a; }\n .all:hover { background: #f2f5f1; }\n .all svg { width: 16px; height: 16px; }\n a { color: #245f43; text-underline-offset: 3px; overflow-wrap: anywhere; }\n a:focus-visible, button:focus-visible, summary:focus-visible { outline: 2px solid #357ce5; outline-offset: 3px; }\n /* An element given its own display would otherwise go on showing while hidden. */\n [hidden] { display: none !important; }\n .summary { display: flex; align-items: center; gap: 8px; margin-top: 16px; }\n .mark { width: 36px; height: 36px; flex-shrink: 0; }\n .provider { width: 14px; height: 14px; }\n .state { font-size: 14px; font-weight: 650; line-height: 1.4; }\n .muted { color: #6b786f; font-size: 12px; }\n .account { position: relative; }\n .veiled > :not(.checking) { filter: blur(4px); opacity: .55; user-select: none; }\n .checking { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 8px; color: #23312b; font-size: 13px; font-weight: 600; }\n .spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin .7s linear infinite; }\n @keyframes spin { to { transform: rotate(360deg); } }\n @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }\n /* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n .summary .signed { display: inline-block; margin-left: 5px; padding: 0 6px; border: 1px solid #cfdcd3; border-radius: 999px; font-size: 10px; font-weight: 550; line-height: 1.3; text-decoration: none; vertical-align: 1px; }\n .summary a.signed:hover { background: #f2f5f1; }\n .summary .signed.bad { border-color: #b3261e; color: #b3261e; font-weight: 650; cursor: help; }\n .caution { display: inline-block; margin-left: 3px; color: #a15c00; vertical-align: -1.5px; }\n .caution svg { display: block; width: 10px; height: 10px; }\n .account { padding: 14px 16px; border: 1px solid #e0e6df; border-radius: 10px; margin-top: 12px; }\n .account h3 { display: flex; align-items: center; gap: 6px; margin: 0 0 3px; color: #6b786f; font-size: 11px; font-weight: 550; }\n .account a, .account strong { font-weight: 650; font-size: 14px; }\n /* The account's own name carries that weight; a link inside a line of prose does not. */\n .summary a, .method a, .additional a { font: inherit; }\n .reference { margin-top: 2px; }\n /* A fingerprint in fours: it breaks between groups, and copies as one unbroken word. */\n .fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }\n .fingerprint span { display: inline-block; margin-right: .5ch; }\n .method { margin-top: 8px; }\n /* In the corner the heading leaves empty, apart from everything that was checked. */\n .listing { position: absolute; top: 12px; right: 14px; padding: 1px 8px; border: 1px solid #e0e6df; border-radius: 999px; color: #6b786f; font-size: 11px; font-weight: 550; }\n .listing.preferred { border-color: #245f43; color: #245f43; }\n /* Quiet, as its pill is: the surface and the name step back, and the controls do not. */\n .account.retired { background: #f6f8f5; }\n .account.retired > a, .account.retired > strong, .account.retired > .named > a, .account.retired > .named > strong { color: #6b786f; }\n /* The name, then a dot and the account's creation date. The date wraps under a long name. */\n /* Clipped 4px left of the text, which hides the dot when the date wraps to a new line. */\n .named { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 14px; margin-left: -4px; padding-left: 4px; overflow-x: clip; }\n .since { position: relative; white-space: nowrap; }\n /* The dot sits in the gap before the date. */\n .since::before { content: \"\u00B7\"; position: absolute; left: -7px; transform: translateX(-50%); }\n .account.retired .mark { opacity: .45; }\n .group { margin: 20px 0 0; color: #6b786f; font-size: 11px; font-weight: 550; }\n summary.group { display: flex; align-items: center; gap: 4px; width: fit-content; border-radius: 4px; list-style: none; cursor: pointer; }\n summary::-webkit-details-marker { display: none; }\n .fold { display: flex; align-items: center; gap: 4px; width: auto; height: auto; padding: 2px 8px 2px 4px; margin-left: -4px; border: 0; background: none; font: 550 12px/1.6 system-ui, sans-serif; }\n .fold svg, summary svg { width: 12px; height: 12px; }\n .fold[aria-expanded=\"true\"] svg, details[open] > summary svg { transform: rotate(90deg); }\n @media (prefers-reduced-motion: no-preference) { .fold svg, summary svg { transition: transform .15s; } }\n /* The fold and what the holder may do share a line, until a question needs the whole of it. */\n .bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 10px; }\n .account .bar .row { margin: 0 0 0 auto; }\n .additional { margin-top: 2px; padding-left: 12px; }\n .joiner { display: block; width: 20px; height: 20px; margin: 8px auto -4px; color: #90a096; }\n dl { margin: 16px 0 0; padding-top: 12px; border-top: 1px solid #e5e9e3; display: grid; grid-template-columns: auto 1fr; gap: 5px 16px; font-size: 11px; }\n dt { color: #6b786f; }\n dd { margin: 0; text-align: right; overflow-wrap: anywhere; }\n .explanation { margin-top: 16px; }\n .action { width: auto; height: auto; padding: 9px 14px; font: 600 13px/1.3 system-ui, sans-serif; color: #23312b; text-align: left; }\n .action.primary { border-color: #245f43; background: #245f43; color: #fff; }\n .action.primary:hover { background: #1d4f37; }\n .action:disabled { opacity: .6; cursor: progress; }\n .action.danger:hover { border-color: #b3261e; background: #fdecea; color: #b3261e; }\n .row { display: flex; justify-content: end; gap: 8px; margin-top: 16px; }\n .account .row { align-items: center; justify-content: start; flex-wrap: wrap; margin-top: 12px; }\n .account .action { padding: 5px 10px; font-size: 12px; }\n .account .action.dots { display: grid; place-items: center; align-self: stretch; width: 30px; padding: 0; }\n .dots svg { width: 16px; height: 16px; }\n /* Placed under its button each time it opens, above the dialog and clipped by none of it. */\n .menu { position: fixed; inset: auto; margin: 0; min-width: 170px; padding: 6px; border: 1px solid #dce2de; border-radius: 10px; background: #fff; color: #23312b; box-shadow: 0 12px 40px #10201930; }\n .menu:popover-open { display: grid; }\n .account .menu .action { border: 0; border-radius: 6px; padding: 6px 10px; font-weight: 550; }\n .menu .danger { color: #b3261e; }\n .menu hr { width: 100%; margin: 4px 0; border: 0; border-top: 1px solid #e5e9e3; }\n footer { display: flex; align-items: center; justify-content: start; gap: 6px; margin: 20px 0 -8px; color: #9aa9a0; font-size: 11px; }\n footer a { display: flex; color: inherit; }\n .logo { display: block; height: 13px; }\n \n @media (prefers-reduced-motion: no-preference) {\n .mark.current path:last-child { animation: verily-draw .8s cubic-bezier(.65, 0, .35, 1) .1s backwards; }\n }\n @keyframes verily-draw { from { stroke-dasharray: 0 176; } to { stroke-dasharray: 108 176; } }\n\n";
|
|
5
5
|
export declare function node<K extends keyof HTMLElementTagNameMap>(tag: K, text?: string, className?: string): HTMLElementTagNameMap[K];
|
|
6
6
|
/**
|
|
7
7
|
* Points an anchor out of the dialog. Every link here opens in a new tab: the dialog is
|
|
@@ -9,6 +9,15 @@ export declare function node<K extends keyof HTMLElementTagNameMap>(tag: K, text
|
|
|
9
9
|
* take the reader off the page the pill was on. `noreferrer` keeps the opener unreachable.
|
|
10
10
|
*/
|
|
11
11
|
export declare function outward(anchor: HTMLAnchorElement, url: string): HTMLAnchorElement;
|
|
12
|
+
/**
|
|
13
|
+
* Where the verifier lists every public record of a record's subject. A record whose
|
|
14
|
+
* address is not the usual one is left linking to itself.
|
|
15
|
+
*/
|
|
16
|
+
export declare function subjectUrl(e: Evidence): string;
|
|
17
|
+
/** A cross drawn in a square, so it sits in the middle of its button in any font. */
|
|
18
|
+
export declare const closeMark: () => SVGSVGElement;
|
|
19
|
+
/** Points back the way the reader came, drawn for the same reason the cross is. */
|
|
20
|
+
export declare const backMark: () => SVGSVGElement;
|
|
12
21
|
/** Joins the two cards: the link itself, drawn rather than described. */
|
|
13
22
|
export declare function linkMark(): SVGSVGElement;
|
|
14
23
|
/**
|
|
@@ -16,7 +25,12 @@ export declare function linkMark(): SVGSVGElement;
|
|
|
16
25
|
* `extra` carries evidence belonging to that side alone: a provider authenticates and
|
|
17
26
|
* expires on its own terms, and a second provider on the same subject would differ.
|
|
18
27
|
*/
|
|
19
|
-
export declare function accountCard(heading: Node[], name: string, reference: string | undefined, url?: string, ...extra: HTMLElement[]): HTMLElement;
|
|
28
|
+
export declare function accountCard(heading: Node[], name: string, reference: string | string[] | undefined, url?: string, ...extra: HTMLElement[]): HTMLElement;
|
|
29
|
+
/**
|
|
30
|
+
* The card for an external account: its name, link and id, with the "Joined <date>" text
|
|
31
|
+
* on the name's line when the creation date is known.
|
|
32
|
+
*/
|
|
33
|
+
export declare function namedCard(heading: Node[], external: ExternalAccount, ...extra: HTMLElement[]): HTMLElement;
|
|
20
34
|
/**
|
|
21
35
|
* A record as a card shows it. Where the card speaks for several records of one account,
|
|
22
36
|
* `links` names every one not yet revoked, whether or not it is shown, so removing the
|
|
@@ -74,4 +88,9 @@ arrange?: (records: Evidence[]) => Account[],
|
|
|
74
88
|
* Given where whoever is looking holds these accounts: each then has its renewal and
|
|
75
89
|
* removal under it, and the dialog offers to connect another.
|
|
76
90
|
*/
|
|
77
|
-
manage?: Manage
|
|
91
|
+
manage?: Manage,
|
|
92
|
+
/**
|
|
93
|
+
* Whether the cards of several accounts start with their details open. They start shut
|
|
94
|
+
* unless the page asked otherwise, and a reader may turn each either way from there.
|
|
95
|
+
*/
|
|
96
|
+
detailsOpen?: boolean): void;
|