@bkazemi/verily 0.7.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 +183 -94
- package/dist/browser/mark.d.ts +2 -6
- package/dist/browser/mark.js +6 -12
- package/dist/browser/provider-mark.d.ts +3 -3
- package/dist/browser/provider-mark.js +15 -90
- package/dist/instance/auth.d.ts +14 -0
- package/dist/instance/auth.js +80 -0
- package/dist/instance/bin.d.ts +2 -0
- package/dist/instance/bin.js +36 -0
- package/dist/instance/cloudflare.d.ts +57 -0
- package/dist/instance/cloudflare.js +71 -0
- package/dist/instance/env.d.ts +51 -0
- package/dist/instance/env.js +84 -0
- package/dist/instance/index.d.ts +136 -0
- package/dist/instance/index.js +777 -0
- package/dist/instance/init.d.ts +6 -0
- package/dist/instance/init.js +28 -0
- package/dist/instance/limits.d.ts +30 -0
- package/dist/instance/limits.js +45 -0
- package/dist/instance/node.d.ts +13 -0
- package/dist/instance/node.js +43 -0
- package/dist/instance/page.d.ts +18 -0
- package/dist/instance/page.js +30 -0
- package/dist/instance/postgres.d.ts +19 -0
- package/dist/instance/postgres.js +83 -0
- package/dist/instance/routes.d.ts +7 -0
- package/dist/instance/routes.js +22 -0
- package/dist/instance/serve.d.ts +31 -0
- package/dist/instance/serve.js +49 -0
- package/dist/instance/sites.d.ts +169 -0
- package/dist/instance/sites.js +413 -0
- package/dist/instance/starter.d.ts +6 -0
- package/dist/instance/starter.js +99 -0
- package/dist/instance/store.d.ts +20 -0
- package/dist/instance/store.js +11 -0
- package/dist/instance/undici.d.ts +8 -0
- package/dist/instance/undici.js +9 -0
- package/dist/marks.d.ts +44 -0
- package/dist/marks.js +98 -0
- package/dist/server/index.js +46 -67
- package/dist/server/render.d.ts +49 -0
- package/dist/server/render.js +104 -0
- package/dist/server/style.d.ts +1 -1
- package/dist/server/style.js +145 -1
- package/dist/verily.js +96 -104
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +30 -2
package/dist/marks.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** GitHub mark from Primer Octicons (MIT); license in docs/licenses/octicons.txt. */
|
|
2
|
+
const github = {
|
|
3
|
+
viewBox: '0 0 16 16',
|
|
4
|
+
paths: [
|
|
5
|
+
'M6.766 11.328c-2.063-.25-3.516-1.734-3.516-3.656 0-.781.281-1.625.75-2.188-.203-.515-.172-1.609.063-2.062.625-.078 1.468.25 1.968.703.594-.187 1.219-.281 1.985-.281.765 0 1.39.094 1.953.265.484-.437 1.344-.765 1.969-.687.218.422.25 1.515.046 2.047.5.593.766 1.39.766 2.203 0 1.922-1.453 3.375-3.547 3.64.531.344.89 1.094.89 1.954v1.625c0 .468.391.734.86.547C13.781 14.359 16 11.53 16 8.03 16 3.61 12.406 0 7.984 0 3.563 0 0 3.61 0 8.031a7.88 7.88 0 0 0 5.172 7.422c.422.156.828-.125.828-.547v-1.25c-.219.094-.5.156-.75.156-1.031 0-1.64-.562-2.078-1.609-.172-.422-.36-.672-.719-.719-.187-.015-.25-.093-.25-.187 0-.188.313-.328.625-.328.453 0 .844.281 1.25.86.313.452.64.655 1.031.655s.641-.14 1-.5c.266-.265.47-.5.657-.656',
|
|
6
|
+
],
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Discord mark from Simple Icons (CC0); notice in docs/licenses/simple-icons.txt. The mark
|
|
10
|
+
* itself is Discord's trademark, used here only to say an account is a Discord account.
|
|
11
|
+
*/
|
|
12
|
+
const discord = {
|
|
13
|
+
viewBox: '0 0 24 24',
|
|
14
|
+
paths: [
|
|
15
|
+
'M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z',
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
/** YouTube mark from Simple Icons (CC0); notice in docs/licenses/simple-icons.txt. */
|
|
19
|
+
const youtube = {
|
|
20
|
+
viewBox: '0 0 24 24',
|
|
21
|
+
paths: [
|
|
22
|
+
'M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z',
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A key, drawn here rather than taken from anywhere. It is not the OpenPGP logo: that mark
|
|
27
|
+
* is somebody's to license and this repository does not ship artwork it cannot account for.
|
|
28
|
+
* A key is also the truer picture, since what was proved is control of one, not membership
|
|
29
|
+
* of an organisation.
|
|
30
|
+
*/
|
|
31
|
+
const key = {
|
|
32
|
+
viewBox: '0 0 16 16',
|
|
33
|
+
stroke: true,
|
|
34
|
+
paths: [
|
|
35
|
+
'M7.4 8.6a3.3 3.3 0 1 0-4.7 4.7 3.3 3.3 0 0 0 4.7-4.7Z',
|
|
36
|
+
'M7.4 8.6 14 2',
|
|
37
|
+
'M11.2 4.8l1.6 1.6',
|
|
38
|
+
'M9.4 6.6l1.6 1.6',
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
/** A globe, drawn here like the key, for a page or a domain on the open web. */
|
|
42
|
+
const globe = {
|
|
43
|
+
viewBox: '0 0 16 16',
|
|
44
|
+
stroke: true,
|
|
45
|
+
paths: [
|
|
46
|
+
'M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Z',
|
|
47
|
+
'M1.5 8h13',
|
|
48
|
+
'M8 1.5C6.1 3.3 5.1 5.5 5.1 8s1 4.7 2.9 6.5',
|
|
49
|
+
'M8 1.5c1.9 1.8 2.9 4 2.9 6.5s-1 4.7-2.9 6.5',
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
/** An envelope, drawn here like the key, for a mailbox that read a code sent to it. */
|
|
53
|
+
const envelope = {
|
|
54
|
+
viewBox: '0 0 16 16',
|
|
55
|
+
stroke: true,
|
|
56
|
+
paths: ['M2 3.5h12v9H2z', 'M2.4 4l5.6 4.6L13.6 4'],
|
|
57
|
+
};
|
|
58
|
+
const marks = { github, discord, youtube, openpgp: key, email: envelope };
|
|
59
|
+
/**
|
|
60
|
+
* The mark for a provider, or nothing where there is none. Returning nothing rather than
|
|
61
|
+
* the provider's name keeps this to one job: a caller that also writes the name would
|
|
62
|
+
* otherwise print it twice, and only the caller knows where the name belongs.
|
|
63
|
+
*/
|
|
64
|
+
export function markOf(provider, method) {
|
|
65
|
+
// A site names its own link-back providers, so those fall back on the method's mark,
|
|
66
|
+
// which a domain shown by its DNS or by a file shares.
|
|
67
|
+
return (marks[provider] ?? (['backlink', 'dns', 'wellknown'].includes(method ?? '') ? globe : undefined));
|
|
68
|
+
}
|
|
69
|
+
/** The attributes every path of a mark gets, as name and value pairs. */
|
|
70
|
+
export const pathAttributes = (mark) => mark.stroke
|
|
71
|
+
? [
|
|
72
|
+
['stroke', 'currentColor'],
|
|
73
|
+
['stroke-width', '1.6'],
|
|
74
|
+
['stroke-linecap', 'round'],
|
|
75
|
+
['stroke-linejoin', 'round'],
|
|
76
|
+
]
|
|
77
|
+
: [];
|
|
78
|
+
/** The V's two strokes in each tone: the V, then the check drawn inside it. */
|
|
79
|
+
export const tones = {
|
|
80
|
+
current: ['#D3444C', '#149766'],
|
|
81
|
+
inactive: ['#149766', '#D3444C'],
|
|
82
|
+
pending: ['currentColor', 'currentColor'],
|
|
83
|
+
};
|
|
84
|
+
/** The tone a record's mark is drawn in. A retired account claims nothing, not a failure. */
|
|
85
|
+
export function toneOf(evidence, now) {
|
|
86
|
+
if (evidence.status === 'verified' && evidence.expiresAt > now)
|
|
87
|
+
return 'current';
|
|
88
|
+
return evidence.status === 'retired' ? 'pending' : 'inactive';
|
|
89
|
+
}
|
|
90
|
+
/** V2: the green check fills its section of the symmetric red V. */
|
|
91
|
+
export const verification = {
|
|
92
|
+
viewBox: '-4 -4 264 264',
|
|
93
|
+
/** The box around the strokes alone: the arms' squared ends and the mitred foot. */
|
|
94
|
+
strokes: '25.6 29.1 204.8 228',
|
|
95
|
+
path: 'M40 36 128 220 216 36',
|
|
96
|
+
/** The check's stretch of the V, as a dash along a path measured as 176 long. */
|
|
97
|
+
check: { pathLength: '176', 'stroke-dasharray': '108 176', 'stroke-dashoffset': '-54' },
|
|
98
|
+
};
|
package/dist/server/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { attestationLabel,
|
|
1
|
+
import { attestationLabel, 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';
|
|
5
|
-
import {
|
|
5
|
+
import { card, externalCard, marked, moment, page, safeUrl, times, verdict } from './render.js';
|
|
6
|
+
import { toneOf } from '../marks.js';
|
|
6
7
|
import { stylesheet, styleVersion } from './style.js';
|
|
7
8
|
export { VerilyService, Unavailable } from './service.js';
|
|
8
9
|
export { githubProvider } from './github.js';
|
|
@@ -22,7 +23,6 @@ export { resendSender } from './resend.js';
|
|
|
22
23
|
* one runs to tens of kilobytes. Still small enough that the size itself costs nothing.
|
|
23
24
|
*/
|
|
24
25
|
const maxBodyBytes = 65536;
|
|
25
|
-
const page = (prefix, title, body) => `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escape(title)} · Verily</title><link rel="stylesheet" href="${escape(prefix)}/style.css?v=${styleVersion}"><body><main>${logo}<h1>${escape(title)}</h1>${body}</main></body></html>`;
|
|
26
26
|
const headers = {
|
|
27
27
|
'Cache-Control': 'no-store',
|
|
28
28
|
'Referrer-Policy': 'same-origin',
|
|
@@ -76,33 +76,6 @@ const redirect = (url, cookie) => new Response(null, {
|
|
|
76
76
|
status: 303,
|
|
77
77
|
headers: { ...headers, Location: url, ...(cookie ? { 'Set-Cookie': cookie } : {}) },
|
|
78
78
|
});
|
|
79
|
-
/**
|
|
80
|
-
* One side of a link, with each thing known about it on its own line. A name, the
|
|
81
|
-
* identifier behind it and how it was shown are three separate claims, and a reader
|
|
82
|
-
* running them together in one sentence is the way to misread which was established.
|
|
83
|
-
*
|
|
84
|
-
* Both sides render as the same card, so the pair reads as a pair.
|
|
85
|
-
*/
|
|
86
|
-
function card(heading, name, url, reference, ...extra) {
|
|
87
|
-
return side(heading, name, url, reference, undefined, extra);
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* The card for an external account: its name, link and id, with the "Joined <date>" text
|
|
91
|
-
* on the name's line when the creation date is known.
|
|
92
|
-
*/
|
|
93
|
-
function externalCard(heading, external, ...extra) {
|
|
94
|
-
return side(heading, externalName(external), externalLink(external),
|
|
95
|
-
// A mailbox's address or a domain is its name already, so there is no second identifier.
|
|
96
|
-
externalIdGroups(external) ?? externalId(external), externalSince(external, (time) => new Date(time).toISOString().slice(0, 10)), extra);
|
|
97
|
-
}
|
|
98
|
-
function side(heading, name, url, reference, since, extra) {
|
|
99
|
-
const profile = url && safeUrl(url);
|
|
100
|
-
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)}${since ? `<span class="since">${escape(since)}</span>` : ''}</p>${typeof reference === 'string'
|
|
101
|
-
? `<p class="reference">${escape(reference)}</p>`
|
|
102
|
-
: reference
|
|
103
|
-
? `<p class="reference fingerprint">${reference.map((group) => `<span>${escape(group)}</span>`).join('')}</p>`
|
|
104
|
-
: ''}${extra.join('')}</div>`;
|
|
105
|
-
}
|
|
106
79
|
/**
|
|
107
80
|
* Names how one side was established, inside that side's card. Where the method published
|
|
108
81
|
* a proof the name is the link to it, which is what lets the reader check the claim
|
|
@@ -228,17 +201,18 @@ function heldNames(providers) {
|
|
|
228
201
|
const names = [...new Set(providers.map(held))];
|
|
229
202
|
return names.length > 1 ? `${names.slice(0, -1).join(', ')} or ${names.at(-1)}` : names[0];
|
|
230
203
|
}
|
|
231
|
-
/**
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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];
|
|
242
216
|
}
|
|
243
217
|
/** Provider setup with the flow’s return instructions, shared by HTML and the dialog. */
|
|
244
218
|
function proofInstructions(provider, flow) {
|
|
@@ -273,15 +247,6 @@ function inline(piece) {
|
|
|
273
247
|
? `<a href="${escape(href)}" rel="noreferrer" target="_blank">${escape(piece.text)}</a>`
|
|
274
248
|
: escape(piece.text);
|
|
275
249
|
}
|
|
276
|
-
function safeUrl(value) {
|
|
277
|
-
try {
|
|
278
|
-
const url = new URL(value);
|
|
279
|
-
return ['https:', 'http:'].includes(url.protocol) ? url.href : undefined;
|
|
280
|
-
}
|
|
281
|
-
catch {
|
|
282
|
-
return undefined;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
250
|
/**
|
|
286
251
|
* The holder's choice of who may read the record, or the one visibility there is when the
|
|
287
252
|
* subject has no choice. A subject from another site is told what each means there: that
|
|
@@ -320,7 +285,8 @@ function evidencePage(e, base, report, keyId, named) {
|
|
|
320
285
|
// site's own reference would be a third line saying the same thing. The provider's
|
|
321
286
|
// identifier stays: that one is the provider's word, not the site's own wording.
|
|
322
287
|
card(local.heading, local.value, e.local.profileUrl, undefined, attestationNote(e.attestations.local, names)) +
|
|
323
|
-
|
|
288
|
+
joiner +
|
|
289
|
+
externalCard(marked(names.provider, e.provider, e.attestations.external[0].method), e.external, externalNotes(e.attestations, names)) +
|
|
324
290
|
times(e.status === 'retired'
|
|
325
291
|
? [
|
|
326
292
|
['Approved', e.approvedAt],
|
|
@@ -350,9 +316,9 @@ function evidencePage(e, base, report, keyId, named) {
|
|
|
350
316
|
`${e.linkExpiresAt ? '<p class="fine">Anyone with this link can view and forward it.</p>' : ''}
|
|
351
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>` : ''}
|
|
352
318
|
<p class="fine">This connection does not establish legal identity, trustworthiness, content authorship, or permanent ownership.</p>
|
|
353
|
-
<p class="fine"
|
|
354
|
-
${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>` : ''}
|
|
355
|
-
<p class="fine"><a href="${escape(report)}" rel="noreferrer">Report an incorrect record</a></p
|
|
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));
|
|
356
322
|
}
|
|
357
323
|
/** Joins a subject's card to its accounts: the link itself, drawn as the dialog draws it. */
|
|
358
324
|
const joiner = '<svg class="joiner" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">' +
|
|
@@ -371,7 +337,7 @@ const joiner = '<svg class="joiner" viewBox="0 0 24 24" fill="none" aria-hidden=
|
|
|
371
337
|
function accountCard(e, external, now) {
|
|
372
338
|
const retired = e.status === 'retired';
|
|
373
339
|
const record = escape(safeUrl(e.evidenceUrl));
|
|
374
|
-
return externalCard(e.providerName, e.external, `<p class="how">${escape(statusLabel(e, now))} · via: <a href="${record}">${escape(e.verifierName)}</a>${e.signedUrl ? `<a class="signed-mark" href="${record}#signed">signed</a>` : ''}</p>`, externalNotes({ ...e.attestations, external }, { site: e.siteName, provider: e.providerName }), times(retired
|
|
340
|
+
return externalCard(marked(e.providerName, e.provider, e.attestations.external[0].method), e.external, `<p class="how">${verdict(toneOf(e, now))}${escape(statusLabel(e, now))} · via: <a href="${record}">${escape(e.verifierName)}</a>${e.signedUrl ? `<a class="signed-mark" href="${record}#signed">signed</a>` : ''}</p>`, externalNotes({ ...e.attestations, external }, { site: e.siteName, provider: e.providerName }), times(retired
|
|
375
341
|
? [
|
|
376
342
|
['Approved', e.approvedAt],
|
|
377
343
|
['Last verified', lastProved(e)],
|
|
@@ -440,7 +406,8 @@ function checkedPage(d, keyId, base, named) {
|
|
|
440
406
|
const retired = d.version === 2;
|
|
441
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>` +
|
|
442
408
|
card(local.heading, local.value, d.local.profileUrl, undefined, attestationNote(d.attestations.local, names)) +
|
|
443
|
-
|
|
409
|
+
joiner +
|
|
410
|
+
externalCard(marked(names.provider, d.provider, d.attestations.external[0].method), d.external, externalNotes(d.attestations, names)) +
|
|
444
411
|
times(retired
|
|
445
412
|
? [
|
|
446
413
|
['Signed', d.issuedAt],
|
|
@@ -735,22 +702,33 @@ export function createVerily(options) {
|
|
|
735
702
|
// A new link started from this page always ends on its approval, as it did when
|
|
736
703
|
// this form was a GET, so it says so: `/verify` is an address any site can send a
|
|
737
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
|
+
};
|
|
738
712
|
const forms = offered
|
|
739
|
-
.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>${
|
|
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>`)
|
|
740
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;
|
|
741
717
|
return html(page(prefix, external
|
|
742
|
-
?
|
|
718
|
+
? kind === 'share-revoke'
|
|
719
|
+
? 'Revoke a sharing link'
|
|
720
|
+
: 'Remove a connection'
|
|
743
721
|
: kind === 'renew'
|
|
744
722
|
? 'Renew this connection'
|
|
745
|
-
:
|
|
746
|
-
? '
|
|
747
|
-
:
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
<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>
|
|
752
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>' : ''}
|
|
753
|
-
${
|
|
731
|
+
${choices}`));
|
|
754
732
|
}
|
|
755
733
|
if (path === '/callback') {
|
|
756
734
|
const state = url.searchParams.get('state');
|
|
@@ -833,7 +811,8 @@ export function createVerily(options) {
|
|
|
833
811
|
? 'Add to connection'
|
|
834
812
|
: 'Confirm connection', `
|
|
835
813
|
${card(subject(flow.local).heading, subject(flow.local).value, flow.local.profileUrl)}
|
|
836
|
-
${
|
|
814
|
+
${joiner}
|
|
815
|
+
${externalCard(marked(provider.name, provider.id, providerMethod(provider)), flow.external)}
|
|
837
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>` : ''}
|
|
838
817
|
${found ? `<p>${escape(found)}</p>` : ''}
|
|
839
818
|
<p class="fine">${escape(service.siteOf(flow.local))} receives the result. Verified via ${escape(options.verifierName)}.</p>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type ExternalAccount } from '../core/index.js';
|
|
2
|
+
import { type MarkTone } from '../marks.js';
|
|
3
|
+
/**
|
|
4
|
+
* The shell every Verily page shares: the stylesheet, the logotype and the heading. `variant`
|
|
5
|
+
* is a class on the page's `main`, such as `single` for a page with one thing on it.
|
|
6
|
+
* `script` is the path of one script the page runs. `mark` is drawn before the heading.
|
|
7
|
+
*/
|
|
8
|
+
export declare const page: (prefix: string, title: string, body: string, variant?: string, script?: string, mark?: string) => string;
|
|
9
|
+
/**
|
|
10
|
+
* The V the badge draws for a record, in the tone that says how the record stands.
|
|
11
|
+
* `cropped` fits the box to the strokes, for a V that has to meet the edges around it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function verdict(tone: MarkTone, cropped?: boolean): string;
|
|
14
|
+
/** A card's heading: its text, and the provider's mark before it where there is one. */
|
|
15
|
+
export type Heading = string | {
|
|
16
|
+
text: string;
|
|
17
|
+
mark: string;
|
|
18
|
+
};
|
|
19
|
+
/** A provider's name with its mark, for a card's heading. */
|
|
20
|
+
export declare function marked(text: string, provider: string, method?: string): Heading;
|
|
21
|
+
/**
|
|
22
|
+
* One side of a link, with each thing known about it on its own line. A name, the
|
|
23
|
+
* identifier behind it and how it was shown are three separate claims, and a reader
|
|
24
|
+
* running them together in one sentence is the way to misread which was established.
|
|
25
|
+
*
|
|
26
|
+
* Both sides render as the same card, so the pair reads as a pair.
|
|
27
|
+
*/
|
|
28
|
+
export declare function card(heading: Heading, name: string, url: string | undefined, reference?: string | string[], ...extra: string[]): string;
|
|
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 externalCard(heading: Heading, external: ExternalAccount, ...extra: string[]): string;
|
|
34
|
+
export declare function side(heading: Heading, name: string, url: string | undefined, reference: string | string[] | undefined, since: string | undefined, extra: string[]): string;
|
|
35
|
+
/** A day, as the date alone. */
|
|
36
|
+
export declare const day: (time: number) => string;
|
|
37
|
+
/** A day as it is said, such as "Jan 11, 2027". In UTC, as every other time here is. */
|
|
38
|
+
export declare const date: (time: number) => string;
|
|
39
|
+
/** How far off a time is in whole days, such as "in 90 days" or "12 days ago". */
|
|
40
|
+
export declare function distance(time: number, now: number): string;
|
|
41
|
+
/** Seconds are the finest thing a record measured in days can mean; milliseconds are noise. */
|
|
42
|
+
export declare const moment: (time: number) => string;
|
|
43
|
+
/**
|
|
44
|
+
* Every time on the page in one list. A date inside a sentence is a date a reader has to
|
|
45
|
+
* find; the times of a record belong together, where they can be compared at a glance.
|
|
46
|
+
*/
|
|
47
|
+
export declare function times(rows: [string, number | undefined][]): string;
|
|
48
|
+
/** A URL as written if it is http(s), and nothing otherwise. */
|
|
49
|
+
export declare function safeUrl(value: string): string | undefined;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { externalId, externalIdGroups, externalLink, externalName, externalSince, } from '../core/index.js';
|
|
2
|
+
import { logo } from '../logo.js';
|
|
3
|
+
import { markOf, pathAttributes, tones, verification } from '../marks.js';
|
|
4
|
+
import { escape } from './escape.js';
|
|
5
|
+
import { styleVersion } from './style.js';
|
|
6
|
+
/**
|
|
7
|
+
* The shell every Verily page shares: the stylesheet, the logotype and the heading. `variant`
|
|
8
|
+
* is a class on the page's `main`, such as `single` for a page with one thing on it.
|
|
9
|
+
* `script` is the path of one script the page runs. `mark` is drawn before the heading.
|
|
10
|
+
*/
|
|
11
|
+
export const page = (prefix, title, body, variant = '', script = '', mark = '') => `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escape(title)} · Verily</title><link rel="stylesheet" href="${escape(prefix)}/style.css?v=${styleVersion}"><body><main${variant && ` class="${variant}"`}>${logo}<h1>${mark}${escape(title)}</h1>${body}</main>${script && `<script src="${escape(script)}" defer></script>`}</body></html>`;
|
|
12
|
+
/**
|
|
13
|
+
* The V the badge draws for a record, in the tone that says how the record stands.
|
|
14
|
+
* `cropped` fits the box to the strokes, for a V that has to meet the edges around it.
|
|
15
|
+
*/
|
|
16
|
+
export function verdict(tone, cropped = false) {
|
|
17
|
+
const [first, second] = tones[tone];
|
|
18
|
+
const stroke = 'stroke-width="32" stroke-linejoin="miter"';
|
|
19
|
+
const check = Object.entries(verification.check)
|
|
20
|
+
.map(([name, value]) => ` ${name}="${value}"`)
|
|
21
|
+
.join('');
|
|
22
|
+
return `<svg class="verdict ${tone}" viewBox="${cropped ? verification.strokes : verification.viewBox}" fill="none" aria-hidden="true" focusable="false"><path d="${verification.path}" ${stroke} stroke="${first}"/><path d="${verification.path}" ${stroke}${check} stroke="${second}"/></svg>`;
|
|
23
|
+
}
|
|
24
|
+
/** A provider's name with its mark, for a card's heading. */
|
|
25
|
+
export function marked(text, provider, method) {
|
|
26
|
+
const mark = markOf(provider, method);
|
|
27
|
+
if (!mark)
|
|
28
|
+
return text;
|
|
29
|
+
const attributes = pathAttributes(mark)
|
|
30
|
+
.map(([name, value]) => ` ${name}="${value}"`)
|
|
31
|
+
.join('');
|
|
32
|
+
return {
|
|
33
|
+
text,
|
|
34
|
+
mark: `<svg class="provider" viewBox="${mark.viewBox}" fill="${mark.stroke ? 'none' : 'currentColor'}" aria-hidden="true" focusable="false">${mark.paths
|
|
35
|
+
.map((d) => `<path d="${escape(d)}"${attributes}/>`)
|
|
36
|
+
.join('')}</svg>`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One side of a link, with each thing known about it on its own line. A name, the
|
|
41
|
+
* identifier behind it and how it was shown are three separate claims, and a reader
|
|
42
|
+
* running them together in one sentence is the way to misread which was established.
|
|
43
|
+
*
|
|
44
|
+
* Both sides render as the same card, so the pair reads as a pair.
|
|
45
|
+
*/
|
|
46
|
+
export function card(heading, name, url, reference, ...extra) {
|
|
47
|
+
return side(heading, name, url, reference, undefined, extra);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The card for an external account: its name, link and id, with the "Joined <date>" text
|
|
51
|
+
* on the name's line when the creation date is known.
|
|
52
|
+
*/
|
|
53
|
+
export function externalCard(heading, external, ...extra) {
|
|
54
|
+
return side(heading, externalName(external), externalLink(external),
|
|
55
|
+
// A mailbox's address or a domain is its name already, so there is no second identifier.
|
|
56
|
+
externalIdGroups(external) ?? externalId(external), externalSince(external, day), extra);
|
|
57
|
+
}
|
|
58
|
+
export function side(heading, name, url, reference, since, extra) {
|
|
59
|
+
const profile = url && safeUrl(url);
|
|
60
|
+
return `<div class="side"><p class="who">${typeof heading === 'string' ? escape(heading) : `${heading.mark}${escape(heading.text)}`}</p><p class="name">${profile ? `<a href="${escape(profile)}" rel="noreferrer">${escape(name)}</a>` : escape(name)}${since ? `<span class="since">${escape(since)}</span>` : ''}</p>${typeof reference === 'string'
|
|
61
|
+
? `<p class="reference">${escape(reference)}</p>`
|
|
62
|
+
: reference
|
|
63
|
+
? `<p class="reference fingerprint">${reference.map((group) => `<span>${escape(group)}</span>`).join('')}</p>`
|
|
64
|
+
: ''}${extra.join('')}</div>`;
|
|
65
|
+
}
|
|
66
|
+
/** A day, as the date alone. */
|
|
67
|
+
export const day = (time) => new Date(time).toISOString().slice(0, 10);
|
|
68
|
+
/** A day as it is said, such as "Jan 11, 2027". In UTC, as every other time here is. */
|
|
69
|
+
export const date = (time) => new Date(time).toLocaleDateString('en-US', {
|
|
70
|
+
year: 'numeric',
|
|
71
|
+
month: 'short',
|
|
72
|
+
day: 'numeric',
|
|
73
|
+
timeZone: 'UTC',
|
|
74
|
+
});
|
|
75
|
+
/** How far off a time is in whole days, such as "in 90 days" or "12 days ago". */
|
|
76
|
+
export function distance(time, now) {
|
|
77
|
+
const days = Math.round((time - now) / 86400000);
|
|
78
|
+
if (days === 0)
|
|
79
|
+
return 'today';
|
|
80
|
+
const count = `${Math.abs(days)} day${Math.abs(days) === 1 ? '' : 's'}`;
|
|
81
|
+
return days > 0 ? `in ${count}` : `${count} ago`;
|
|
82
|
+
}
|
|
83
|
+
/** Seconds are the finest thing a record measured in days can mean; milliseconds are noise. */
|
|
84
|
+
export const moment = (time) => new Date(time).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
85
|
+
/**
|
|
86
|
+
* Every time on the page in one list. A date inside a sentence is a date a reader has to
|
|
87
|
+
* find; the times of a record belong together, where they can be compared at a glance.
|
|
88
|
+
*/
|
|
89
|
+
export function times(rows) {
|
|
90
|
+
return `<dl>${rows
|
|
91
|
+
.filter(([, time]) => time !== undefined)
|
|
92
|
+
.map(([label, time]) => `<dt>${escape(label)}</dt><dd>${escape(moment(time))}</dd>`)
|
|
93
|
+
.join('')}</dl>`;
|
|
94
|
+
}
|
|
95
|
+
/** A URL as written if it is http(s), and nothing otherwise. */
|
|
96
|
+
export function safeUrl(value) {
|
|
97
|
+
try {
|
|
98
|
+
const url = new URL(value);
|
|
99
|
+
return ['https:', 'http:'].includes(url.protocol) ? url.href : undefined;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
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/*\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/* 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 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 --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
|