@bkazemi/verily 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/browser/badge.d.ts +4 -1
- package/dist/browser/badge.js +7 -4
- package/dist/browser/connect-dialog.js +10 -6
- package/dist/browser/evidence-dialog.d.ts +17 -3
- package/dist/browser/evidence-dialog.js +278 -28
- package/dist/browser/index.d.ts +7 -1
- package/dist/browser/index.js +24 -42
- package/dist/core/index.d.ts +28 -0
- package/dist/core/index.js +48 -0
- package/dist/server/index.js +83 -7
- package/dist/server/openpgp.d.ts +4 -0
- package/dist/server/openpgp.js +10 -1
- package/dist/server/service.d.ts +15 -1
- package/dist/server/service.js +15 -3
- package/dist/server/style.d.ts +1 -1
- package/dist/server/style.js +40 -0
- package/dist/verily.js +286 -64
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/core/index.d.ts
CHANGED
|
@@ -547,6 +547,29 @@ export declare function lastProved(evidence: Pick<Evidence, 'authenticatedAt' |
|
|
|
547
547
|
* that has run out since reads as expired whatever status it was issued with.
|
|
548
548
|
*/
|
|
549
549
|
export declare function statusLabel(evidence: Pick<Evidence, 'status' | 'expiresAt'>, now: number): string;
|
|
550
|
+
/**
|
|
551
|
+
* Where an account is listed among a subject's others, before the order they were
|
|
552
|
+
* connected in. This is all there is to preferring an account: there is no separate pin.
|
|
553
|
+
*/
|
|
554
|
+
export declare function place(account: Pick<Evidence, 'status' | 'mark'>): number;
|
|
555
|
+
/**
|
|
556
|
+
* One subject's records as its accounts, in the order they are listed everywhere: the
|
|
557
|
+
* preferred one, then the rest as first connected, then those unused, then those retired.
|
|
558
|
+
* Records are not accounts: the same account can stand on several. `held` is every record
|
|
559
|
+
* of the account, earliest first, and `verified` those of them that stand now. `lead`
|
|
560
|
+
* speaks for the account: the earliest that stands, or else the latest approved, which
|
|
561
|
+
* says what became of it. `external` is how the account was shown: the lead's methods,
|
|
562
|
+
* then any other its standing records add.
|
|
563
|
+
*
|
|
564
|
+
* A record from a backend older than `connectedAt` is placed by when it was approved.
|
|
565
|
+
*/
|
|
566
|
+
export declare function accountsOf(records: Evidence[], now: number): {
|
|
567
|
+
lead: Evidence;
|
|
568
|
+
held: Evidence[];
|
|
569
|
+
verified: Evidence[];
|
|
570
|
+
external: Attestations['external'];
|
|
571
|
+
connectedAt: number;
|
|
572
|
+
}[];
|
|
550
573
|
/**
|
|
551
574
|
* What to call a link's local side, and what to write as its value. When the site itself is
|
|
552
575
|
* what was linked there is no subject on it to name, so the site is the value rather than a
|
|
@@ -584,6 +607,11 @@ export declare function attestationLabel(method: string, names: {
|
|
|
584
607
|
* already all of it: a mailbox's address and a domain are each their own id.
|
|
585
608
|
*/
|
|
586
609
|
export declare function externalId(external: ExternalAccount): string | undefined;
|
|
610
|
+
/**
|
|
611
|
+
* A key's fingerprint in groups of four, as gpg prints it, so a renderer can break the line
|
|
612
|
+
* between groups. Nothing for any other subject, whose id is written as it stands.
|
|
613
|
+
*/
|
|
614
|
+
export declare function externalIdGroups(external: ExternalAccount): string[] | undefined;
|
|
587
615
|
/**
|
|
588
616
|
* Where a subject's name may link, or nothing where it has no page of its own. A mailbox's
|
|
589
617
|
* address is `mailto:`, which opens a message to it and shows a reader nothing.
|
package/dist/core/index.js
CHANGED
|
@@ -79,6 +79,47 @@ export function statusLabel(evidence, now) {
|
|
|
79
79
|
return 'Expired';
|
|
80
80
|
return evidence.status === 'unconfirmed' ? 'Unconfirmed' : 'Verified';
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Where an account is listed among a subject's others, before the order they were
|
|
84
|
+
* connected in. This is all there is to preferring an account: there is no separate pin.
|
|
85
|
+
*/
|
|
86
|
+
export function place(account) {
|
|
87
|
+
if (account.status === 'retired')
|
|
88
|
+
return 3;
|
|
89
|
+
return account.mark === 'preferred' ? 0 : account.mark === 'unused' ? 2 : 1;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* One subject's records as its accounts, in the order they are listed everywhere: the
|
|
93
|
+
* preferred one, then the rest as first connected, then those unused, then those retired.
|
|
94
|
+
* Records are not accounts: the same account can stand on several. `held` is every record
|
|
95
|
+
* of the account, earliest first, and `verified` those of them that stand now. `lead`
|
|
96
|
+
* speaks for the account: the earliest that stands, or else the latest approved, which
|
|
97
|
+
* says what became of it. `external` is how the account was shown: the lead's methods,
|
|
98
|
+
* then any other its standing records add.
|
|
99
|
+
*
|
|
100
|
+
* A record from a backend older than `connectedAt` is placed by when it was approved.
|
|
101
|
+
*/
|
|
102
|
+
export function accountsOf(records, now) {
|
|
103
|
+
const when = (e) => e.connectedAt ?? e.approvedAt;
|
|
104
|
+
const byAccount = new Map();
|
|
105
|
+
for (const record of records) {
|
|
106
|
+
const key = JSON.stringify([record.provider, record.external.id]);
|
|
107
|
+
byAccount.set(key, [...(byAccount.get(key) ?? []), record]);
|
|
108
|
+
}
|
|
109
|
+
return [...byAccount.values()]
|
|
110
|
+
.map((unsorted) => {
|
|
111
|
+
const held = [...unsorted].sort((a, b) => when(a) - when(b));
|
|
112
|
+
const verified = held.filter((e) => e.status === 'verified' && e.expiresAt > now);
|
|
113
|
+
const lead = verified[0] ?? held.reduce((a, b) => (b.approvedAt > a.approvedAt ? b : a));
|
|
114
|
+
const external = [...lead.attestations.external];
|
|
115
|
+
for (const other of verified.slice(1))
|
|
116
|
+
for (const attestation of other.attestations.external)
|
|
117
|
+
if (!external.some((shown) => shown.method === attestation.method))
|
|
118
|
+
external.push(attestation);
|
|
119
|
+
return { lead, held, verified, external, connectedAt: when(held[0]) };
|
|
120
|
+
})
|
|
121
|
+
.sort((a, b) => place(a.lead) - place(b.lead) || a.connectedAt - b.connectedAt);
|
|
122
|
+
}
|
|
82
123
|
/**
|
|
83
124
|
* What to call a link's local side, and what to write as its value. When the site itself is
|
|
84
125
|
* what was linked there is no subject on it to name, so the site is the value rather than a
|
|
@@ -140,6 +181,13 @@ export function attestationLabel(method, names, proof = {}) {
|
|
|
140
181
|
export function externalId(external) {
|
|
141
182
|
return external.kind === 'mailbox' || external.kind === 'domain' ? undefined : external.id;
|
|
142
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* A key's fingerprint in groups of four, as gpg prints it, so a renderer can break the line
|
|
186
|
+
* between groups. Nothing for any other subject, whose id is written as it stands.
|
|
187
|
+
*/
|
|
188
|
+
export function externalIdGroups(external) {
|
|
189
|
+
return external.kind === 'key' ? (external.id.match(/.{1,4}/g) ?? undefined) : undefined;
|
|
190
|
+
}
|
|
143
191
|
/**
|
|
144
192
|
* Where a subject's name may link, or nothing where it has no page of its own. A mailbox's
|
|
145
193
|
* address is `mailto:`, which opens a message to it and shows a reader nothing.
|
package/dist/server/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { attestationLabel, externalId, externalLink, externalName, isArtifactProvider, isCodeProvider, isRedirectProvider, lastProved, localSide, proofTitle, providerMethod, statusLabel, signedBy, } from '../core/index.js';
|
|
1
|
+
import { attestationLabel, externalId, externalIdGroups, externalLink, externalName, isArtifactProvider, isCodeProvider, isRedirectProvider, lastProved, accountsOf, localSide, proofTitle, providerMethod, statusLabel, signedBy, } from '../core/index.js';
|
|
2
2
|
import { artifactAttempts, codeAttempts, VerilyService, Unavailable, } from './service.js';
|
|
3
3
|
import { copyScript, waitScript } from './copy.js';
|
|
4
4
|
import { escape } from './escape.js';
|
|
@@ -85,7 +85,11 @@ const redirect = (url, cookie) => new Response(null, {
|
|
|
85
85
|
*/
|
|
86
86
|
function card(heading, name, url, reference, ...extra) {
|
|
87
87
|
const profile = url && safeUrl(url);
|
|
88
|
-
return `<div class="side"><p class="who">${escape(heading)}</p><p class="name">${profile ? `<a href="${escape(profile)}" rel="noreferrer">${escape(name)}</a>` : escape(name)}</p>${
|
|
88
|
+
return `<div class="side"><p class="who">${escape(heading)}</p><p class="name">${profile ? `<a href="${escape(profile)}" rel="noreferrer">${escape(name)}</a>` : escape(name)}</p>${typeof reference === 'string'
|
|
89
|
+
? `<p class="reference">${escape(reference)}</p>`
|
|
90
|
+
: reference
|
|
91
|
+
? `<p class="reference fingerprint">${reference.map((group) => `<span>${escape(group)}</span>`).join('')}</p>`
|
|
92
|
+
: ''}${extra.join('')}</div>`;
|
|
89
93
|
}
|
|
90
94
|
/**
|
|
91
95
|
* Names how one side was established, inside that side's card. Where the method published
|
|
@@ -295,7 +299,7 @@ function evidencePage(e, base, report, keyId, named) {
|
|
|
295
299
|
card(local.heading, local.value, e.local.profileUrl, undefined, attestationNote(e.attestations.local, names)) +
|
|
296
300
|
card(names.provider, externalName(e.external), externalLink(e.external),
|
|
297
301
|
// A mailbox's address or a domain is its name already, so there is no second identifier.
|
|
298
|
-
externalId(e.external), externalNotes(e.attestations, names)) +
|
|
302
|
+
externalIdGroups(e.external) ?? externalId(e.external), externalNotes(e.attestations, names)) +
|
|
299
303
|
times(e.status === 'retired'
|
|
300
304
|
? [
|
|
301
305
|
['Approved', e.approvedAt],
|
|
@@ -329,6 +333,66 @@ function evidencePage(e, base, report, keyId, named) {
|
|
|
329
333
|
${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>` : ''}
|
|
330
334
|
<p class="fine"><a href="${escape(report)}" rel="noreferrer">Report an incorrect record</a></p>`);
|
|
331
335
|
}
|
|
336
|
+
/** Joins a subject's card to its accounts: the link itself, drawn as the dialog draws it. */
|
|
337
|
+
const joiner = '<svg class="joiner" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">' +
|
|
338
|
+
[
|
|
339
|
+
'M10.5 7.5 13 5a4.95 4.95 0 0 1 7 7l-2.5 2.5',
|
|
340
|
+
'M13.5 16.5 11 19a4.95 4.95 0 0 1-7-7l2.5-2.5',
|
|
341
|
+
'M9 15l6-6',
|
|
342
|
+
]
|
|
343
|
+
.map((d) => `<path d="${d}" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>`)
|
|
344
|
+
.join('') +
|
|
345
|
+
'</svg>';
|
|
346
|
+
/**
|
|
347
|
+
* One account as a badge's dialog draws it: what it is and how it stands, the verifier's
|
|
348
|
+
* own page for it, how it was shown, how its holder lists it, and its times.
|
|
349
|
+
*/
|
|
350
|
+
function accountCard(e, external, now) {
|
|
351
|
+
const retired = e.status === 'retired';
|
|
352
|
+
const record = escape(safeUrl(e.evidenceUrl));
|
|
353
|
+
return card(e.providerName, externalName(e.external), externalLink(e.external), externalIdGroups(e.external) ?? externalId(e.external), `<p class="how">${escape(statusLabel(e, now))} · via: <a href="${record}">${escape(e.verifierName)}</a>${e.signedUrl ? ` · <a href="${record}#signed">signed</a>` : ''}</p>`, externalNotes({ ...e.attestations, external }, { site: e.siteName, provider: e.providerName }), times(retired
|
|
354
|
+
? [
|
|
355
|
+
['Approved', e.approvedAt],
|
|
356
|
+
['Last verified', lastProved(e)],
|
|
357
|
+
['Retired', e.retiredAt],
|
|
358
|
+
]
|
|
359
|
+
: [
|
|
360
|
+
['Approved', e.approvedAt],
|
|
361
|
+
[e.expiresAt > now ? 'Valid until' : 'Expired on', e.expiresAt],
|
|
362
|
+
['Last checked', external[0].artifactUrl ? external[0].confirmedAt : undefined],
|
|
363
|
+
]),
|
|
364
|
+
// The holder's own word on the account, which nothing checked, so it sits apart from
|
|
365
|
+
// the methods, in the card's corner as the dialog has it.
|
|
366
|
+
retired || !e.mark
|
|
367
|
+
? ''
|
|
368
|
+
: e.mark === 'preferred'
|
|
369
|
+
? '<div class="listing preferred">Preferred</div>'
|
|
370
|
+
: '<div class="listing">No longer used</div>');
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* The public records given, as a page that shows what a badge's dialog does: a card to a
|
|
374
|
+
* subject, then one to each of its accounts in the badge's own order.
|
|
375
|
+
*/
|
|
376
|
+
function publishedPage(records, base) {
|
|
377
|
+
const now = Date.now();
|
|
378
|
+
const subjects = new Map();
|
|
379
|
+
for (const e of records) {
|
|
380
|
+
const key = JSON.stringify([e.siteName, e.local.reference]);
|
|
381
|
+
subjects.set(key, [...(subjects.get(key) ?? []), e]);
|
|
382
|
+
}
|
|
383
|
+
return page(base, 'Public connections', records.length
|
|
384
|
+
? [...subjects.values()]
|
|
385
|
+
.map((held) => {
|
|
386
|
+
const local = localSide(held[0].local, held[0].siteName);
|
|
387
|
+
return (card(local.heading, local.value, held[0].local.profileUrl) +
|
|
388
|
+
joiner +
|
|
389
|
+
`<div class="accounts">${accountsOf(held, now)
|
|
390
|
+
.map(({ lead, external }) => accountCard(lead, external, now))
|
|
391
|
+
.join('')}</div>`);
|
|
392
|
+
})
|
|
393
|
+
.join('')
|
|
394
|
+
: '<p>No public connections.</p>');
|
|
395
|
+
}
|
|
332
396
|
/**
|
|
333
397
|
* Names a signing key on one line: its last sixteen digits, linked to the key itself with
|
|
334
398
|
* the whole fingerprint on hover, then whether a domain's DNS was read naming it. That is a
|
|
@@ -355,7 +419,7 @@ function checkedPage(d, keyId, base, named) {
|
|
|
355
419
|
const retired = d.version === 2;
|
|
356
420
|
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>` +
|
|
357
421
|
card(local.heading, local.value, d.local.profileUrl, undefined, attestationNote(d.attestations.local, names)) +
|
|
358
|
-
card(names.provider, externalName(d.external), externalLink(d.external), externalId(d.external), externalNotes(d.attestations, names)) +
|
|
422
|
+
card(names.provider, externalName(d.external), externalLink(d.external), externalIdGroups(d.external) ?? externalId(d.external), externalNotes(d.attestations, names)) +
|
|
359
423
|
times(retired
|
|
360
424
|
? [
|
|
361
425
|
['Signed', d.issuedAt],
|
|
@@ -644,6 +708,8 @@ export function createVerily(options) {
|
|
|
644
708
|
// With one visibility there is nothing for the holder to change it to.
|
|
645
709
|
if (kind === 'visibility' && visibilities(user).length < 2)
|
|
646
710
|
throw new Unavailable();
|
|
711
|
+
// Several providers are named by their buttons, not strung together in the heading.
|
|
712
|
+
const several = new Set(offered.map((p) => p.name)).size > 1;
|
|
647
713
|
// One form per method. With one on offer the provider is all there is to name.
|
|
648
714
|
// A new link started from this page always ends on its approval, as it did when
|
|
649
715
|
// this form was a GET, so it says so: `/verify` is an address any site can send a
|
|
@@ -655,7 +721,9 @@ export function createVerily(options) {
|
|
|
655
721
|
? 'Remove a connection'
|
|
656
722
|
: kind === 'renew'
|
|
657
723
|
? 'Renew this connection'
|
|
658
|
-
:
|
|
724
|
+
: several
|
|
725
|
+
? 'Verify an account'
|
|
726
|
+
: `Verify with ${providerNames(offered)}`, `
|
|
659
727
|
${user
|
|
660
728
|
? card(subject(user).heading, subject(user).value, user.profileUrl)
|
|
661
729
|
: '<p>Authenticate with the matching external account to review and remove this link.</p>'}
|
|
@@ -744,7 +812,7 @@ export function createVerily(options) {
|
|
|
744
812
|
? 'Add to connection'
|
|
745
813
|
: 'Confirm connection', `
|
|
746
814
|
${card(subject(flow.local).heading, subject(flow.local).value, flow.local.profileUrl)}
|
|
747
|
-
${card(provider.name, externalName(flow.external), externalLink(flow.external), externalId(flow.external))}
|
|
815
|
+
${card(provider.name, externalName(flow.external), externalLink(flow.external), externalIdGroups(flow.external) ?? externalId(flow.external))}
|
|
748
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>` : ''}
|
|
749
817
|
${found ? `<p>${escape(found)}</p>` : ''}
|
|
750
818
|
<p class="fine">${escape(service.siteOf(flow.local))} receives the result. Verified via ${escape(options.verifierName)}.</p>
|
|
@@ -782,7 +850,15 @@ export function createVerily(options) {
|
|
|
782
850
|
}
|
|
783
851
|
// Lets a static embed track current connections without hardcoding an id.
|
|
784
852
|
if (path === '/published') {
|
|
785
|
-
const
|
|
853
|
+
const site = url.searchParams.get('site');
|
|
854
|
+
const reference = url.searchParams.get('reference') ?? undefined;
|
|
855
|
+
const subject = site === null ? undefined : { site, reference };
|
|
856
|
+
// A page only where one is asked for by name. An embed published before `Accept`
|
|
857
|
+
// was sent asks for nothing, and must go on getting the list.
|
|
858
|
+
if (url.searchParams.get('format') !== 'json' &&
|
|
859
|
+
/\btext\/html\b/i.test(request.headers.get('accept') ?? ''))
|
|
860
|
+
return html(publishedPage(await service.listed(subject), prefix));
|
|
861
|
+
const response = json(await service.published(subject));
|
|
786
862
|
response.headers.set('Access-Control-Allow-Origin', '*');
|
|
787
863
|
return response;
|
|
788
864
|
}
|
package/dist/server/openpgp.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ import { Refused } from '../core/index.js';
|
|
|
10
10
|
export declare class Malformed extends Refused {
|
|
11
11
|
constructor(what: string, options?: ErrorOptions);
|
|
12
12
|
}
|
|
13
|
+
/** The block is not there at all: a holder who pasted half is told which half is missing. */
|
|
14
|
+
export declare class Missing extends Malformed {
|
|
15
|
+
constructor(what: string);
|
|
16
|
+
}
|
|
13
17
|
export type Certificate = Key;
|
|
14
18
|
export type Cleartext = CleartextMessage;
|
|
15
19
|
/** The public key in armored text, wherever in the text its block is. */
|
package/dist/server/openpgp.js
CHANGED
|
@@ -34,6 +34,13 @@ export class Malformed extends Refused {
|
|
|
34
34
|
super(`Malformed OpenPGP ${what}`, options);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
/** The block is not there at all: a holder who pasted half is told which half is missing. */
|
|
38
|
+
export class Missing extends Malformed {
|
|
39
|
+
constructor(what) {
|
|
40
|
+
super(what);
|
|
41
|
+
this.message = `No OpenPGP ${what} found`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
37
44
|
/**
|
|
38
45
|
* The one armored block of the named kind out of text that may hold others: a holder
|
|
39
46
|
* pastes the signed message and the key together, and the library reads only the first
|
|
@@ -43,8 +50,10 @@ function block(text, begin, end, what) {
|
|
|
43
50
|
if (text.length > maxArmorBytes)
|
|
44
51
|
throw new Malformed(what);
|
|
45
52
|
const start = text.indexOf(`-----BEGIN PGP ${begin}-----`);
|
|
53
|
+
if (start < 0)
|
|
54
|
+
throw new Missing(what);
|
|
46
55
|
const stop = text.indexOf(`-----END PGP ${end}-----`, start);
|
|
47
|
-
if (
|
|
56
|
+
if (stop < 0)
|
|
48
57
|
throw new Malformed(what);
|
|
49
58
|
return text.slice(start, stop + `-----END PGP ${end}-----`.length);
|
|
50
59
|
}
|
package/dist/server/service.d.ts
CHANGED
|
@@ -2,6 +2,14 @@ import { type Attestations, type Connection, type Evidence, type ExternalAccount
|
|
|
2
2
|
import { type DnsOptions, type Published } from './domain.js';
|
|
3
3
|
export declare const secret: () => string;
|
|
4
4
|
export declare const hash: (value: string) => string;
|
|
5
|
+
/**
|
|
6
|
+
* A subject as its public records name it: the site it belongs to and its reference there.
|
|
7
|
+
* A reference means nothing outside its site, so with none this is everyone on the site.
|
|
8
|
+
*/
|
|
9
|
+
export interface Subject {
|
|
10
|
+
site: string;
|
|
11
|
+
reference?: string;
|
|
12
|
+
}
|
|
5
13
|
export declare class Unavailable extends Error {
|
|
6
14
|
constructor();
|
|
7
15
|
}
|
|
@@ -345,7 +353,13 @@ export declare class VerilyService {
|
|
|
345
353
|
* hardcoding an id that dies whenever one is revoked and replaced. Unlisted records are
|
|
346
354
|
* never included: they must not appear in any directory listing.
|
|
347
355
|
*/
|
|
348
|
-
published(): Promise<Evidence[]>;
|
|
356
|
+
published(subject?: Subject): Promise<Evidence[]>;
|
|
357
|
+
/**
|
|
358
|
+
* Every public record a badge could show, for the page a person reads: one that has lapsed
|
|
359
|
+
* or been retired is there to be marked, and one removed is not. `subject` narrows it to
|
|
360
|
+
* one site or one subject, named by what its records already publish. Earliest connected first.
|
|
361
|
+
*/
|
|
362
|
+
listed(subject?: Subject): Promise<Evidence[]>;
|
|
349
363
|
/**
|
|
350
364
|
* Every record this instance holds, public and unlisted, for whoever runs it. No route
|
|
351
365
|
* of the library serves this: a deployment that shows it must first know the reader is
|
package/dist/server/service.js
CHANGED
|
@@ -1174,11 +1174,23 @@ export class VerilyService {
|
|
|
1174
1174
|
* hardcoding an id that dies whenever one is revoked and replaced. Unlisted records are
|
|
1175
1175
|
* never included: they must not appear in any directory listing.
|
|
1176
1176
|
*/
|
|
1177
|
-
async published() {
|
|
1177
|
+
async published(subject) {
|
|
1178
|
+
return (await this.listed(subject)).filter((e) => e.status === 'verified');
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Every public record a badge could show, for the page a person reads: one that has lapsed
|
|
1182
|
+
* or been retired is there to be marked, and one removed is not. `subject` narrows it to
|
|
1183
|
+
* one site or one subject, named by what its records already publish. Earliest connected first.
|
|
1184
|
+
*/
|
|
1185
|
+
async listed(subject) {
|
|
1178
1186
|
return this.transaction(async (tx) => (await tx.list('connections'))
|
|
1179
|
-
.filter((c) => c.visibility === 'public'
|
|
1187
|
+
.filter((c) => c.visibility === 'public' &&
|
|
1188
|
+
(!subject ||
|
|
1189
|
+
(this.siteOf(c.local) === subject.site &&
|
|
1190
|
+
(subject.reference === undefined || c.local.reference === subject.reference))))
|
|
1180
1191
|
.map((c) => this.evidence(c))
|
|
1181
|
-
.filter((e) => e.status
|
|
1192
|
+
.filter((e) => e.status !== 'revoked')
|
|
1193
|
+
.sort((a, b) => a.connectedAt - b.connectedAt));
|
|
1182
1194
|
}
|
|
1183
1195
|
/**
|
|
1184
1196
|
* Every record this instance holds, public and unlisted, for whoever runs it. No route
|
package/dist/server/style.d.ts
CHANGED
|
@@ -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 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 }\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\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.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.name { margin: 0; font-size: 1.0625rem; font-weight: 620; letter-spacing: -.01em; }\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.how.additional { padding-left: .75rem; }\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\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 inherit;\n cursor: pointer;\n}\n\nbutton + button { margin-left: .5rem; border-color: var(--line); background: none; color: var(--ink); }\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 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 }\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\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.name { margin: 0; font-size: 1.0625rem; font-weight: 620; letter-spacing: -.01em; }\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\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 inherit;\n cursor: pointer;\n}\n\nbutton + button { margin-left: .5rem; border-color: var(--line); background: none; color: var(--ink); }\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
|
package/dist/server/style.js
CHANGED
|
@@ -134,6 +134,43 @@ code { font: .8125rem/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
|
134
134
|
background: var(--raised);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/*
|
|
138
|
+
* A subject's accounts are peers, so where there is room they sit two across. The page
|
|
139
|
+
* that lists them is wider for it, and a narrow one keeps them in a single column.
|
|
140
|
+
*/
|
|
141
|
+
main:has(.accounts) { max-width: 54rem; }
|
|
142
|
+
|
|
143
|
+
.accounts {
|
|
144
|
+
display: grid;
|
|
145
|
+
grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
|
|
146
|
+
gap: .75rem;
|
|
147
|
+
margin-bottom: .75rem;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.accounts .side { position: relative; display: flex; flex-direction: column; margin-bottom: 0; }
|
|
151
|
+
|
|
152
|
+
/* How the holder lists the account, in the card's corner and out of the lines below. */
|
|
153
|
+
.listing {
|
|
154
|
+
position: absolute;
|
|
155
|
+
top: .75rem;
|
|
156
|
+
right: .875rem;
|
|
157
|
+
padding: .0625rem .5rem;
|
|
158
|
+
border: 1px solid var(--line);
|
|
159
|
+
border-radius: 999px;
|
|
160
|
+
color: var(--muted);
|
|
161
|
+
font-size: .6875rem;
|
|
162
|
+
font-weight: 550;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.listing.preferred { border-color: var(--accent); color: var(--accent); }
|
|
166
|
+
|
|
167
|
+
/* The times sit at the foot of the card, so they line up along a row of uneven cards. */
|
|
168
|
+
.accounts .side dl { margin-top: auto; }
|
|
169
|
+
.accounts .side p:last-of-type { margin-bottom: .625rem; }
|
|
170
|
+
|
|
171
|
+
/* Between a subject and its accounts on the page that lists them. */
|
|
172
|
+
.joiner { display: block; width: 1.25rem; height: 1.25rem; margin: 0 auto .75rem; color: var(--muted); }
|
|
173
|
+
|
|
137
174
|
.who {
|
|
138
175
|
margin: 0 0 .125rem;
|
|
139
176
|
color: var(--muted);
|
|
@@ -152,6 +189,8 @@ code { font: .8125rem/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
|
152
189
|
.reference, .how { margin: 0; color: var(--muted); font-size: .8125rem; line-height: 1.5; }
|
|
153
190
|
|
|
154
191
|
.name + .how, .reference + .how { margin-top: .5rem; }
|
|
192
|
+
.fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }
|
|
193
|
+
.fingerprint span { display: inline-block; margin-right: .5ch; }
|
|
155
194
|
.how.additional { padding-left: .75rem; }
|
|
156
195
|
|
|
157
196
|
.fine { color: var(--muted); font-size: .8125rem; line-height: 1.55; margin-bottom: .625rem; }
|
|
@@ -178,6 +217,7 @@ dd { margin: 0; font-variant-numeric: tabular-nums; }
|
|
|
178
217
|
|
|
179
218
|
/* Inside a card the rule above it is the card's own edge, so the list needs none. */
|
|
180
219
|
section dl { margin: .5rem 0 1rem; padding-top: 0; border-top: 0; }
|
|
220
|
+
.side dl { margin: .625rem 0 0; padding-top: .625rem; }
|
|
181
221
|
|
|
182
222
|
section {
|
|
183
223
|
margin: 0 0 1.25rem;
|