@bkazemi/verily 0.9.5 → 0.10.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 +7 -5
- package/dist/browser/evidence-dialog.js +9 -5
- package/dist/browser/index.d.ts +7 -1
- package/dist/browser/index.js +37 -8
- package/dist/core/index.d.ts +24 -0
- package/dist/core/index.js +40 -0
- package/dist/instance/bin.js +31 -1
- package/dist/instance/github-app.d.ts +26 -0
- package/dist/instance/github-app.js +94 -0
- package/dist/instance/index.js +4 -2
- package/dist/instance/starter.js +1 -1
- package/dist/server/index.js +4 -2
- package/dist/server/service.d.ts +2 -0
- package/dist/server/service.js +23 -2
- package/dist/server/style.css.d.ts +1 -1
- package/dist/server/style.css.js +1 -1
- package/dist/server/style.d.ts +1 -1
- package/dist/site/index.d.ts +6 -3
- package/dist/site/index.js +6 -3
- package/dist/verily.js +64 -14
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ const verily = createVerily({
|
|
|
118
118
|
|
|
119
119
|
Sign-in methods need an app registered with the provider. The callback URL is always `<baseUrl>/callback`, and every place you run Verily, development included, needs its own registered.
|
|
120
120
|
|
|
121
|
-
- **GitHub:** [register an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
|
121
|
+
- **GitHub:** run `npx verily github-app <callback>...`, open the address it prints in a browser signed in to GitHub, and click **Create GitHub App**. It prints the client id and secret. One app can list up to 10 callback URLs, so development and production can share it. Add `--org <name>` for an organization to own the app, or `--name <name>` if the default name is taken. You can instead [register an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) by hand, which has only one callback URL.
|
|
122
122
|
- **Discord:** create an application in the [Discord developer portal](https://discord.com/developers/applications) and add the callback URL under **OAuth2 → Redirects**. One application can list several. Discord shows the client secret only when you reset it.
|
|
123
123
|
- **YouTube:** in the [Google Cloud console](https://console.cloud.google.com/), enable the YouTube Data API v3, create an OAuth client of type **Web application**, and add the callback URL as an authorized redirect URI. One client can list several. The `youtube.readonly` scope is sensitive: until Google verifies your app, users see a warning and only 100 accounts can ever use it, so apply for verification before offering it publicly.
|
|
124
124
|
|
|
@@ -168,7 +168,7 @@ import { init } from '@bkazemi/verily';
|
|
|
168
168
|
const client = init({ backendUrl: '/api/verily' });
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
Without one, serve `node_modules/@bkazemi/verily/dist/verily.js` yourself, or load it from a CDN such as `https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.
|
|
171
|
+
Without one, serve `node_modules/@bkazemi/verily/dist/verily.js` yourself, or load it from a CDN such as `https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.10.0/dist/verily.js`. The script defines a global `Verily`.
|
|
172
172
|
|
|
173
173
|
Then place the badge wherever the account appears:
|
|
174
174
|
|
|
@@ -217,6 +217,8 @@ It lists up to four verified accounts, then counts the rest, such as `+2 more`.
|
|
|
217
217
|
|
|
218
218
|
With several accounts, each card shows the name, status and verifier, with the method and dates behind a Details toggle. Add `details-open`, or pass `{ detailsOpen: true }`, to start every card open.
|
|
219
219
|
|
|
220
|
+
A sign-in counts until its record expires, a year by default. To ask for more recent proof, add `max-age`, such as `max-age="30d"`, or pass `{ maxAgeMs }`. A record whose account was last proved before then reads as Unconfirmed until its holder renews it. A bare number is seconds, and `s`, `m`, `h` and `d` set the unit. A published proof is reread on a schedule, so `max-age` mostly affects sign-ins and mailed codes. Each card shows when its account was last proved. For a published proof that is Last checked. For a sign-in or code it is Approved, or Authenticated where a renewal by another method moved the approval later.
|
|
221
|
+
|
|
220
222
|
The badge refreshes every 30 seconds and reads only public evidence. The client can also start and end connections, and draw a badge into your own element:
|
|
221
223
|
|
|
222
224
|
```js
|
|
@@ -370,7 +372,7 @@ A static site only needs the script and the badge markup; the backend runs somew
|
|
|
370
372
|
|
|
371
373
|
```html
|
|
372
374
|
<script
|
|
373
|
-
src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.
|
|
375
|
+
src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.10.0/dist/verily.js"
|
|
374
376
|
defer
|
|
375
377
|
></script>
|
|
376
378
|
<verily-badge
|
|
@@ -460,7 +462,7 @@ const { [user.id]: connections } = await verily.connections([user.id]);
|
|
|
460
462
|
const verified = connections.filter((c) => c.status === 'verified');
|
|
461
463
|
```
|
|
462
464
|
|
|
463
|
-
Show only `verified` records as verified. A `retired` record carries `retiredAt` and can be shown as history, never as a current account. `mark` is `preferred` or `unused` where the user set one; it is their ordering, not something checked.
|
|
465
|
+
Show only `verified` records as verified. Pass `{ maxAgeMs }` to read a record whose account was last proved longer ago than that as `unconfirmed`. A `retired` record carries `retiredAt` and can be shown as history, never as a current account. `mark` is `preferred` or `unused` where the user set one; it is their ordering, not something checked.
|
|
464
466
|
|
|
465
467
|
To show them, hand the records to the badge. It draws them as given and fetches nothing, so it works for unlisted links, which a browser cannot read for itself:
|
|
466
468
|
|
|
@@ -598,7 +600,7 @@ An instance sets `dns: true`.
|
|
|
598
600
|
|
|
599
601
|
## Operations
|
|
600
602
|
|
|
601
|
-
Verification lasts
|
|
603
|
+
Verification lasts a year, flows 10 minutes, and sharing links 7 days. Change them with `validityMs`, `flowTtlMs` and `shareTtlMs`. Evidence is never cached. A published proof that `recheck()` still finds extends its record once half the term is gone, so only a sign-in or a mailed code needs renewing. Raising `validityMs` lengthens records still running on the next `recheck()`; lowering it shortens nothing.
|
|
602
604
|
|
|
603
605
|
A user's DNS setup is saved for seven days. If propagation outlasts a flow's ten minutes, they choose DNS again and get the same TXT value and the last domain entered. Each return reads the proof afresh. The TXT record has to stay after verification, for the ongoing checks.
|
|
604
606
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { attestationLabel, externalId, externalIdGroups, externalLink, externalName, externalSince, lastProved, localSide, proofTitle, statusLabel, } from '../core/index.js';
|
|
1
|
+
import { attestationLabel, externalId, externalIdGroups, externalLink, externalName, externalSince, authenticatedEarlier, lastProved, localSide, proofTitle, statusLabel, } from '../core/index.js';
|
|
2
2
|
import { markStyles, verificationMark } from './mark.js';
|
|
3
3
|
import dialogCss from './evidence-dialog.css.js';
|
|
4
4
|
import { providerMark } from './provider-mark.js';
|
|
@@ -561,11 +561,15 @@ function externalCard(evidence, manage, later = () => { }, fold, warned = () =>
|
|
|
561
561
|
evidence.expiresAt,
|
|
562
562
|
]);
|
|
563
563
|
}
|
|
564
|
-
// When
|
|
565
|
-
//
|
|
564
|
+
// When the account was last proved: a published proof when it was last read, and a
|
|
565
|
+
// sign-in or mailed code when it happened, where that is not already the approval date.
|
|
566
566
|
const main = evidence.attestations.external[0];
|
|
567
|
-
if (
|
|
568
|
-
|
|
567
|
+
if (!retired) {
|
|
568
|
+
if (main.artifactUrl)
|
|
569
|
+
dateRows.push(['Last checked', main.confirmedAt]);
|
|
570
|
+
else if (authenticatedEarlier(evidence))
|
|
571
|
+
dateRows.push(['Authenticated', evidence.authenticatedAt]);
|
|
572
|
+
}
|
|
569
573
|
for (const [label, time] of dateRows) {
|
|
570
574
|
dates.append(node('dt', label), node('dd', moment(time)));
|
|
571
575
|
}
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -20,6 +20,11 @@ export interface Arrangement {
|
|
|
20
20
|
* open or shut each either way.
|
|
21
21
|
*/
|
|
22
22
|
detailsOpen?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* How recently an account must have been proved to count as verified, in milliseconds.
|
|
25
|
+
* An older record reads as unconfirmed. Unset, a record counts until it expires.
|
|
26
|
+
*/
|
|
27
|
+
maxAgeMs?: number;
|
|
23
28
|
}
|
|
24
29
|
/**
|
|
25
30
|
* Presents records the page already holds, without asking any backend for them. This is
|
|
@@ -51,9 +56,10 @@ export declare function init({ backendUrl, handoffUrl }: {
|
|
|
51
56
|
/** The site's current public connections, so an embed need not hardcode ids. */
|
|
52
57
|
listPublished(): Promise<Evidence[]>;
|
|
53
58
|
getConnection(id: string): Promise<Evidence>;
|
|
54
|
-
mountBadge(element: HTMLElement, { connectionId, evidence }: {
|
|
59
|
+
mountBadge(element: HTMLElement, { connectionId, evidence, maxAgeMs, }: {
|
|
55
60
|
connectionId: string;
|
|
56
61
|
evidence?: Evidence;
|
|
62
|
+
maxAgeMs?: number;
|
|
57
63
|
}): Promise<void>;
|
|
58
64
|
/**
|
|
59
65
|
* Several of one subject's public connections as a single pill. One that cannot be
|
package/dist/browser/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { accountsOf } from '../core/index.js';
|
|
1
|
+
import { accountsOf, parseMaxAge, withMaxAge } from '../core/index.js';
|
|
2
2
|
import { routePath } from '../core/routes.js';
|
|
3
3
|
import { badgeShown, quietBadge, renderBadge, renderBadgeMessage, renderBadgePending, renderConnectPill, } from './badge.js';
|
|
4
4
|
import { openConnectDialog, } from './connect-dialog.js';
|
|
@@ -92,7 +92,7 @@ function details(element) {
|
|
|
92
92
|
throw error;
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
}, latestGroup.get(element), accounts, managers.get(element), openHosts.has(element));
|
|
95
|
+
}, latestGroup.get(element), (records) => accounts(aged(element, records)), managers.get(element), openHosts.has(element));
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
@@ -103,6 +103,24 @@ function details(element) {
|
|
|
103
103
|
const stackedHosts = new WeakSet();
|
|
104
104
|
const quietHosts = new WeakSet();
|
|
105
105
|
const openHosts = new WeakSet();
|
|
106
|
+
/** The max age each host was given, where the page set one. */
|
|
107
|
+
const maxAges = new WeakMap();
|
|
108
|
+
/** Sets or clears a host's max age. */
|
|
109
|
+
function bound(element, maxAgeMs) {
|
|
110
|
+
if (maxAgeMs === undefined)
|
|
111
|
+
maxAges.delete(element);
|
|
112
|
+
else
|
|
113
|
+
maxAges.set(element, maxAgeMs);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The records as this host's max age has them read. Call it on records already resolved
|
|
117
|
+
* by `standing()`, since a signed record can carry different proof dates.
|
|
118
|
+
*/
|
|
119
|
+
function aged(element, records) {
|
|
120
|
+
const maxAgeMs = maxAges.get(element);
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
return records.map((e) => withMaxAge(e, maxAgeMs, now));
|
|
123
|
+
}
|
|
106
124
|
/**
|
|
107
125
|
* How many accounts a pill names before it counts the rest. A stacked pill pays for each
|
|
108
126
|
* row in the page's height. A panel floats over the page and pays nothing, but it cannot
|
|
@@ -111,7 +129,8 @@ const openHosts = new WeakSet();
|
|
|
111
129
|
const stackedAccounts = 4;
|
|
112
130
|
const peekedAccounts = 8;
|
|
113
131
|
/** Records how a host's pill is arranged, from what the page last said. */
|
|
114
|
-
function arrange(element, { stacked, peek, detailsOpen }) {
|
|
132
|
+
function arrange(element, { stacked, peek, detailsOpen, maxAgeMs }) {
|
|
133
|
+
bound(element, maxAgeMs);
|
|
115
134
|
if (detailsOpen)
|
|
116
135
|
openHosts.add(element);
|
|
117
136
|
else
|
|
@@ -164,7 +183,7 @@ function accounts(records) {
|
|
|
164
183
|
* `load` gives the records afresh when the dialog opens and as it stays open.
|
|
165
184
|
*/
|
|
166
185
|
function presentGroup(element, records, load) {
|
|
167
|
-
const ordered = accounts(records.map(standing));
|
|
186
|
+
const ordered = accounts(aged(element, records.map(standing)));
|
|
168
187
|
const verified = ordered.filter(live);
|
|
169
188
|
const lead = verified[0] ?? ordered[0];
|
|
170
189
|
latestGroup.set(element, records);
|
|
@@ -448,7 +467,8 @@ export function init({ backendUrl, handoffUrl }) {
|
|
|
448
467
|
throw new Error('Invalid evidence response');
|
|
449
468
|
return data;
|
|
450
469
|
},
|
|
451
|
-
async mountBadge(element, { connectionId, evidence }) {
|
|
470
|
+
async mountBadge(element, { connectionId, evidence, maxAgeMs, }) {
|
|
471
|
+
bound(element, maxAgeMs);
|
|
452
472
|
// Evidence already in hand renders at once. Otherwise a host with nothing to show
|
|
453
473
|
// gets the waiting pill, whose frame and mark the finished badge keeps; a host that
|
|
454
474
|
// already shows a badge keeps it up until the check answers, so a periodic refresh
|
|
@@ -477,7 +497,7 @@ export function init({ backendUrl, handoffUrl }) {
|
|
|
477
497
|
return [fresh];
|
|
478
498
|
});
|
|
479
499
|
const draw = () => {
|
|
480
|
-
const badge = renderBadge(element, standing(e));
|
|
500
|
+
const badge = renderBadge(element, aged(element, [standing(e)])[0]);
|
|
481
501
|
// Null means the pill on screen is unchanged. Its handler stands, and reads the above.
|
|
482
502
|
if (badge)
|
|
483
503
|
opens(element, badge);
|
|
@@ -503,7 +523,10 @@ export function init({ backendUrl, handoffUrl }) {
|
|
|
503
523
|
const ids = [...new Set(connectionIds)];
|
|
504
524
|
arrange(element, arrangement);
|
|
505
525
|
if (ids.length === 1)
|
|
506
|
-
return client.mountBadge(element, {
|
|
526
|
+
return client.mountBadge(element, {
|
|
527
|
+
connectionId: ids[0],
|
|
528
|
+
maxAgeMs: arrangement.maxAgeMs,
|
|
529
|
+
});
|
|
507
530
|
if (!badgeShown(element))
|
|
508
531
|
renderBadgePending(element);
|
|
509
532
|
const read = async () => {
|
|
@@ -713,6 +736,7 @@ if (typeof customElements !== 'undefined' && !customElements.get('verily-badge')
|
|
|
713
736
|
'stacked',
|
|
714
737
|
'peek',
|
|
715
738
|
'details-open',
|
|
739
|
+
'max-age',
|
|
716
740
|
];
|
|
717
741
|
handed;
|
|
718
742
|
/**
|
|
@@ -773,6 +797,7 @@ if (typeof customElements !== 'undefined' && !customElements.get('verily-badge')
|
|
|
773
797
|
stacked: this.getAttribute('stacked') !== null,
|
|
774
798
|
peek: this.getAttribute('peek') !== 'off',
|
|
775
799
|
detailsOpen: this.getAttribute('details-open') !== null,
|
|
800
|
+
maxAgeMs: parseMaxAge(this.getAttribute('max-age')),
|
|
776
801
|
};
|
|
777
802
|
// The holder's own badge, where the page says so: its dialog connects another
|
|
778
803
|
// account, renews one and removes one, through the backend named. The host hears
|
|
@@ -838,7 +863,11 @@ if (typeof customElements !== 'undefined' && !customElements.get('verily-badge')
|
|
|
838
863
|
return;
|
|
839
864
|
}
|
|
840
865
|
if (backendUrl)
|
|
841
|
-
void init({ backendUrl }).mountBadge(this, {
|
|
866
|
+
void init({ backendUrl }).mountBadge(this, {
|
|
867
|
+
connectionId,
|
|
868
|
+
evidence,
|
|
869
|
+
maxAgeMs: arrangement.maxAgeMs,
|
|
870
|
+
});
|
|
842
871
|
}
|
|
843
872
|
});
|
|
844
873
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -615,6 +615,30 @@ export declare function fresh(attestation: Attestation, now: number, freshness?:
|
|
|
615
615
|
* proved when it happened, and a published proof when it was last read.
|
|
616
616
|
*/
|
|
617
617
|
export declare function lastProved(evidence: Pick<Evidence, 'authenticatedAt' | 'attestations'>): number;
|
|
618
|
+
/**
|
|
619
|
+
* Whether a sign-in or mailed code happened more than a day before the record was last
|
|
620
|
+
* approved. That only happens after a renewal by another method. Otherwise the two dates
|
|
621
|
+
* match and a card shows only the approval.
|
|
622
|
+
*/
|
|
623
|
+
export declare function authenticatedEarlier(evidence: Pick<Evidence, 'authenticatedAt' | 'approvedAt'>): boolean;
|
|
624
|
+
/**
|
|
625
|
+
* Whether a record's account was last proved within `maxAgeMs` of `now`. A reader who
|
|
626
|
+
* wants recent proof sets its own bound: a sign-in counts once, a year ago or this morning,
|
|
627
|
+
* and only the reader knows how old is too old.
|
|
628
|
+
*/
|
|
629
|
+
export declare function recent(evidence: Pick<Evidence, 'authenticatedAt' | 'attestations'>, maxAgeMs: number, now: number): boolean;
|
|
630
|
+
/**
|
|
631
|
+
* The record as a reader with a `maxAgeMs` bound sees it. A verified record last proved
|
|
632
|
+
* before the bound reads as unconfirmed: not disproved, only not proved recently enough.
|
|
633
|
+
* With no bound the record is returned as is.
|
|
634
|
+
*/
|
|
635
|
+
export declare function withMaxAge<T extends Evidence>(evidence: T, maxAgeMs: number | undefined, now: number): T;
|
|
636
|
+
/**
|
|
637
|
+
* A max age written as a whole number with an optional unit: `s`, `m`, `h` or `d`. A bare
|
|
638
|
+
* number is seconds, as in HTTP and OpenID Connect. Returns milliseconds, or undefined
|
|
639
|
+
* for anything else.
|
|
640
|
+
*/
|
|
641
|
+
export declare function parseMaxAge(text: string | null | undefined): number | undefined;
|
|
618
642
|
/**
|
|
619
643
|
* The word for a record's state. Evidence can be read after it was issued, so an approval
|
|
620
644
|
* that has run out since reads as expired whatever status it was issued with.
|
package/dist/core/index.js
CHANGED
|
@@ -80,6 +80,46 @@ export function lastProved(evidence) {
|
|
|
80
80
|
const main = evidence.attestations.external[0];
|
|
81
81
|
return main.artifactUrl ? main.confirmedAt : evidence.authenticatedAt;
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Whether a sign-in or mailed code happened more than a day before the record was last
|
|
85
|
+
* approved. That only happens after a renewal by another method. Otherwise the two dates
|
|
86
|
+
* match and a card shows only the approval.
|
|
87
|
+
*/
|
|
88
|
+
export function authenticatedEarlier(evidence) {
|
|
89
|
+
return evidence.approvedAt - evidence.authenticatedAt > 86400000;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Whether a record's account was last proved within `maxAgeMs` of `now`. A reader who
|
|
93
|
+
* wants recent proof sets its own bound: a sign-in counts once, a year ago or this morning,
|
|
94
|
+
* and only the reader knows how old is too old.
|
|
95
|
+
*/
|
|
96
|
+
export function recent(evidence, maxAgeMs, now) {
|
|
97
|
+
return now - lastProved(evidence) <= maxAgeMs;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The record as a reader with a `maxAgeMs` bound sees it. A verified record last proved
|
|
101
|
+
* before the bound reads as unconfirmed: not disproved, only not proved recently enough.
|
|
102
|
+
* With no bound the record is returned as is.
|
|
103
|
+
*/
|
|
104
|
+
export function withMaxAge(evidence, maxAgeMs, now) {
|
|
105
|
+
return maxAgeMs !== undefined &&
|
|
106
|
+
evidence.status === 'verified' &&
|
|
107
|
+
!recent(evidence, maxAgeMs, now)
|
|
108
|
+
? { ...evidence, status: 'unconfirmed' }
|
|
109
|
+
: evidence;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A max age written as a whole number with an optional unit: `s`, `m`, `h` or `d`. A bare
|
|
113
|
+
* number is seconds, as in HTTP and OpenID Connect. Returns milliseconds, or undefined
|
|
114
|
+
* for anything else.
|
|
115
|
+
*/
|
|
116
|
+
export function parseMaxAge(text) {
|
|
117
|
+
const match = /^\s*(\d+)\s*([smhd]?)\s*$/.exec(text ?? '');
|
|
118
|
+
if (!match)
|
|
119
|
+
return undefined;
|
|
120
|
+
const unit = { '': 1000, s: 1000, m: 60000, h: 3600000, d: 86400000 }[match[2]];
|
|
121
|
+
return Number(match[1]) * unit;
|
|
122
|
+
}
|
|
83
123
|
/**
|
|
84
124
|
* The word for a record's state. Evidence can be read after it was issued, so an approval
|
|
85
125
|
* that has run out since reads as expired whatever status it was issued with.
|
package/dist/instance/bin.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
2
3
|
import { serve } from './serve.js';
|
|
3
4
|
import { init } from './init.js';
|
|
5
|
+
import { githubApp } from './github-app.js';
|
|
4
6
|
const usage = `Usage:
|
|
5
7
|
verily serve Run an instance on Node, from the environment.
|
|
6
|
-
verily init cloudflare [dir] Write a Cloudflare project into dir
|
|
8
|
+
verily init cloudflare [dir] Write a Cloudflare project into dir.
|
|
9
|
+
verily github-app <callback>... [--org <name>] [--name <name>]
|
|
10
|
+
Create a GitHub App for sign-in, up to 10 callbacks.`;
|
|
7
11
|
/** Writes the starter, then says what to do with it. */
|
|
8
12
|
async function start(directory) {
|
|
9
13
|
await init(directory);
|
|
@@ -15,6 +19,29 @@ Next, in that directory:
|
|
|
15
19
|
3. npx wrangler secret put OWNER_KEY
|
|
16
20
|
4. npm run deploy`);
|
|
17
21
|
}
|
|
22
|
+
/** Runs the manifest flow, then prints the app's credentials. */
|
|
23
|
+
async function createGithubApp(args) {
|
|
24
|
+
const { values, positionals } = parseArgs({
|
|
25
|
+
args,
|
|
26
|
+
allowPositionals: true,
|
|
27
|
+
options: { org: { type: 'string' }, name: { type: 'string' } },
|
|
28
|
+
});
|
|
29
|
+
const app = await githubApp({
|
|
30
|
+
callbacks: positionals,
|
|
31
|
+
org: values.org,
|
|
32
|
+
name: values.name,
|
|
33
|
+
onReady: (url) => console.log(`Open ${url} in a browser signed in to GitHub.`),
|
|
34
|
+
});
|
|
35
|
+
console.log(`Created ${app.htmlUrl}
|
|
36
|
+
|
|
37
|
+
GITHUB_CLIENT_ID=${app.clientId}
|
|
38
|
+
GITHUB_CLIENT_SECRET=${app.clientSecret}
|
|
39
|
+
|
|
40
|
+
Keep the secret on the server. GitHub shows it only this once.
|
|
41
|
+
|
|
42
|
+
To show a logo when people sign in, upload one under Display information at
|
|
43
|
+
${app.htmlUrl}`);
|
|
44
|
+
}
|
|
18
45
|
const [command, target, directory] = process.argv.slice(2);
|
|
19
46
|
try {
|
|
20
47
|
if (command === 'serve') {
|
|
@@ -25,6 +52,9 @@ try {
|
|
|
25
52
|
else if (command === 'init' && target === 'cloudflare') {
|
|
26
53
|
await start(directory ?? '.');
|
|
27
54
|
}
|
|
55
|
+
else if (command === 'github-app') {
|
|
56
|
+
await createGithubApp(process.argv.slice(3));
|
|
57
|
+
}
|
|
28
58
|
else {
|
|
29
59
|
console.error(usage);
|
|
30
60
|
process.exit(command === undefined || command === '--help' || command === '-h' ? 0 : 2);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** What `githubApp()` gets back from GitHub once the app is created. */
|
|
2
|
+
export interface GithubAppCredentials {
|
|
3
|
+
clientId: string;
|
|
4
|
+
clientSecret: string;
|
|
5
|
+
/** The app's settings page on GitHub. */
|
|
6
|
+
htmlUrl: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Registers a GitHub App for sign-in through GitHub's manifest flow. A local server serves a
|
|
10
|
+
* form that posts the manifest to GitHub, the holder clicks Create there, and GitHub
|
|
11
|
+
* redirects back with a code that is exchanged for the app's client id and secret.
|
|
12
|
+
*
|
|
13
|
+
* The app is public, so any GitHub account can sign in with it, and asks for no
|
|
14
|
+
* permissions and no webhook.
|
|
15
|
+
*/
|
|
16
|
+
export declare function githubApp(options: {
|
|
17
|
+
/** The callback URLs to register, at most 10. */
|
|
18
|
+
callbacks: string[];
|
|
19
|
+
/** The organization to own the app. Without one, the signed-in GitHub account owns it. */
|
|
20
|
+
org?: string;
|
|
21
|
+
/** The app's name, unique across GitHub. Defaults to one made from the first callback. */
|
|
22
|
+
name?: string;
|
|
23
|
+
fetch?: typeof fetch;
|
|
24
|
+
/** Called with the local address to open in a browser. */
|
|
25
|
+
onReady: (url: string) => void;
|
|
26
|
+
}): Promise<GithubAppCredentials>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { escape } from '../server/escape.js';
|
|
4
|
+
/**
|
|
5
|
+
* Registers a GitHub App for sign-in through GitHub's manifest flow. A local server serves a
|
|
6
|
+
* form that posts the manifest to GitHub, the holder clicks Create there, and GitHub
|
|
7
|
+
* redirects back with a code that is exchanged for the app's client id and secret.
|
|
8
|
+
*
|
|
9
|
+
* The app is public, so any GitHub account can sign in with it, and asks for no
|
|
10
|
+
* permissions and no webhook.
|
|
11
|
+
*/
|
|
12
|
+
export async function githubApp(options) {
|
|
13
|
+
const { callbacks, org } = options;
|
|
14
|
+
const request = options.fetch ?? fetch;
|
|
15
|
+
if (callbacks.length === 0)
|
|
16
|
+
throw new Error('Name at least one callback URL');
|
|
17
|
+
if (callbacks.length > 10)
|
|
18
|
+
throw new Error('GitHub allows at most 10 callback URLs');
|
|
19
|
+
for (const callback of callbacks)
|
|
20
|
+
if (!URL.canParse(callback) || !/^https?:$/.test(new URL(callback).protocol))
|
|
21
|
+
throw new Error(`${callback} is not an http or https URL`);
|
|
22
|
+
if (org !== undefined && !/^[a-z\d](?:[a-z\d-]*[a-z\d])?$/i.test(org))
|
|
23
|
+
throw new Error(`${org} is not a GitHub organization name`);
|
|
24
|
+
// GitHub caps app names at 34 characters.
|
|
25
|
+
const name = options.name ?? `Verily ${new URL(callbacks[0]).hostname}`.slice(0, 34);
|
|
26
|
+
const state = randomBytes(16).toString('base64url');
|
|
27
|
+
const server = createServer();
|
|
28
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
29
|
+
const local = `http://127.0.0.1:${server.address().port}`;
|
|
30
|
+
const manifest = {
|
|
31
|
+
name,
|
|
32
|
+
description: `Sign-in for Verily on ${new URL(callbacks[0]).hostname}.`,
|
|
33
|
+
url: new URL(callbacks[0]).origin,
|
|
34
|
+
redirect_url: `${local}/done`,
|
|
35
|
+
callback_urls: callbacks,
|
|
36
|
+
public: true,
|
|
37
|
+
hook_attributes: { url: new URL(callbacks[0]).origin, active: false },
|
|
38
|
+
default_permissions: {},
|
|
39
|
+
};
|
|
40
|
+
const target = new URL(org
|
|
41
|
+
? `https://github.com/organizations/${org}/settings/apps/new`
|
|
42
|
+
: 'https://github.com/settings/apps/new');
|
|
43
|
+
target.searchParams.set('state', state);
|
|
44
|
+
const credentials = new Promise((resolve, reject) => {
|
|
45
|
+
server.on('request', (req, res) => {
|
|
46
|
+
const url = new URL(req.url ?? '/', local);
|
|
47
|
+
const reply = (status, title, text, body = '') => {
|
|
48
|
+
res.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
49
|
+
res.end(`<!doctype html><meta charset="utf-8"><title>${escape(title)}</title><p>${escape(text)}</p>${body}`);
|
|
50
|
+
};
|
|
51
|
+
if (url.pathname === '/') {
|
|
52
|
+
// The form submits itself, with a button for when scripts are off.
|
|
53
|
+
return reply(200, 'Create the GitHub App', 'Sending you to GitHub to create the app.', `<form method="post" action="${escape(target.href)}">` +
|
|
54
|
+
`<input type="hidden" name="manifest" value="${escape(JSON.stringify(manifest))}">` +
|
|
55
|
+
'<button>Continue to GitHub</button></form>' +
|
|
56
|
+
'<script>document.forms[0].submit()</script>');
|
|
57
|
+
}
|
|
58
|
+
if (url.pathname !== '/done')
|
|
59
|
+
return reply(404, 'Not found', 'Not found.');
|
|
60
|
+
const code = url.searchParams.get('code');
|
|
61
|
+
// A request without the state this run made did not come from GitHub's redirect.
|
|
62
|
+
if (url.searchParams.get('state') !== state || !code)
|
|
63
|
+
return reply(400, 'Wrong link', 'This link is not from this run. Nothing was created.');
|
|
64
|
+
void convert(code).then((result) => {
|
|
65
|
+
reply(200, 'GitHub App created', 'The app is created. Go back to the terminal.');
|
|
66
|
+
resolve(result);
|
|
67
|
+
}, (error) => {
|
|
68
|
+
reply(502, 'Failed', 'GitHub did not return the app. See the terminal.');
|
|
69
|
+
reject(error);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
async function convert(code) {
|
|
74
|
+
const response = await request(`https://api.github.com/app-manifests/${encodeURIComponent(code)}/conversions`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'Verily-V0' },
|
|
77
|
+
signal: AbortSignal.timeout(15000),
|
|
78
|
+
});
|
|
79
|
+
const app = await response.json().catch(() => undefined);
|
|
80
|
+
if (!response.ok ||
|
|
81
|
+
!isRecord(app) ||
|
|
82
|
+
typeof app.client_id !== 'string' ||
|
|
83
|
+
typeof app.client_secret !== 'string' ||
|
|
84
|
+
typeof app.html_url !== 'string')
|
|
85
|
+
throw new Error(`GitHub did not return the app (HTTP ${response.status})`);
|
|
86
|
+
return { clientId: app.client_id, clientSecret: app.client_secret, htmlUrl: app.html_url };
|
|
87
|
+
}
|
|
88
|
+
options.onReady(`${local}/`);
|
|
89
|
+
// Closing waits for the last reply to be sent, then drops idle connections.
|
|
90
|
+
return await credentials.finally(() => server.close());
|
|
91
|
+
}
|
|
92
|
+
function isRecord(value) {
|
|
93
|
+
return typeof value === 'object' && value !== null;
|
|
94
|
+
}
|
package/dist/instance/index.js
CHANGED
|
@@ -907,8 +907,10 @@ class Server {
|
|
|
907
907
|
html `<p class="evidence"><a href="${e.evidenceUrl}">Inspect evidence</a></p>`, html `<dl>${rows.map(([label, value]) => html `<dt>${label}</dt><dd>${value}</dd>`)}</dl>`, shown && html `<p class="fine">${shown}</p>`, !site &&
|
|
908
908
|
e.visibility === 'public' &&
|
|
909
909
|
html `
|
|
910
|
-
<
|
|
911
|
-
|
|
910
|
+
<details class="embed">
|
|
911
|
+
<summary class="fine">Embed on your site</summary>
|
|
912
|
+
<pre><code>${highlightHtml(embed)}</code></pre>
|
|
913
|
+
</details>
|
|
912
914
|
`, html `
|
|
913
915
|
${(goes.length > 0 || listing) && html `<div class="actions">${goes}${listing}</div>`}
|
|
914
916
|
${ending}
|
package/dist/instance/starter.js
CHANGED
|
@@ -20,7 +20,7 @@ const wrangler = `// Your instance's Cloudflare settings. Fill in account_id and
|
|
|
20
20
|
"account_id": "YOUR_CLOUDFLARE_ACCOUNT_ID",
|
|
21
21
|
"main": "worker.ts",
|
|
22
22
|
"compatibility_date": "2026-07-01",
|
|
23
|
-
"compatibility_flags": ["nodejs_compat"],
|
|
23
|
+
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
|
24
24
|
// The library loads undici only on Node, so the Worker build gets a stand-in for it.
|
|
25
25
|
"alias": { "undici": "@bkazemi/verily/instance/undici" },
|
|
26
26
|
|
package/dist/server/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { attestationLabel, externalName, isArtifactProvider, isCodeProvider, isRedirectProvider, lastProved, accountsOf, localSide, proofTitle, providerMethod, statusLabel, signedBy, } from '../core/index.js';
|
|
1
|
+
import { attestationLabel, externalName, isArtifactProvider, isCodeProvider, isRedirectProvider, authenticatedEarlier, 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 { html } from './html.js';
|
|
@@ -400,7 +400,9 @@ function accountCard(e, external, now) {
|
|
|
400
400
|
: [
|
|
401
401
|
['Approved', e.approvedAt],
|
|
402
402
|
[e.expiresAt > now ? 'Valid until' : 'Expired on', e.expiresAt],
|
|
403
|
-
|
|
403
|
+
external[0].artifactUrl
|
|
404
|
+
? ['Last checked', external[0].confirmedAt]
|
|
405
|
+
: ['Authenticated', authenticatedEarlier(e) ? e.authenticatedAt : undefined],
|
|
404
406
|
])}
|
|
405
407
|
</div>
|
|
406
408
|
`,
|
package/dist/server/service.d.ts
CHANGED
|
@@ -142,6 +142,8 @@ export declare class VerilyService {
|
|
|
142
142
|
/** What a signed record says, if this instance signed it. */
|
|
143
143
|
checked(signed: unknown): Promise<SignedDocument | undefined>;
|
|
144
144
|
private get sendLimits();
|
|
145
|
+
/** How long an approval stands. A published proof that is still up extends it. */
|
|
146
|
+
private get validity();
|
|
145
147
|
/** A proof is only as fresh as the schedule that reads it, so both live together. */
|
|
146
148
|
private get freshness();
|
|
147
149
|
validateLocal(local: LocalAccount): LocalAccount;
|
package/dist/server/service.js
CHANGED
|
@@ -173,6 +173,10 @@ export class VerilyService {
|
|
|
173
173
|
address: this.options.sendLimits?.address ?? 5,
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
|
+
/** How long an approval stands. A published proof that is still up extends it. */
|
|
177
|
+
get validity() {
|
|
178
|
+
return this.options.validityMs ?? 365 * 86400000;
|
|
179
|
+
}
|
|
176
180
|
/** A proof is only as fresh as the schedule that reads it, so both live together. */
|
|
177
181
|
get freshness() {
|
|
178
182
|
return this.options.freshnessMs ?? freshnessMs;
|
|
@@ -925,7 +929,7 @@ export class VerilyService {
|
|
|
925
929
|
authenticatedAt: flow.authenticatedAt,
|
|
926
930
|
approvedAt: this.now(),
|
|
927
931
|
connectedAt: this.now(),
|
|
928
|
-
expiresAt: this.now() +
|
|
932
|
+
expiresAt: this.now() + this.validity,
|
|
929
933
|
attestations: {
|
|
930
934
|
local: this.declared(flow),
|
|
931
935
|
external: [this.attestation(flow, provider, connectionId)],
|
|
@@ -1229,7 +1233,7 @@ export class VerilyService {
|
|
|
1229
1233
|
const created = createdAt === undefined ? {} : { createdAt };
|
|
1230
1234
|
connection.local = flow.local;
|
|
1231
1235
|
connection.approvedAt = this.now();
|
|
1232
|
-
connection.expiresAt = this.now() +
|
|
1236
|
+
connection.expiresAt = this.now() + this.validity;
|
|
1233
1237
|
connection.revocationReason = undefined;
|
|
1234
1238
|
if (attestation.method === main.method) {
|
|
1235
1239
|
connection.authenticatedAt = flow.authenticatedAt;
|
|
@@ -1537,6 +1541,19 @@ export class VerilyService {
|
|
|
1537
1541
|
const interval = this.options.recheckMs ?? 86400000;
|
|
1538
1542
|
const now = this.now();
|
|
1539
1543
|
const spent = new Map();
|
|
1544
|
+
// Validity counts from approval, so a longer term set since applies to running records.
|
|
1545
|
+
await this.transaction(async (tx) => {
|
|
1546
|
+
for (const connection of await tx.list('connections')) {
|
|
1547
|
+
const term = connection.approvedAt + this.validity;
|
|
1548
|
+
if (connection.revokedAt === undefined &&
|
|
1549
|
+
connection.retiredAt === undefined &&
|
|
1550
|
+
connection.expiresAt > now &&
|
|
1551
|
+
connection.expiresAt < term) {
|
|
1552
|
+
connection.expiresAt = term;
|
|
1553
|
+
await tx.put('connections', connection.id, connection);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1540
1557
|
// When a proof was last read, or last tried. One whose last read did not confirm it,
|
|
1541
1558
|
// because the read failed or the proof was missing, is tried again four times as soon.
|
|
1542
1559
|
const tried = (a) => a.readAt ?? a.confirmedAt;
|
|
@@ -1718,6 +1735,10 @@ export class VerilyService {
|
|
|
1718
1735
|
}
|
|
1719
1736
|
held.confirmedAt = started;
|
|
1720
1737
|
delete held.absentAt;
|
|
1738
|
+
// A proof still up keeps its record running. It is moved only once half the term is
|
|
1739
|
+
// gone, so a daily read does not report a new expiry every time.
|
|
1740
|
+
if (current.expiresAt > started && current.expiresAt - started < this.validity / 2)
|
|
1741
|
+
current.expiresAt = started + this.validity;
|
|
1721
1742
|
// A zone can start or stop being signed, so the flag is as the last read found it.
|
|
1722
1743
|
if (dnssec)
|
|
1723
1744
|
held.dnssec = true;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: ":root {\n --ink: #23312b;\n --muted: #6b786f;\n --line: #dce2de;\n --surface: #fff;\n --raised: #f7f9f7;\n --accent: #245f43;\n --warn: #a4442b;\n --caution: #b07a12;\n --syntax-tag: #245f43;\n --syntax-attr: #8a5a0b;\n --syntax-string: #2b5f8a;\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 --caution: #d9a843;\n --syntax-tag: #7fc4a2;\n --syntax-attr: #d9a843;\n --syntax-string: #8fbbe0;\n }\n}\n\n* {\n box-sizing: border-box;\n}\n\nbody {\n margin: 0;\n padding: 0 1rem 4rem;\n background: var(--surface);\n color: var(--ink);\n font:\n 16px/1.65 system-ui,\n -apple-system,\n 'Segoe UI',\n 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: -0.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 {\n margin-bottom: 1.25rem;\n padding-bottom: 0;\n border-bottom: none;\n}\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) {\n display: flex;\n min-height: 100vh;\n min-height: 100dvh;\n}\nmain.single {\n width: 100%;\n max-width: 24rem;\n margin: auto;\n padding-bottom: 3rem;\n}\n\nh2 {\n margin: 2rem 0 0.75rem;\n font-size: 1.125rem;\n font-weight: 620;\n letter-spacing: -0.01em;\n}\n\nh3 {\n margin: 0 0 0.5rem;\n font-size: 1rem;\n font-weight: 620;\n}\n\np {\n margin: 0 0 1rem;\n overflow-wrap: break-word;\n}\n\na {\n color: var(--accent);\n text-underline-offset: 3px;\n overflow-wrap: anywhere;\n}\n\na:hover {\n text-decoration-thickness: 2px;\n}\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: 0.75rem 5.25rem 0.75rem 0.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 {\n font:\n 0.8125rem/1.6 ui-monospace,\n SFMono-Regular,\n Menlo,\n monospace;\n}\n\n/* Highlighted HTML, marked up by highlight.js. Brackets stay muted so the names read first. */\n.hljs-tag {\n color: var(--muted);\n}\n.hljs-name {\n color: var(--syntax-tag);\n}\n.hljs-attr {\n color: var(--syntax-attr);\n}\n.hljs-string {\n color: var(--syntax-string);\n}\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: 0.5rem;\n right: 0.5rem;\n padding: 0.1875rem 0.5rem;\n border: 1px solid var(--line);\n border-radius: 5px;\n background: var(--surface);\n color: var(--muted);\n font:\n 500 0.75rem/1.5 system-ui,\n sans-serif;\n user-select: none;\n}\n\n.copy:hover {\n color: var(--ink);\n border-color: var(--muted);\n}\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: 0.75rem;\n padding: 0.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) {\n max-width: 54rem;\n}\n\n.accounts {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n gap: 0.75rem;\n margin-bottom: 0.75rem;\n}\n\n.accounts .side {\n position: relative;\n display: flex;\n flex-direction: column;\n margin-bottom: 0;\n}\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: 0.75rem;\n right: 0.875rem;\n padding: 0.0625rem 0.5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n color: var(--muted);\n font-size: 0.6875rem;\n font-weight: 550;\n}\n\n.listing.preferred {\n border-color: var(--accent);\n color: var(--accent);\n}\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 {\n margin-top: auto;\n}\n.accounts .rest {\n display: flex;\n flex-direction: column;\n}\n.accounts .rest > p:last-of-type {\n margin-bottom: 0.625rem;\n}\n\n/*\n * Two across, with the subject one card wide and centred above them. Every card on the\n * page is then the same width.\n */\n@media (min-width: 48rem) {\n .accounts {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n /* Each card is three rows of the grid: who, its verdict, and the rest. A row is as tall\n as the taller card's part, so the verdicts and times of neighbouring cards line up. */\n .accounts > .side {\n display: grid;\n grid-row: span 3;\n grid-template-rows: subgrid;\n row-gap: 0;\n }\n .subject {\n width: calc(50% - 0.375rem);\n margin-inline: auto;\n text-align: center;\n }\n .subject .name {\n justify-content: center;\n }\n}\n\n/* Between a subject and its accounts on the page that lists them. */\n.joiner {\n display: block;\n width: 1.25rem;\n height: 1.25rem;\n margin: 0 auto 0.75rem;\n color: var(--muted);\n}\n\n.who {\n margin: 0 0 0.125rem;\n color: var(--muted);\n font-size: 0.75rem;\n font-weight: 550;\n letter-spacing: 0.02em;\n}\n\n/* A provider's mark before its name, at the height of the letters. */\n.who .provider {\n display: inline-block;\n width: 0.875rem;\n height: 0.875rem;\n margin-right: 0.375rem;\n vertical-align: -0.125rem;\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 {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 0 1rem;\n margin: 0 0 0 -4px;\n padding-left: 4px;\n overflow-x: clip;\n font-size: 1.0625rem;\n font-weight: 620;\n letter-spacing: -0.01em;\n}\n\n/* The account's creation date, after the name and a dot. It wraps under a long name. */\n.name .since {\n position: relative;\n color: var(--muted);\n font-size: 0.8125rem;\n font-weight: 400;\n letter-spacing: 0;\n white-space: nowrap;\n}\n\n/* The dot sits in the gap before the date. */\n.name .since::before {\n content: '\u00B7';\n position: absolute;\n left: -0.5rem;\n transform: translateX(-50%);\n}\n\n/* The name is the card's subject before it is a link, so the rule is drawn on hover. */\n.name a {\n text-decoration: none;\n}\n\n.name a:hover {\n text-decoration: underline;\n}\n\n.reference,\n.how {\n margin: 0;\n color: var(--muted);\n font-size: 0.8125rem;\n line-height: 1.5;\n}\n\n.identity + .how {\n margin-top: 0.5rem;\n}\n.fingerprint {\n font-family: ui-monospace, monospace;\n text-wrap: balance;\n}\n.fingerprint span {\n display: inline-block;\n margin-right: 0.5ch;\n}\n.how.additional {\n padding-left: 0.75rem;\n}\n/* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n.how .signed-mark {\n display: inline-block;\n margin-left: 0.3125rem;\n padding: 0 0.375rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n font-size: 0.625rem;\n font-weight: 550;\n line-height: 1.3;\n text-decoration: none;\n vertical-align: 0.0625rem;\n}\n\n.fine {\n color: var(--muted);\n font-size: 0.8125rem;\n line-height: 1.55;\n margin-bottom: 0.625rem;\n}\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 {\n font-size: 0.875rem;\n line-height: 1.55;\n margin-bottom: 0.875rem;\n}\n/* Arrived at from a badge's \"signed\" link, so the reader is shown where they landed. */\n.signed:target {\n margin-inline: -0.75rem;\n padding: 0.625rem 0.75rem;\n border-radius: 8px;\n background: var(--raised);\n}\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: 0.3rem 1.5rem;\n margin: 1.25rem 0;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n font-size: 0.8125rem;\n}\n\ndt {\n color: var(--muted);\n}\n\ndd {\n margin: 0;\n font-variant-numeric: tabular-nums;\n}\n\n/* Inside a card the rule above it is the card's own edge, so the list needs none. */\nsection dl {\n margin: 0.5rem 0 1rem;\n padding-top: 0;\n border-top: 0;\n}\n\n/* Right under the heading, the heading's own rule is the one above it. */\nh1 + dl {\n padding-top: 0;\n border-top: 0;\n}\n.side dl {\n margin: 0.625rem 0 0;\n padding-top: 0.625rem;\n}\n\n/* The links under a record, ruled off from the record and its disclaimer. */\nfooter {\n margin-top: 1.25rem;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n}\nfooter > :last-child {\n margin-bottom: 0;\n}\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 {\n margin-bottom: 0;\n}\n\n/* What holds a control that cannot be undone. */\nsection.danger,\nsection.caution {\n margin-top: 2rem;\n}\nsection.danger {\n border-color: var(--warn);\n}\n\n/* What holds a control that breaks something for a while, but can be recovered from. */\nsection.caution {\n border-color: var(--caution);\n}\nsection > h2:first-child {\n margin-top: 0;\n}\n\nform {\n margin: 0 0 1.5rem;\n}\n\nlabel {\n display: block;\n margin: 0 0 0.75rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n\ninput,\ntextarea {\n display: block;\n width: 100%;\n margin-top: 0.375rem;\n padding: 0.5rem 0.625rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--surface);\n color: var(--ink);\n font:\n 0.875rem/1.5 ui-monospace,\n SFMono-Regular,\n Menlo,\n monospace;\n}\n\n/* Height comes from the rows attribute, which differs per box. */\ntextarea {\n resize: vertical;\n}\n\ninput:focus-visible,\ntextarea:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 1px;\n}\n\nbutton {\n padding: 0.5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font:\n 500 0.9375rem/1.4 system-ui,\n -apple-system,\n 'Segoe UI',\n 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: 0.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: 0.9375rem;\n line-height: 1.4;\n text-decoration: none;\n}\n\nbutton + button {\n margin-left: 0.5rem;\n border-color: var(--line);\n background: none;\n color: var(--ink);\n}\n\n/* The other choice beside a page's one action, such as Cancel. */\na.button.secondary {\n border-color: var(--line);\n background: none;\n color: var(--ink);\n}\n\n/* A page's action and its alternative, side by side. */\n.choice {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n}\n.choice form {\n margin: 0;\n}\n\n/* Ways to verify: one full-width button per method, none filled over the others. */\n.choices {\n display: grid;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n}\n.choices form {\n margin: 0;\n}\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 {\n border-color: var(--muted);\n}\n\n/* A provider's mark before a button's label, at the height of the letters. */\nbutton .provider {\n width: 1rem;\n height: 1rem;\n margin-right: 0.5rem;\n vertical-align: -0.1875rem;\n}\n\n/* A control that ends something. Filled where it is the page's one action. */\nbutton.danger {\n border-color: var(--warn);\n background: var(--warn);\n}\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 {\n position: relative;\n margin-bottom: 1rem;\n}\n\n.state {\n position: absolute;\n top: 0.75rem;\n right: 0.875rem;\n display: flex;\n align-items: center;\n gap: 0.375rem;\n height: 1.625rem;\n padding: 0 0.75rem 0 0.5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--surface);\n color: var(--ink);\n font-size: 0.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 {\n flex-shrink: 0;\n width: 1rem;\n height: 1rem;\n}\n.verdict.pending {\n opacity: 0.45;\n}\n.how .verdict {\n width: 0.875rem;\n height: 0.875rem;\n margin-right: 0.3125rem;\n vertical-align: -0.125rem;\n}\n/* Cropped to its strokes and taller than the heading's letters, centred beside them. */\nh1:has(.verdict) {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n}\nh1 .verdict {\n width: auto;\n height: 2.25rem;\n}\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 {\n width: auto;\n height: 100%;\n}\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 {\n padding: 0.3125rem 0.5rem;\n text-box: trim-both cap alphabetic;\n }\n .state > span {\n text-box: trim-both cap alphabetic;\n }\n}\n\n/* Links that sit together on one line, wrapping where there is no room. */\n.links {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5rem 1.25rem;\n font-size: 0.875rem;\n}\n\n/* Under the account's id, with the facts about it rather than the controls. */\n.evidence {\n margin: 0.375rem 0 0;\n font-size: 0.8125rem;\n}\n\n.connection dl {\n margin-bottom: 0.75rem;\n}\n.connection .side > :last-child {\n margin-bottom: 0;\n}\n.connection pre {\n margin: 0.75rem 0;\n background: var(--surface);\n}\n.connection .actions {\n margin-top: 0.875rem;\n}\n\n/* The buttons for one connection in a row, wrapping where there is no room. */\n.actions {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n.actions form {\n display: contents;\n}\n\n.actions button,\n.actions a.button {\n margin: 0;\n padding: 0.3125rem 0.75rem;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n font-size: 0.8125rem;\n}\n\n.actions button:hover,\n.actions a.button:hover {\n border-color: var(--muted);\n}\n\n/* The controls that cannot be undone, together at the foot of the card. */\n.ending {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 0.5rem;\n margin-top: 0.75rem;\n padding-top: 0.75rem;\n border-top: 1px solid var(--line);\n}\n.ending form {\n margin: 0;\n}\n\n.ending button.danger {\n padding: 0.3125rem 0.75rem;\n background: var(--surface);\n color: var(--warn);\n font-size: 0.8125rem;\n}\n\nfieldset {\n margin: 0 0 1.25rem;\n padding: 1rem 1.125rem 0.25rem;\n border: 1px solid var(--line);\n border-radius: 8px;\n}\n\nlegend {\n padding: 0 0.375rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n\nfieldset label {\n display: flex;\n align-items: baseline;\n gap: 0.5rem;\n color: var(--ink);\n font-size: 0.9375rem;\n}\n\nfieldset input {\n width: auto;\n margin: 0;\n}\n\nfieldset p {\n margin: 0.25rem 0 0.875rem 1.5rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n";
|
|
1
|
+
declare const _default: ":root {\n --ink: #23312b;\n --muted: #6b786f;\n --line: #dce2de;\n --surface: #fff;\n --raised: #f7f9f7;\n --accent: #245f43;\n --warn: #a4442b;\n --caution: #b07a12;\n --syntax-tag: #245f43;\n --syntax-attr: #8a5a0b;\n --syntax-string: #2b5f8a;\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 --caution: #d9a843;\n --syntax-tag: #7fc4a2;\n --syntax-attr: #d9a843;\n --syntax-string: #8fbbe0;\n }\n}\n\n* {\n box-sizing: border-box;\n}\n\nbody {\n margin: 0;\n padding: 0 1rem 4rem;\n background: var(--surface);\n color: var(--ink);\n font:\n 16px/1.65 system-ui,\n -apple-system,\n 'Segoe UI',\n 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: -0.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 {\n margin-bottom: 1.25rem;\n padding-bottom: 0;\n border-bottom: none;\n}\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) {\n display: flex;\n min-height: 100vh;\n min-height: 100dvh;\n}\nmain.single {\n width: 100%;\n max-width: 24rem;\n margin: auto;\n padding-bottom: 3rem;\n}\n\nh2 {\n margin: 2rem 0 0.75rem;\n font-size: 1.125rem;\n font-weight: 620;\n letter-spacing: -0.01em;\n}\n\nh3 {\n margin: 0 0 0.5rem;\n font-size: 1rem;\n font-weight: 620;\n}\n\np {\n margin: 0 0 1rem;\n overflow-wrap: break-word;\n}\n\na {\n color: var(--accent);\n text-underline-offset: 3px;\n overflow-wrap: anywhere;\n}\n\na:hover {\n text-decoration-thickness: 2px;\n}\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: 0.75rem 5.25rem 0.75rem 0.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 {\n font:\n 0.8125rem/1.6 ui-monospace,\n SFMono-Regular,\n Menlo,\n monospace;\n}\n\n/* Highlighted HTML, marked up by highlight.js. Brackets stay muted so the names read first. */\n.hljs-tag {\n color: var(--muted);\n}\n.hljs-name {\n color: var(--syntax-tag);\n}\n.hljs-attr {\n color: var(--syntax-attr);\n}\n.hljs-string {\n color: var(--syntax-string);\n}\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: 0.5rem;\n right: 0.5rem;\n padding: 0.1875rem 0.5rem;\n border: 1px solid var(--line);\n border-radius: 5px;\n background: var(--surface);\n color: var(--muted);\n font:\n 500 0.75rem/1.5 system-ui,\n sans-serif;\n user-select: none;\n}\n\n.copy:hover {\n color: var(--ink);\n border-color: var(--muted);\n}\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: 0.75rem;\n padding: 0.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) {\n max-width: 54rem;\n}\n\n.accounts {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n gap: 0.75rem;\n margin-bottom: 0.75rem;\n}\n\n.accounts .side {\n position: relative;\n display: flex;\n flex-direction: column;\n margin-bottom: 0;\n}\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: 0.75rem;\n right: 0.875rem;\n padding: 0.0625rem 0.5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n color: var(--muted);\n font-size: 0.6875rem;\n font-weight: 550;\n}\n\n.listing.preferred {\n border-color: var(--accent);\n color: var(--accent);\n}\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 {\n margin-top: auto;\n}\n.accounts .rest {\n display: flex;\n flex-direction: column;\n}\n.accounts .rest > p:last-of-type {\n margin-bottom: 0.625rem;\n}\n\n/*\n * Two across, with the subject one card wide and centred above them. Every card on the\n * page is then the same width.\n */\n@media (min-width: 48rem) {\n .accounts {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n /* Each card is three rows of the grid: who, its verdict, and the rest. A row is as tall\n as the taller card's part, so the verdicts and times of neighbouring cards line up. */\n .accounts > .side {\n display: grid;\n grid-row: span 3;\n grid-template-rows: subgrid;\n row-gap: 0;\n }\n .subject {\n width: calc(50% - 0.375rem);\n margin-inline: auto;\n text-align: center;\n }\n .subject .name {\n justify-content: center;\n }\n}\n\n/* Between a subject and its accounts on the page that lists them. */\n.joiner {\n display: block;\n width: 1.25rem;\n height: 1.25rem;\n margin: 0 auto 0.75rem;\n color: var(--muted);\n}\n\n.who {\n margin: 0 0 0.125rem;\n color: var(--muted);\n font-size: 0.75rem;\n font-weight: 550;\n letter-spacing: 0.02em;\n}\n\n/* A provider's mark before its name, at the height of the letters. */\n.who .provider {\n display: inline-block;\n width: 0.875rem;\n height: 0.875rem;\n margin-right: 0.375rem;\n vertical-align: -0.125rem;\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 {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 0 1rem;\n margin: 0 0 0 -4px;\n padding-left: 4px;\n overflow-x: clip;\n font-size: 1.0625rem;\n font-weight: 620;\n letter-spacing: -0.01em;\n}\n\n/* The account's creation date, after the name and a dot. It wraps under a long name. */\n.name .since {\n position: relative;\n color: var(--muted);\n font-size: 0.8125rem;\n font-weight: 400;\n letter-spacing: 0;\n white-space: nowrap;\n}\n\n/* The dot sits in the gap before the date. */\n.name .since::before {\n content: '\u00B7';\n position: absolute;\n left: -0.5rem;\n transform: translateX(-50%);\n}\n\n/* The name is the card's subject before it is a link, so the rule is drawn on hover. */\n.name a {\n text-decoration: none;\n}\n\n.name a:hover {\n text-decoration: underline;\n}\n\n.reference,\n.how {\n margin: 0;\n color: var(--muted);\n font-size: 0.8125rem;\n line-height: 1.5;\n}\n\n.identity + .how {\n margin-top: 0.5rem;\n}\n.fingerprint {\n font-family: ui-monospace, monospace;\n text-wrap: balance;\n}\n.fingerprint span {\n display: inline-block;\n margin-right: 0.5ch;\n}\n.how.additional {\n padding-left: 0.75rem;\n}\n/* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n.how .signed-mark {\n display: inline-block;\n margin-left: 0.3125rem;\n padding: 0 0.375rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n font-size: 0.625rem;\n font-weight: 550;\n line-height: 1.3;\n text-decoration: none;\n vertical-align: 0.0625rem;\n}\n\n.fine {\n color: var(--muted);\n font-size: 0.8125rem;\n line-height: 1.55;\n margin-bottom: 0.625rem;\n}\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 {\n font-size: 0.875rem;\n line-height: 1.55;\n margin-bottom: 0.875rem;\n}\n/* Arrived at from a badge's \"signed\" link, so the reader is shown where they landed. */\n.signed:target {\n margin-inline: -0.75rem;\n padding: 0.625rem 0.75rem;\n border-radius: 8px;\n background: var(--raised);\n}\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: 0.3rem 1.5rem;\n margin: 1.25rem 0;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n font-size: 0.8125rem;\n}\n\ndt {\n color: var(--muted);\n}\n\ndd {\n margin: 0;\n font-variant-numeric: tabular-nums;\n}\n\n/* Inside a card the rule above it is the card's own edge, so the list needs none. */\nsection dl {\n margin: 0.5rem 0 1rem;\n padding-top: 0;\n border-top: 0;\n}\n\n/* Right under the heading, the heading's own rule is the one above it. */\nh1 + dl {\n padding-top: 0;\n border-top: 0;\n}\n.side dl {\n margin: 0.625rem 0 0;\n padding-top: 0.625rem;\n}\n\n/* The links under a record, ruled off from the record and its disclaimer. */\nfooter {\n margin-top: 1.25rem;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n}\nfooter > :last-child {\n margin-bottom: 0;\n}\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 {\n margin-bottom: 0;\n}\n\n/* What holds a control that cannot be undone. */\nsection.danger,\nsection.caution {\n margin-top: 2rem;\n}\nsection.danger {\n border-color: var(--warn);\n}\n\n/* What holds a control that breaks something for a while, but can be recovered from. */\nsection.caution {\n border-color: var(--caution);\n}\nsection > h2:first-child {\n margin-top: 0;\n}\n\nform {\n margin: 0 0 1.5rem;\n}\n\nlabel {\n display: block;\n margin: 0 0 0.75rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n\ninput,\ntextarea {\n display: block;\n width: 100%;\n margin-top: 0.375rem;\n padding: 0.5rem 0.625rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--surface);\n color: var(--ink);\n font:\n 0.875rem/1.5 ui-monospace,\n SFMono-Regular,\n Menlo,\n monospace;\n}\n\n/* Height comes from the rows attribute, which differs per box. */\ntextarea {\n resize: vertical;\n}\n\ninput:focus-visible,\ntextarea:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 1px;\n}\n\nbutton {\n padding: 0.5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font:\n 500 0.9375rem/1.4 system-ui,\n -apple-system,\n 'Segoe UI',\n 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: 0.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: 0.9375rem;\n line-height: 1.4;\n text-decoration: none;\n}\n\nbutton + button {\n margin-left: 0.5rem;\n border-color: var(--line);\n background: none;\n color: var(--ink);\n}\n\n/* The other choice beside a page's one action, such as Cancel. */\na.button.secondary {\n border-color: var(--line);\n background: none;\n color: var(--ink);\n}\n\n/* A page's action and its alternative, side by side. */\n.choice {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n}\n.choice form {\n margin: 0;\n}\n\n/* Ways to verify: one full-width button per method, none filled over the others. */\n.choices {\n display: grid;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n}\n.choices form {\n margin: 0;\n}\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 {\n border-color: var(--muted);\n}\n\n/* A provider's mark before a button's label, at the height of the letters. */\nbutton .provider {\n width: 1rem;\n height: 1rem;\n margin-right: 0.5rem;\n vertical-align: -0.1875rem;\n}\n\n/* A control that ends something. Filled where it is the page's one action. */\nbutton.danger {\n border-color: var(--warn);\n background: var(--warn);\n}\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 {\n position: relative;\n margin-bottom: 1rem;\n}\n\n.state {\n position: absolute;\n top: 0.75rem;\n right: 0.875rem;\n display: flex;\n align-items: center;\n gap: 0.375rem;\n height: 1.625rem;\n padding: 0 0.75rem 0 0.5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--surface);\n color: var(--ink);\n font-size: 0.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 {\n flex-shrink: 0;\n width: 1rem;\n height: 1rem;\n}\n.verdict.pending {\n opacity: 0.45;\n}\n.how .verdict {\n width: 0.875rem;\n height: 0.875rem;\n margin-right: 0.3125rem;\n vertical-align: -0.125rem;\n}\n/* Cropped to its strokes and taller than the heading's letters, centred beside them. */\nh1:has(.verdict) {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n}\nh1 .verdict {\n width: auto;\n height: 2.25rem;\n}\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 {\n width: auto;\n height: 100%;\n}\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 {\n padding: 0.3125rem 0.5rem;\n text-box: trim-both cap alphabetic;\n }\n .state > span {\n text-box: trim-both cap alphabetic;\n }\n}\n\n/* Links that sit together on one line, wrapping where there is no room. */\n.links {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5rem 1.25rem;\n font-size: 0.875rem;\n}\n\n/* Under the account's id, with the facts about it rather than the controls. */\n.evidence {\n margin: 0.375rem 0 0;\n font-size: 0.8125rem;\n}\n\n.connection dl {\n margin-bottom: 0.75rem;\n}\n.connection .side > :last-child {\n margin-bottom: 0;\n}\n.connection pre {\n margin: 0.75rem 0;\n background: var(--surface);\n}\n\n/* The embed snippet is copied once, so it stays closed until asked for. */\n.embed summary {\n width: fit-content;\n cursor: pointer;\n}\n.embed summary:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 1px;\n}\n.embed pre {\n margin-top: 0;\n}\n.connection .actions {\n margin-top: 0.875rem;\n}\n\n/* The buttons for one connection in a row, wrapping where there is no room. */\n.actions {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n.actions form {\n display: contents;\n}\n\n.actions button,\n.actions a.button {\n margin: 0;\n padding: 0.3125rem 0.75rem;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n font-size: 0.8125rem;\n}\n\n.actions button:hover,\n.actions a.button:hover {\n border-color: var(--muted);\n}\n\n/* The controls that cannot be undone, together at the foot of the card. */\n.ending {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 0.5rem;\n margin-top: 0.75rem;\n padding-top: 0.75rem;\n border-top: 1px solid var(--line);\n}\n.ending form {\n margin: 0;\n}\n\n.ending button.danger {\n padding: 0.3125rem 0.75rem;\n background: var(--surface);\n color: var(--warn);\n font-size: 0.8125rem;\n}\n\nfieldset {\n margin: 0 0 1.25rem;\n padding: 1rem 1.125rem 0.25rem;\n border: 1px solid var(--line);\n border-radius: 8px;\n}\n\nlegend {\n padding: 0 0.375rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n\nfieldset label {\n display: flex;\n align-items: baseline;\n gap: 0.5rem;\n color: var(--ink);\n font-size: 0.9375rem;\n}\n\nfieldset input {\n width: auto;\n margin: 0;\n}\n\nfieldset p {\n margin: 0.25rem 0 0.875rem 1.5rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n";
|
|
2
2
|
export default _default;
|
package/dist/server/style.css.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated from style.css by scripts/css.mjs. Do not edit.
|
|
2
|
-
export default ":root {\n --ink: #23312b;\n --muted: #6b786f;\n --line: #dce2de;\n --surface: #fff;\n --raised: #f7f9f7;\n --accent: #245f43;\n --warn: #a4442b;\n --caution: #b07a12;\n --syntax-tag: #245f43;\n --syntax-attr: #8a5a0b;\n --syntax-string: #2b5f8a;\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 --caution: #d9a843;\n --syntax-tag: #7fc4a2;\n --syntax-attr: #d9a843;\n --syntax-string: #8fbbe0;\n }\n}\n\n* {\n box-sizing: border-box;\n}\n\nbody {\n margin: 0;\n padding: 0 1rem 4rem;\n background: var(--surface);\n color: var(--ink);\n font:\n 16px/1.65 system-ui,\n -apple-system,\n 'Segoe UI',\n 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: -0.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 {\n margin-bottom: 1.25rem;\n padding-bottom: 0;\n border-bottom: none;\n}\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) {\n display: flex;\n min-height: 100vh;\n min-height: 100dvh;\n}\nmain.single {\n width: 100%;\n max-width: 24rem;\n margin: auto;\n padding-bottom: 3rem;\n}\n\nh2 {\n margin: 2rem 0 0.75rem;\n font-size: 1.125rem;\n font-weight: 620;\n letter-spacing: -0.01em;\n}\n\nh3 {\n margin: 0 0 0.5rem;\n font-size: 1rem;\n font-weight: 620;\n}\n\np {\n margin: 0 0 1rem;\n overflow-wrap: break-word;\n}\n\na {\n color: var(--accent);\n text-underline-offset: 3px;\n overflow-wrap: anywhere;\n}\n\na:hover {\n text-decoration-thickness: 2px;\n}\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: 0.75rem 5.25rem 0.75rem 0.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 {\n font:\n 0.8125rem/1.6 ui-monospace,\n SFMono-Regular,\n Menlo,\n monospace;\n}\n\n/* Highlighted HTML, marked up by highlight.js. Brackets stay muted so the names read first. */\n.hljs-tag {\n color: var(--muted);\n}\n.hljs-name {\n color: var(--syntax-tag);\n}\n.hljs-attr {\n color: var(--syntax-attr);\n}\n.hljs-string {\n color: var(--syntax-string);\n}\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: 0.5rem;\n right: 0.5rem;\n padding: 0.1875rem 0.5rem;\n border: 1px solid var(--line);\n border-radius: 5px;\n background: var(--surface);\n color: var(--muted);\n font:\n 500 0.75rem/1.5 system-ui,\n sans-serif;\n user-select: none;\n}\n\n.copy:hover {\n color: var(--ink);\n border-color: var(--muted);\n}\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: 0.75rem;\n padding: 0.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) {\n max-width: 54rem;\n}\n\n.accounts {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n gap: 0.75rem;\n margin-bottom: 0.75rem;\n}\n\n.accounts .side {\n position: relative;\n display: flex;\n flex-direction: column;\n margin-bottom: 0;\n}\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: 0.75rem;\n right: 0.875rem;\n padding: 0.0625rem 0.5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n color: var(--muted);\n font-size: 0.6875rem;\n font-weight: 550;\n}\n\n.listing.preferred {\n border-color: var(--accent);\n color: var(--accent);\n}\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 {\n margin-top: auto;\n}\n.accounts .rest {\n display: flex;\n flex-direction: column;\n}\n.accounts .rest > p:last-of-type {\n margin-bottom: 0.625rem;\n}\n\n/*\n * Two across, with the subject one card wide and centred above them. Every card on the\n * page is then the same width.\n */\n@media (min-width: 48rem) {\n .accounts {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n /* Each card is three rows of the grid: who, its verdict, and the rest. A row is as tall\n as the taller card's part, so the verdicts and times of neighbouring cards line up. */\n .accounts > .side {\n display: grid;\n grid-row: span 3;\n grid-template-rows: subgrid;\n row-gap: 0;\n }\n .subject {\n width: calc(50% - 0.375rem);\n margin-inline: auto;\n text-align: center;\n }\n .subject .name {\n justify-content: center;\n }\n}\n\n/* Between a subject and its accounts on the page that lists them. */\n.joiner {\n display: block;\n width: 1.25rem;\n height: 1.25rem;\n margin: 0 auto 0.75rem;\n color: var(--muted);\n}\n\n.who {\n margin: 0 0 0.125rem;\n color: var(--muted);\n font-size: 0.75rem;\n font-weight: 550;\n letter-spacing: 0.02em;\n}\n\n/* A provider's mark before its name, at the height of the letters. */\n.who .provider {\n display: inline-block;\n width: 0.875rem;\n height: 0.875rem;\n margin-right: 0.375rem;\n vertical-align: -0.125rem;\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 {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 0 1rem;\n margin: 0 0 0 -4px;\n padding-left: 4px;\n overflow-x: clip;\n font-size: 1.0625rem;\n font-weight: 620;\n letter-spacing: -0.01em;\n}\n\n/* The account's creation date, after the name and a dot. It wraps under a long name. */\n.name .since {\n position: relative;\n color: var(--muted);\n font-size: 0.8125rem;\n font-weight: 400;\n letter-spacing: 0;\n white-space: nowrap;\n}\n\n/* The dot sits in the gap before the date. */\n.name .since::before {\n content: '·';\n position: absolute;\n left: -0.5rem;\n transform: translateX(-50%);\n}\n\n/* The name is the card's subject before it is a link, so the rule is drawn on hover. */\n.name a {\n text-decoration: none;\n}\n\n.name a:hover {\n text-decoration: underline;\n}\n\n.reference,\n.how {\n margin: 0;\n color: var(--muted);\n font-size: 0.8125rem;\n line-height: 1.5;\n}\n\n.identity + .how {\n margin-top: 0.5rem;\n}\n.fingerprint {\n font-family: ui-monospace, monospace;\n text-wrap: balance;\n}\n.fingerprint span {\n display: inline-block;\n margin-right: 0.5ch;\n}\n.how.additional {\n padding-left: 0.75rem;\n}\n/* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n.how .signed-mark {\n display: inline-block;\n margin-left: 0.3125rem;\n padding: 0 0.375rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n font-size: 0.625rem;\n font-weight: 550;\n line-height: 1.3;\n text-decoration: none;\n vertical-align: 0.0625rem;\n}\n\n.fine {\n color: var(--muted);\n font-size: 0.8125rem;\n line-height: 1.55;\n margin-bottom: 0.625rem;\n}\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 {\n font-size: 0.875rem;\n line-height: 1.55;\n margin-bottom: 0.875rem;\n}\n/* Arrived at from a badge's \"signed\" link, so the reader is shown where they landed. */\n.signed:target {\n margin-inline: -0.75rem;\n padding: 0.625rem 0.75rem;\n border-radius: 8px;\n background: var(--raised);\n}\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: 0.3rem 1.5rem;\n margin: 1.25rem 0;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n font-size: 0.8125rem;\n}\n\ndt {\n color: var(--muted);\n}\n\ndd {\n margin: 0;\n font-variant-numeric: tabular-nums;\n}\n\n/* Inside a card the rule above it is the card's own edge, so the list needs none. */\nsection dl {\n margin: 0.5rem 0 1rem;\n padding-top: 0;\n border-top: 0;\n}\n\n/* Right under the heading, the heading's own rule is the one above it. */\nh1 + dl {\n padding-top: 0;\n border-top: 0;\n}\n.side dl {\n margin: 0.625rem 0 0;\n padding-top: 0.625rem;\n}\n\n/* The links under a record, ruled off from the record and its disclaimer. */\nfooter {\n margin-top: 1.25rem;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n}\nfooter > :last-child {\n margin-bottom: 0;\n}\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 {\n margin-bottom: 0;\n}\n\n/* What holds a control that cannot be undone. */\nsection.danger,\nsection.caution {\n margin-top: 2rem;\n}\nsection.danger {\n border-color: var(--warn);\n}\n\n/* What holds a control that breaks something for a while, but can be recovered from. */\nsection.caution {\n border-color: var(--caution);\n}\nsection > h2:first-child {\n margin-top: 0;\n}\n\nform {\n margin: 0 0 1.5rem;\n}\n\nlabel {\n display: block;\n margin: 0 0 0.75rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n\ninput,\ntextarea {\n display: block;\n width: 100%;\n margin-top: 0.375rem;\n padding: 0.5rem 0.625rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--surface);\n color: var(--ink);\n font:\n 0.875rem/1.5 ui-monospace,\n SFMono-Regular,\n Menlo,\n monospace;\n}\n\n/* Height comes from the rows attribute, which differs per box. */\ntextarea {\n resize: vertical;\n}\n\ninput:focus-visible,\ntextarea:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 1px;\n}\n\nbutton {\n padding: 0.5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font:\n 500 0.9375rem/1.4 system-ui,\n -apple-system,\n 'Segoe UI',\n 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: 0.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: 0.9375rem;\n line-height: 1.4;\n text-decoration: none;\n}\n\nbutton + button {\n margin-left: 0.5rem;\n border-color: var(--line);\n background: none;\n color: var(--ink);\n}\n\n/* The other choice beside a page's one action, such as Cancel. */\na.button.secondary {\n border-color: var(--line);\n background: none;\n color: var(--ink);\n}\n\n/* A page's action and its alternative, side by side. */\n.choice {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n}\n.choice form {\n margin: 0;\n}\n\n/* Ways to verify: one full-width button per method, none filled over the others. */\n.choices {\n display: grid;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n}\n.choices form {\n margin: 0;\n}\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 {\n border-color: var(--muted);\n}\n\n/* A provider's mark before a button's label, at the height of the letters. */\nbutton .provider {\n width: 1rem;\n height: 1rem;\n margin-right: 0.5rem;\n vertical-align: -0.1875rem;\n}\n\n/* A control that ends something. Filled where it is the page's one action. */\nbutton.danger {\n border-color: var(--warn);\n background: var(--warn);\n}\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 {\n position: relative;\n margin-bottom: 1rem;\n}\n\n.state {\n position: absolute;\n top: 0.75rem;\n right: 0.875rem;\n display: flex;\n align-items: center;\n gap: 0.375rem;\n height: 1.625rem;\n padding: 0 0.75rem 0 0.5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--surface);\n color: var(--ink);\n font-size: 0.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 {\n flex-shrink: 0;\n width: 1rem;\n height: 1rem;\n}\n.verdict.pending {\n opacity: 0.45;\n}\n.how .verdict {\n width: 0.875rem;\n height: 0.875rem;\n margin-right: 0.3125rem;\n vertical-align: -0.125rem;\n}\n/* Cropped to its strokes and taller than the heading's letters, centred beside them. */\nh1:has(.verdict) {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n}\nh1 .verdict {\n width: auto;\n height: 2.25rem;\n}\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 {\n width: auto;\n height: 100%;\n}\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 {\n padding: 0.3125rem 0.5rem;\n text-box: trim-both cap alphabetic;\n }\n .state > span {\n text-box: trim-both cap alphabetic;\n }\n}\n\n/* Links that sit together on one line, wrapping where there is no room. */\n.links {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5rem 1.25rem;\n font-size: 0.875rem;\n}\n\n/* Under the account's id, with the facts about it rather than the controls. */\n.evidence {\n margin: 0.375rem 0 0;\n font-size: 0.8125rem;\n}\n\n.connection dl {\n margin-bottom: 0.75rem;\n}\n.connection .side > :last-child {\n margin-bottom: 0;\n}\n.connection pre {\n margin: 0.75rem 0;\n background: var(--surface);\n}\n.connection .actions {\n margin-top: 0.875rem;\n}\n\n/* The buttons for one connection in a row, wrapping where there is no room. */\n.actions {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n.actions form {\n display: contents;\n}\n\n.actions button,\n.actions a.button {\n margin: 0;\n padding: 0.3125rem 0.75rem;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n font-size: 0.8125rem;\n}\n\n.actions button:hover,\n.actions a.button:hover {\n border-color: var(--muted);\n}\n\n/* The controls that cannot be undone, together at the foot of the card. */\n.ending {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 0.5rem;\n margin-top: 0.75rem;\n padding-top: 0.75rem;\n border-top: 1px solid var(--line);\n}\n.ending form {\n margin: 0;\n}\n\n.ending button.danger {\n padding: 0.3125rem 0.75rem;\n background: var(--surface);\n color: var(--warn);\n font-size: 0.8125rem;\n}\n\nfieldset {\n margin: 0 0 1.25rem;\n padding: 1rem 1.125rem 0.25rem;\n border: 1px solid var(--line);\n border-radius: 8px;\n}\n\nlegend {\n padding: 0 0.375rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n\nfieldset label {\n display: flex;\n align-items: baseline;\n gap: 0.5rem;\n color: var(--ink);\n font-size: 0.9375rem;\n}\n\nfieldset input {\n width: auto;\n margin: 0;\n}\n\nfieldset p {\n margin: 0.25rem 0 0.875rem 1.5rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n";
|
|
2
|
+
export default ":root {\n --ink: #23312b;\n --muted: #6b786f;\n --line: #dce2de;\n --surface: #fff;\n --raised: #f7f9f7;\n --accent: #245f43;\n --warn: #a4442b;\n --caution: #b07a12;\n --syntax-tag: #245f43;\n --syntax-attr: #8a5a0b;\n --syntax-string: #2b5f8a;\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 --caution: #d9a843;\n --syntax-tag: #7fc4a2;\n --syntax-attr: #d9a843;\n --syntax-string: #8fbbe0;\n }\n}\n\n* {\n box-sizing: border-box;\n}\n\nbody {\n margin: 0;\n padding: 0 1rem 4rem;\n background: var(--surface);\n color: var(--ink);\n font:\n 16px/1.65 system-ui,\n -apple-system,\n 'Segoe UI',\n 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: -0.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 {\n margin-bottom: 1.25rem;\n padding-bottom: 0;\n border-bottom: none;\n}\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) {\n display: flex;\n min-height: 100vh;\n min-height: 100dvh;\n}\nmain.single {\n width: 100%;\n max-width: 24rem;\n margin: auto;\n padding-bottom: 3rem;\n}\n\nh2 {\n margin: 2rem 0 0.75rem;\n font-size: 1.125rem;\n font-weight: 620;\n letter-spacing: -0.01em;\n}\n\nh3 {\n margin: 0 0 0.5rem;\n font-size: 1rem;\n font-weight: 620;\n}\n\np {\n margin: 0 0 1rem;\n overflow-wrap: break-word;\n}\n\na {\n color: var(--accent);\n text-underline-offset: 3px;\n overflow-wrap: anywhere;\n}\n\na:hover {\n text-decoration-thickness: 2px;\n}\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: 0.75rem 5.25rem 0.75rem 0.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 {\n font:\n 0.8125rem/1.6 ui-monospace,\n SFMono-Regular,\n Menlo,\n monospace;\n}\n\n/* Highlighted HTML, marked up by highlight.js. Brackets stay muted so the names read first. */\n.hljs-tag {\n color: var(--muted);\n}\n.hljs-name {\n color: var(--syntax-tag);\n}\n.hljs-attr {\n color: var(--syntax-attr);\n}\n.hljs-string {\n color: var(--syntax-string);\n}\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: 0.5rem;\n right: 0.5rem;\n padding: 0.1875rem 0.5rem;\n border: 1px solid var(--line);\n border-radius: 5px;\n background: var(--surface);\n color: var(--muted);\n font:\n 500 0.75rem/1.5 system-ui,\n sans-serif;\n user-select: none;\n}\n\n.copy:hover {\n color: var(--ink);\n border-color: var(--muted);\n}\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: 0.75rem;\n padding: 0.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) {\n max-width: 54rem;\n}\n\n.accounts {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n gap: 0.75rem;\n margin-bottom: 0.75rem;\n}\n\n.accounts .side {\n position: relative;\n display: flex;\n flex-direction: column;\n margin-bottom: 0;\n}\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: 0.75rem;\n right: 0.875rem;\n padding: 0.0625rem 0.5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n color: var(--muted);\n font-size: 0.6875rem;\n font-weight: 550;\n}\n\n.listing.preferred {\n border-color: var(--accent);\n color: var(--accent);\n}\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 {\n margin-top: auto;\n}\n.accounts .rest {\n display: flex;\n flex-direction: column;\n}\n.accounts .rest > p:last-of-type {\n margin-bottom: 0.625rem;\n}\n\n/*\n * Two across, with the subject one card wide and centred above them. Every card on the\n * page is then the same width.\n */\n@media (min-width: 48rem) {\n .accounts {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n /* Each card is three rows of the grid: who, its verdict, and the rest. A row is as tall\n as the taller card's part, so the verdicts and times of neighbouring cards line up. */\n .accounts > .side {\n display: grid;\n grid-row: span 3;\n grid-template-rows: subgrid;\n row-gap: 0;\n }\n .subject {\n width: calc(50% - 0.375rem);\n margin-inline: auto;\n text-align: center;\n }\n .subject .name {\n justify-content: center;\n }\n}\n\n/* Between a subject and its accounts on the page that lists them. */\n.joiner {\n display: block;\n width: 1.25rem;\n height: 1.25rem;\n margin: 0 auto 0.75rem;\n color: var(--muted);\n}\n\n.who {\n margin: 0 0 0.125rem;\n color: var(--muted);\n font-size: 0.75rem;\n font-weight: 550;\n letter-spacing: 0.02em;\n}\n\n/* A provider's mark before its name, at the height of the letters. */\n.who .provider {\n display: inline-block;\n width: 0.875rem;\n height: 0.875rem;\n margin-right: 0.375rem;\n vertical-align: -0.125rem;\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 {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 0 1rem;\n margin: 0 0 0 -4px;\n padding-left: 4px;\n overflow-x: clip;\n font-size: 1.0625rem;\n font-weight: 620;\n letter-spacing: -0.01em;\n}\n\n/* The account's creation date, after the name and a dot. It wraps under a long name. */\n.name .since {\n position: relative;\n color: var(--muted);\n font-size: 0.8125rem;\n font-weight: 400;\n letter-spacing: 0;\n white-space: nowrap;\n}\n\n/* The dot sits in the gap before the date. */\n.name .since::before {\n content: '·';\n position: absolute;\n left: -0.5rem;\n transform: translateX(-50%);\n}\n\n/* The name is the card's subject before it is a link, so the rule is drawn on hover. */\n.name a {\n text-decoration: none;\n}\n\n.name a:hover {\n text-decoration: underline;\n}\n\n.reference,\n.how {\n margin: 0;\n color: var(--muted);\n font-size: 0.8125rem;\n line-height: 1.5;\n}\n\n.identity + .how {\n margin-top: 0.5rem;\n}\n.fingerprint {\n font-family: ui-monospace, monospace;\n text-wrap: balance;\n}\n.fingerprint span {\n display: inline-block;\n margin-right: 0.5ch;\n}\n.how.additional {\n padding-left: 0.75rem;\n}\n/* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n.how .signed-mark {\n display: inline-block;\n margin-left: 0.3125rem;\n padding: 0 0.375rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n font-size: 0.625rem;\n font-weight: 550;\n line-height: 1.3;\n text-decoration: none;\n vertical-align: 0.0625rem;\n}\n\n.fine {\n color: var(--muted);\n font-size: 0.8125rem;\n line-height: 1.55;\n margin-bottom: 0.625rem;\n}\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 {\n font-size: 0.875rem;\n line-height: 1.55;\n margin-bottom: 0.875rem;\n}\n/* Arrived at from a badge's \"signed\" link, so the reader is shown where they landed. */\n.signed:target {\n margin-inline: -0.75rem;\n padding: 0.625rem 0.75rem;\n border-radius: 8px;\n background: var(--raised);\n}\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: 0.3rem 1.5rem;\n margin: 1.25rem 0;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n font-size: 0.8125rem;\n}\n\ndt {\n color: var(--muted);\n}\n\ndd {\n margin: 0;\n font-variant-numeric: tabular-nums;\n}\n\n/* Inside a card the rule above it is the card's own edge, so the list needs none. */\nsection dl {\n margin: 0.5rem 0 1rem;\n padding-top: 0;\n border-top: 0;\n}\n\n/* Right under the heading, the heading's own rule is the one above it. */\nh1 + dl {\n padding-top: 0;\n border-top: 0;\n}\n.side dl {\n margin: 0.625rem 0 0;\n padding-top: 0.625rem;\n}\n\n/* The links under a record, ruled off from the record and its disclaimer. */\nfooter {\n margin-top: 1.25rem;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n}\nfooter > :last-child {\n margin-bottom: 0;\n}\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 {\n margin-bottom: 0;\n}\n\n/* What holds a control that cannot be undone. */\nsection.danger,\nsection.caution {\n margin-top: 2rem;\n}\nsection.danger {\n border-color: var(--warn);\n}\n\n/* What holds a control that breaks something for a while, but can be recovered from. */\nsection.caution {\n border-color: var(--caution);\n}\nsection > h2:first-child {\n margin-top: 0;\n}\n\nform {\n margin: 0 0 1.5rem;\n}\n\nlabel {\n display: block;\n margin: 0 0 0.75rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n\ninput,\ntextarea {\n display: block;\n width: 100%;\n margin-top: 0.375rem;\n padding: 0.5rem 0.625rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--surface);\n color: var(--ink);\n font:\n 0.875rem/1.5 ui-monospace,\n SFMono-Regular,\n Menlo,\n monospace;\n}\n\n/* Height comes from the rows attribute, which differs per box. */\ntextarea {\n resize: vertical;\n}\n\ninput:focus-visible,\ntextarea:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 1px;\n}\n\nbutton {\n padding: 0.5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font:\n 500 0.9375rem/1.4 system-ui,\n -apple-system,\n 'Segoe UI',\n 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: 0.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: 0.9375rem;\n line-height: 1.4;\n text-decoration: none;\n}\n\nbutton + button {\n margin-left: 0.5rem;\n border-color: var(--line);\n background: none;\n color: var(--ink);\n}\n\n/* The other choice beside a page's one action, such as Cancel. */\na.button.secondary {\n border-color: var(--line);\n background: none;\n color: var(--ink);\n}\n\n/* A page's action and its alternative, side by side. */\n.choice {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n}\n.choice form {\n margin: 0;\n}\n\n/* Ways to verify: one full-width button per method, none filled over the others. */\n.choices {\n display: grid;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n}\n.choices form {\n margin: 0;\n}\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 {\n border-color: var(--muted);\n}\n\n/* A provider's mark before a button's label, at the height of the letters. */\nbutton .provider {\n width: 1rem;\n height: 1rem;\n margin-right: 0.5rem;\n vertical-align: -0.1875rem;\n}\n\n/* A control that ends something. Filled where it is the page's one action. */\nbutton.danger {\n border-color: var(--warn);\n background: var(--warn);\n}\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 {\n position: relative;\n margin-bottom: 1rem;\n}\n\n.state {\n position: absolute;\n top: 0.75rem;\n right: 0.875rem;\n display: flex;\n align-items: center;\n gap: 0.375rem;\n height: 1.625rem;\n padding: 0 0.75rem 0 0.5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--surface);\n color: var(--ink);\n font-size: 0.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 {\n flex-shrink: 0;\n width: 1rem;\n height: 1rem;\n}\n.verdict.pending {\n opacity: 0.45;\n}\n.how .verdict {\n width: 0.875rem;\n height: 0.875rem;\n margin-right: 0.3125rem;\n vertical-align: -0.125rem;\n}\n/* Cropped to its strokes and taller than the heading's letters, centred beside them. */\nh1:has(.verdict) {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n}\nh1 .verdict {\n width: auto;\n height: 2.25rem;\n}\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 {\n width: auto;\n height: 100%;\n}\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 {\n padding: 0.3125rem 0.5rem;\n text-box: trim-both cap alphabetic;\n }\n .state > span {\n text-box: trim-both cap alphabetic;\n }\n}\n\n/* Links that sit together on one line, wrapping where there is no room. */\n.links {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5rem 1.25rem;\n font-size: 0.875rem;\n}\n\n/* Under the account's id, with the facts about it rather than the controls. */\n.evidence {\n margin: 0.375rem 0 0;\n font-size: 0.8125rem;\n}\n\n.connection dl {\n margin-bottom: 0.75rem;\n}\n.connection .side > :last-child {\n margin-bottom: 0;\n}\n.connection pre {\n margin: 0.75rem 0;\n background: var(--surface);\n}\n\n/* The embed snippet is copied once, so it stays closed until asked for. */\n.embed summary {\n width: fit-content;\n cursor: pointer;\n}\n.embed summary:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 1px;\n}\n.embed pre {\n margin-top: 0;\n}\n.connection .actions {\n margin-top: 0.875rem;\n}\n\n/* The buttons for one connection in a row, wrapping where there is no room. */\n.actions {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n.actions form {\n display: contents;\n}\n\n.actions button,\n.actions a.button {\n margin: 0;\n padding: 0.3125rem 0.75rem;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n font-size: 0.8125rem;\n}\n\n.actions button:hover,\n.actions a.button:hover {\n border-color: var(--muted);\n}\n\n/* The controls that cannot be undone, together at the foot of the card. */\n.ending {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 0.5rem;\n margin-top: 0.75rem;\n padding-top: 0.75rem;\n border-top: 1px solid var(--line);\n}\n.ending form {\n margin: 0;\n}\n\n.ending button.danger {\n padding: 0.3125rem 0.75rem;\n background: var(--surface);\n color: var(--warn);\n font-size: 0.8125rem;\n}\n\nfieldset {\n margin: 0 0 1.25rem;\n padding: 1rem 1.125rem 0.25rem;\n border: 1px solid var(--line);\n border-radius: 8px;\n}\n\nlegend {\n padding: 0 0.375rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n\nfieldset label {\n display: flex;\n align-items: baseline;\n gap: 0.5rem;\n color: var(--ink);\n font-size: 0.9375rem;\n}\n\nfieldset input {\n width: auto;\n margin: 0;\n}\n\nfieldset p {\n margin: 0.25rem 0 0.875rem 1.5rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n";
|
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 --warn: #a4442b;\n --caution: #b07a12;\n --syntax-tag: #245f43;\n --syntax-attr: #8a5a0b;\n --syntax-string: #2b5f8a;\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 --caution: #d9a843;\n --syntax-tag: #7fc4a2;\n --syntax-attr: #d9a843;\n --syntax-string: #8fbbe0;\n }\n}\n\n* {\n box-sizing: border-box;\n}\n\nbody {\n margin: 0;\n padding: 0 1rem 4rem;\n background: var(--surface);\n color: var(--ink);\n font:\n 16px/1.65 system-ui,\n -apple-system,\n 'Segoe UI',\n 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: -0.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 {\n margin-bottom: 1.25rem;\n padding-bottom: 0;\n border-bottom: none;\n}\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) {\n display: flex;\n min-height: 100vh;\n min-height: 100dvh;\n}\nmain.single {\n width: 100%;\n max-width: 24rem;\n margin: auto;\n padding-bottom: 3rem;\n}\n\nh2 {\n margin: 2rem 0 0.75rem;\n font-size: 1.125rem;\n font-weight: 620;\n letter-spacing: -0.01em;\n}\n\nh3 {\n margin: 0 0 0.5rem;\n font-size: 1rem;\n font-weight: 620;\n}\n\np {\n margin: 0 0 1rem;\n overflow-wrap: break-word;\n}\n\na {\n color: var(--accent);\n text-underline-offset: 3px;\n overflow-wrap: anywhere;\n}\n\na:hover {\n text-decoration-thickness: 2px;\n}\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: 0.75rem 5.25rem 0.75rem 0.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 {\n font:\n 0.8125rem/1.6 ui-monospace,\n SFMono-Regular,\n Menlo,\n monospace;\n}\n\n/* Highlighted HTML, marked up by highlight.js. Brackets stay muted so the names read first. */\n.hljs-tag {\n color: var(--muted);\n}\n.hljs-name {\n color: var(--syntax-tag);\n}\n.hljs-attr {\n color: var(--syntax-attr);\n}\n.hljs-string {\n color: var(--syntax-string);\n}\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: 0.5rem;\n right: 0.5rem;\n padding: 0.1875rem 0.5rem;\n border: 1px solid var(--line);\n border-radius: 5px;\n background: var(--surface);\n color: var(--muted);\n font:\n 500 0.75rem/1.5 system-ui,\n sans-serif;\n user-select: none;\n}\n\n.copy:hover {\n color: var(--ink);\n border-color: var(--muted);\n}\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: 0.75rem;\n padding: 0.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) {\n max-width: 54rem;\n}\n\n.accounts {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n gap: 0.75rem;\n margin-bottom: 0.75rem;\n}\n\n.accounts .side {\n position: relative;\n display: flex;\n flex-direction: column;\n margin-bottom: 0;\n}\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: 0.75rem;\n right: 0.875rem;\n padding: 0.0625rem 0.5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n color: var(--muted);\n font-size: 0.6875rem;\n font-weight: 550;\n}\n\n.listing.preferred {\n border-color: var(--accent);\n color: var(--accent);\n}\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 {\n margin-top: auto;\n}\n.accounts .rest {\n display: flex;\n flex-direction: column;\n}\n.accounts .rest > p:last-of-type {\n margin-bottom: 0.625rem;\n}\n\n/*\n * Two across, with the subject one card wide and centred above them. Every card on the\n * page is then the same width.\n */\n@media (min-width: 48rem) {\n .accounts {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n /* Each card is three rows of the grid: who, its verdict, and the rest. A row is as tall\n as the taller card's part, so the verdicts and times of neighbouring cards line up. */\n .accounts > .side {\n display: grid;\n grid-row: span 3;\n grid-template-rows: subgrid;\n row-gap: 0;\n }\n .subject {\n width: calc(50% - 0.375rem);\n margin-inline: auto;\n text-align: center;\n }\n .subject .name {\n justify-content: center;\n }\n}\n\n/* Between a subject and its accounts on the page that lists them. */\n.joiner {\n display: block;\n width: 1.25rem;\n height: 1.25rem;\n margin: 0 auto 0.75rem;\n color: var(--muted);\n}\n\n.who {\n margin: 0 0 0.125rem;\n color: var(--muted);\n font-size: 0.75rem;\n font-weight: 550;\n letter-spacing: 0.02em;\n}\n\n/* A provider's mark before its name, at the height of the letters. */\n.who .provider {\n display: inline-block;\n width: 0.875rem;\n height: 0.875rem;\n margin-right: 0.375rem;\n vertical-align: -0.125rem;\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 {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 0 1rem;\n margin: 0 0 0 -4px;\n padding-left: 4px;\n overflow-x: clip;\n font-size: 1.0625rem;\n font-weight: 620;\n letter-spacing: -0.01em;\n}\n\n/* The account's creation date, after the name and a dot. It wraps under a long name. */\n.name .since {\n position: relative;\n color: var(--muted);\n font-size: 0.8125rem;\n font-weight: 400;\n letter-spacing: 0;\n white-space: nowrap;\n}\n\n/* The dot sits in the gap before the date. */\n.name .since::before {\n content: '\u00B7';\n position: absolute;\n left: -0.5rem;\n transform: translateX(-50%);\n}\n\n/* The name is the card's subject before it is a link, so the rule is drawn on hover. */\n.name a {\n text-decoration: none;\n}\n\n.name a:hover {\n text-decoration: underline;\n}\n\n.reference,\n.how {\n margin: 0;\n color: var(--muted);\n font-size: 0.8125rem;\n line-height: 1.5;\n}\n\n.identity + .how {\n margin-top: 0.5rem;\n}\n.fingerprint {\n font-family: ui-monospace, monospace;\n text-wrap: balance;\n}\n.fingerprint span {\n display: inline-block;\n margin-right: 0.5ch;\n}\n.how.additional {\n padding-left: 0.75rem;\n}\n/* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n.how .signed-mark {\n display: inline-block;\n margin-left: 0.3125rem;\n padding: 0 0.375rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n font-size: 0.625rem;\n font-weight: 550;\n line-height: 1.3;\n text-decoration: none;\n vertical-align: 0.0625rem;\n}\n\n.fine {\n color: var(--muted);\n font-size: 0.8125rem;\n line-height: 1.55;\n margin-bottom: 0.625rem;\n}\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 {\n font-size: 0.875rem;\n line-height: 1.55;\n margin-bottom: 0.875rem;\n}\n/* Arrived at from a badge's \"signed\" link, so the reader is shown where they landed. */\n.signed:target {\n margin-inline: -0.75rem;\n padding: 0.625rem 0.75rem;\n border-radius: 8px;\n background: var(--raised);\n}\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: 0.3rem 1.5rem;\n margin: 1.25rem 0;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n font-size: 0.8125rem;\n}\n\ndt {\n color: var(--muted);\n}\n\ndd {\n margin: 0;\n font-variant-numeric: tabular-nums;\n}\n\n/* Inside a card the rule above it is the card's own edge, so the list needs none. */\nsection dl {\n margin: 0.5rem 0 1rem;\n padding-top: 0;\n border-top: 0;\n}\n\n/* Right under the heading, the heading's own rule is the one above it. */\nh1 + dl {\n padding-top: 0;\n border-top: 0;\n}\n.side dl {\n margin: 0.625rem 0 0;\n padding-top: 0.625rem;\n}\n\n/* The links under a record, ruled off from the record and its disclaimer. */\nfooter {\n margin-top: 1.25rem;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n}\nfooter > :last-child {\n margin-bottom: 0;\n}\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 {\n margin-bottom: 0;\n}\n\n/* What holds a control that cannot be undone. */\nsection.danger,\nsection.caution {\n margin-top: 2rem;\n}\nsection.danger {\n border-color: var(--warn);\n}\n\n/* What holds a control that breaks something for a while, but can be recovered from. */\nsection.caution {\n border-color: var(--caution);\n}\nsection > h2:first-child {\n margin-top: 0;\n}\n\nform {\n margin: 0 0 1.5rem;\n}\n\nlabel {\n display: block;\n margin: 0 0 0.75rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n\ninput,\ntextarea {\n display: block;\n width: 100%;\n margin-top: 0.375rem;\n padding: 0.5rem 0.625rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--surface);\n color: var(--ink);\n font:\n 0.875rem/1.5 ui-monospace,\n SFMono-Regular,\n Menlo,\n monospace;\n}\n\n/* Height comes from the rows attribute, which differs per box. */\ntextarea {\n resize: vertical;\n}\n\ninput:focus-visible,\ntextarea:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 1px;\n}\n\nbutton {\n padding: 0.5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font:\n 500 0.9375rem/1.4 system-ui,\n -apple-system,\n 'Segoe UI',\n 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: 0.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: 0.9375rem;\n line-height: 1.4;\n text-decoration: none;\n}\n\nbutton + button {\n margin-left: 0.5rem;\n border-color: var(--line);\n background: none;\n color: var(--ink);\n}\n\n/* The other choice beside a page's one action, such as Cancel. */\na.button.secondary {\n border-color: var(--line);\n background: none;\n color: var(--ink);\n}\n\n/* A page's action and its alternative, side by side. */\n.choice {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n}\n.choice form {\n margin: 0;\n}\n\n/* Ways to verify: one full-width button per method, none filled over the others. */\n.choices {\n display: grid;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n}\n.choices form {\n margin: 0;\n}\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 {\n border-color: var(--muted);\n}\n\n/* A provider's mark before a button's label, at the height of the letters. */\nbutton .provider {\n width: 1rem;\n height: 1rem;\n margin-right: 0.5rem;\n vertical-align: -0.1875rem;\n}\n\n/* A control that ends something. Filled where it is the page's one action. */\nbutton.danger {\n border-color: var(--warn);\n background: var(--warn);\n}\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 {\n position: relative;\n margin-bottom: 1rem;\n}\n\n.state {\n position: absolute;\n top: 0.75rem;\n right: 0.875rem;\n display: flex;\n align-items: center;\n gap: 0.375rem;\n height: 1.625rem;\n padding: 0 0.75rem 0 0.5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--surface);\n color: var(--ink);\n font-size: 0.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 {\n flex-shrink: 0;\n width: 1rem;\n height: 1rem;\n}\n.verdict.pending {\n opacity: 0.45;\n}\n.how .verdict {\n width: 0.875rem;\n height: 0.875rem;\n margin-right: 0.3125rem;\n vertical-align: -0.125rem;\n}\n/* Cropped to its strokes and taller than the heading's letters, centred beside them. */\nh1:has(.verdict) {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n}\nh1 .verdict {\n width: auto;\n height: 2.25rem;\n}\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 {\n width: auto;\n height: 100%;\n}\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 {\n padding: 0.3125rem 0.5rem;\n text-box: trim-both cap alphabetic;\n }\n .state > span {\n text-box: trim-both cap alphabetic;\n }\n}\n\n/* Links that sit together on one line, wrapping where there is no room. */\n.links {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5rem 1.25rem;\n font-size: 0.875rem;\n}\n\n/* Under the account's id, with the facts about it rather than the controls. */\n.evidence {\n margin: 0.375rem 0 0;\n font-size: 0.8125rem;\n}\n\n.connection dl {\n margin-bottom: 0.75rem;\n}\n.connection .side > :last-child {\n margin-bottom: 0;\n}\n.connection pre {\n margin: 0.75rem 0;\n background: var(--surface);\n}\n.connection .actions {\n margin-top: 0.875rem;\n}\n\n/* The buttons for one connection in a row, wrapping where there is no room. */\n.actions {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n.actions form {\n display: contents;\n}\n\n.actions button,\n.actions a.button {\n margin: 0;\n padding: 0.3125rem 0.75rem;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n font-size: 0.8125rem;\n}\n\n.actions button:hover,\n.actions a.button:hover {\n border-color: var(--muted);\n}\n\n/* The controls that cannot be undone, together at the foot of the card. */\n.ending {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 0.5rem;\n margin-top: 0.75rem;\n padding-top: 0.75rem;\n border-top: 1px solid var(--line);\n}\n.ending form {\n margin: 0;\n}\n\n.ending button.danger {\n padding: 0.3125rem 0.75rem;\n background: var(--surface);\n color: var(--warn);\n font-size: 0.8125rem;\n}\n\nfieldset {\n margin: 0 0 1.25rem;\n padding: 1rem 1.125rem 0.25rem;\n border: 1px solid var(--line);\n border-radius: 8px;\n}\n\nlegend {\n padding: 0 0.375rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n\nfieldset label {\n display: flex;\n align-items: baseline;\n gap: 0.5rem;\n color: var(--ink);\n font-size: 0.9375rem;\n}\n\nfieldset input {\n width: auto;\n margin: 0;\n}\n\nfieldset p {\n margin: 0.25rem 0 0.875rem 1.5rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\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 --caution: #b07a12;\n --syntax-tag: #245f43;\n --syntax-attr: #8a5a0b;\n --syntax-string: #2b5f8a;\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 --caution: #d9a843;\n --syntax-tag: #7fc4a2;\n --syntax-attr: #d9a843;\n --syntax-string: #8fbbe0;\n }\n}\n\n* {\n box-sizing: border-box;\n}\n\nbody {\n margin: 0;\n padding: 0 1rem 4rem;\n background: var(--surface);\n color: var(--ink);\n font:\n 16px/1.65 system-ui,\n -apple-system,\n 'Segoe UI',\n 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: -0.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 {\n margin-bottom: 1.25rem;\n padding-bottom: 0;\n border-bottom: none;\n}\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) {\n display: flex;\n min-height: 100vh;\n min-height: 100dvh;\n}\nmain.single {\n width: 100%;\n max-width: 24rem;\n margin: auto;\n padding-bottom: 3rem;\n}\n\nh2 {\n margin: 2rem 0 0.75rem;\n font-size: 1.125rem;\n font-weight: 620;\n letter-spacing: -0.01em;\n}\n\nh3 {\n margin: 0 0 0.5rem;\n font-size: 1rem;\n font-weight: 620;\n}\n\np {\n margin: 0 0 1rem;\n overflow-wrap: break-word;\n}\n\na {\n color: var(--accent);\n text-underline-offset: 3px;\n overflow-wrap: anywhere;\n}\n\na:hover {\n text-decoration-thickness: 2px;\n}\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: 0.75rem 5.25rem 0.75rem 0.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 {\n font:\n 0.8125rem/1.6 ui-monospace,\n SFMono-Regular,\n Menlo,\n monospace;\n}\n\n/* Highlighted HTML, marked up by highlight.js. Brackets stay muted so the names read first. */\n.hljs-tag {\n color: var(--muted);\n}\n.hljs-name {\n color: var(--syntax-tag);\n}\n.hljs-attr {\n color: var(--syntax-attr);\n}\n.hljs-string {\n color: var(--syntax-string);\n}\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: 0.5rem;\n right: 0.5rem;\n padding: 0.1875rem 0.5rem;\n border: 1px solid var(--line);\n border-radius: 5px;\n background: var(--surface);\n color: var(--muted);\n font:\n 500 0.75rem/1.5 system-ui,\n sans-serif;\n user-select: none;\n}\n\n.copy:hover {\n color: var(--ink);\n border-color: var(--muted);\n}\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: 0.75rem;\n padding: 0.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) {\n max-width: 54rem;\n}\n\n.accounts {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n gap: 0.75rem;\n margin-bottom: 0.75rem;\n}\n\n.accounts .side {\n position: relative;\n display: flex;\n flex-direction: column;\n margin-bottom: 0;\n}\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: 0.75rem;\n right: 0.875rem;\n padding: 0.0625rem 0.5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n color: var(--muted);\n font-size: 0.6875rem;\n font-weight: 550;\n}\n\n.listing.preferred {\n border-color: var(--accent);\n color: var(--accent);\n}\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 {\n margin-top: auto;\n}\n.accounts .rest {\n display: flex;\n flex-direction: column;\n}\n.accounts .rest > p:last-of-type {\n margin-bottom: 0.625rem;\n}\n\n/*\n * Two across, with the subject one card wide and centred above them. Every card on the\n * page is then the same width.\n */\n@media (min-width: 48rem) {\n .accounts {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n /* Each card is three rows of the grid: who, its verdict, and the rest. A row is as tall\n as the taller card's part, so the verdicts and times of neighbouring cards line up. */\n .accounts > .side {\n display: grid;\n grid-row: span 3;\n grid-template-rows: subgrid;\n row-gap: 0;\n }\n .subject {\n width: calc(50% - 0.375rem);\n margin-inline: auto;\n text-align: center;\n }\n .subject .name {\n justify-content: center;\n }\n}\n\n/* Between a subject and its accounts on the page that lists them. */\n.joiner {\n display: block;\n width: 1.25rem;\n height: 1.25rem;\n margin: 0 auto 0.75rem;\n color: var(--muted);\n}\n\n.who {\n margin: 0 0 0.125rem;\n color: var(--muted);\n font-size: 0.75rem;\n font-weight: 550;\n letter-spacing: 0.02em;\n}\n\n/* A provider's mark before its name, at the height of the letters. */\n.who .provider {\n display: inline-block;\n width: 0.875rem;\n height: 0.875rem;\n margin-right: 0.375rem;\n vertical-align: -0.125rem;\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 {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 0 1rem;\n margin: 0 0 0 -4px;\n padding-left: 4px;\n overflow-x: clip;\n font-size: 1.0625rem;\n font-weight: 620;\n letter-spacing: -0.01em;\n}\n\n/* The account's creation date, after the name and a dot. It wraps under a long name. */\n.name .since {\n position: relative;\n color: var(--muted);\n font-size: 0.8125rem;\n font-weight: 400;\n letter-spacing: 0;\n white-space: nowrap;\n}\n\n/* The dot sits in the gap before the date. */\n.name .since::before {\n content: '\u00B7';\n position: absolute;\n left: -0.5rem;\n transform: translateX(-50%);\n}\n\n/* The name is the card's subject before it is a link, so the rule is drawn on hover. */\n.name a {\n text-decoration: none;\n}\n\n.name a:hover {\n text-decoration: underline;\n}\n\n.reference,\n.how {\n margin: 0;\n color: var(--muted);\n font-size: 0.8125rem;\n line-height: 1.5;\n}\n\n.identity + .how {\n margin-top: 0.5rem;\n}\n.fingerprint {\n font-family: ui-monospace, monospace;\n text-wrap: balance;\n}\n.fingerprint span {\n display: inline-block;\n margin-right: 0.5ch;\n}\n.how.additional {\n padding-left: 0.75rem;\n}\n/* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n.how .signed-mark {\n display: inline-block;\n margin-left: 0.3125rem;\n padding: 0 0.375rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n font-size: 0.625rem;\n font-weight: 550;\n line-height: 1.3;\n text-decoration: none;\n vertical-align: 0.0625rem;\n}\n\n.fine {\n color: var(--muted);\n font-size: 0.8125rem;\n line-height: 1.55;\n margin-bottom: 0.625rem;\n}\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 {\n font-size: 0.875rem;\n line-height: 1.55;\n margin-bottom: 0.875rem;\n}\n/* Arrived at from a badge's \"signed\" link, so the reader is shown where they landed. */\n.signed:target {\n margin-inline: -0.75rem;\n padding: 0.625rem 0.75rem;\n border-radius: 8px;\n background: var(--raised);\n}\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: 0.3rem 1.5rem;\n margin: 1.25rem 0;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n font-size: 0.8125rem;\n}\n\ndt {\n color: var(--muted);\n}\n\ndd {\n margin: 0;\n font-variant-numeric: tabular-nums;\n}\n\n/* Inside a card the rule above it is the card's own edge, so the list needs none. */\nsection dl {\n margin: 0.5rem 0 1rem;\n padding-top: 0;\n border-top: 0;\n}\n\n/* Right under the heading, the heading's own rule is the one above it. */\nh1 + dl {\n padding-top: 0;\n border-top: 0;\n}\n.side dl {\n margin: 0.625rem 0 0;\n padding-top: 0.625rem;\n}\n\n/* The links under a record, ruled off from the record and its disclaimer. */\nfooter {\n margin-top: 1.25rem;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n}\nfooter > :last-child {\n margin-bottom: 0;\n}\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 {\n margin-bottom: 0;\n}\n\n/* What holds a control that cannot be undone. */\nsection.danger,\nsection.caution {\n margin-top: 2rem;\n}\nsection.danger {\n border-color: var(--warn);\n}\n\n/* What holds a control that breaks something for a while, but can be recovered from. */\nsection.caution {\n border-color: var(--caution);\n}\nsection > h2:first-child {\n margin-top: 0;\n}\n\nform {\n margin: 0 0 1.5rem;\n}\n\nlabel {\n display: block;\n margin: 0 0 0.75rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n\ninput,\ntextarea {\n display: block;\n width: 100%;\n margin-top: 0.375rem;\n padding: 0.5rem 0.625rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--surface);\n color: var(--ink);\n font:\n 0.875rem/1.5 ui-monospace,\n SFMono-Regular,\n Menlo,\n monospace;\n}\n\n/* Height comes from the rows attribute, which differs per box. */\ntextarea {\n resize: vertical;\n}\n\ninput:focus-visible,\ntextarea:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 1px;\n}\n\nbutton {\n padding: 0.5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font:\n 500 0.9375rem/1.4 system-ui,\n -apple-system,\n 'Segoe UI',\n 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: 0.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: 0.9375rem;\n line-height: 1.4;\n text-decoration: none;\n}\n\nbutton + button {\n margin-left: 0.5rem;\n border-color: var(--line);\n background: none;\n color: var(--ink);\n}\n\n/* The other choice beside a page's one action, such as Cancel. */\na.button.secondary {\n border-color: var(--line);\n background: none;\n color: var(--ink);\n}\n\n/* A page's action and its alternative, side by side. */\n.choice {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n}\n.choice form {\n margin: 0;\n}\n\n/* Ways to verify: one full-width button per method, none filled over the others. */\n.choices {\n display: grid;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n}\n.choices form {\n margin: 0;\n}\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 {\n border-color: var(--muted);\n}\n\n/* A provider's mark before a button's label, at the height of the letters. */\nbutton .provider {\n width: 1rem;\n height: 1rem;\n margin-right: 0.5rem;\n vertical-align: -0.1875rem;\n}\n\n/* A control that ends something. Filled where it is the page's one action. */\nbutton.danger {\n border-color: var(--warn);\n background: var(--warn);\n}\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 {\n position: relative;\n margin-bottom: 1rem;\n}\n\n.state {\n position: absolute;\n top: 0.75rem;\n right: 0.875rem;\n display: flex;\n align-items: center;\n gap: 0.375rem;\n height: 1.625rem;\n padding: 0 0.75rem 0 0.5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--surface);\n color: var(--ink);\n font-size: 0.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 {\n flex-shrink: 0;\n width: 1rem;\n height: 1rem;\n}\n.verdict.pending {\n opacity: 0.45;\n}\n.how .verdict {\n width: 0.875rem;\n height: 0.875rem;\n margin-right: 0.3125rem;\n vertical-align: -0.125rem;\n}\n/* Cropped to its strokes and taller than the heading's letters, centred beside them. */\nh1:has(.verdict) {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n}\nh1 .verdict {\n width: auto;\n height: 2.25rem;\n}\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 {\n width: auto;\n height: 100%;\n}\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 {\n padding: 0.3125rem 0.5rem;\n text-box: trim-both cap alphabetic;\n }\n .state > span {\n text-box: trim-both cap alphabetic;\n }\n}\n\n/* Links that sit together on one line, wrapping where there is no room. */\n.links {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5rem 1.25rem;\n font-size: 0.875rem;\n}\n\n/* Under the account's id, with the facts about it rather than the controls. */\n.evidence {\n margin: 0.375rem 0 0;\n font-size: 0.8125rem;\n}\n\n.connection dl {\n margin-bottom: 0.75rem;\n}\n.connection .side > :last-child {\n margin-bottom: 0;\n}\n.connection pre {\n margin: 0.75rem 0;\n background: var(--surface);\n}\n\n/* The embed snippet is copied once, so it stays closed until asked for. */\n.embed summary {\n width: fit-content;\n cursor: pointer;\n}\n.embed summary:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 1px;\n}\n.embed pre {\n margin-top: 0;\n}\n.connection .actions {\n margin-top: 0.875rem;\n}\n\n/* The buttons for one connection in a row, wrapping where there is no room. */\n.actions {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n.actions form {\n display: contents;\n}\n\n.actions button,\n.actions a.button {\n margin: 0;\n padding: 0.3125rem 0.75rem;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n font-size: 0.8125rem;\n}\n\n.actions button:hover,\n.actions a.button:hover {\n border-color: var(--muted);\n}\n\n/* The controls that cannot be undone, together at the foot of the card. */\n.ending {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 0.5rem;\n margin-top: 0.75rem;\n padding-top: 0.75rem;\n border-top: 1px solid var(--line);\n}\n.ending form {\n margin: 0;\n}\n\n.ending button.danger {\n padding: 0.3125rem 0.75rem;\n background: var(--surface);\n color: var(--warn);\n font-size: 0.8125rem;\n}\n\nfieldset {\n margin: 0 0 1.25rem;\n padding: 1rem 1.125rem 0.25rem;\n border: 1px solid var(--line);\n border-radius: 8px;\n}\n\nlegend {\n padding: 0 0.375rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\n\nfieldset label {\n display: flex;\n align-items: baseline;\n gap: 0.5rem;\n color: var(--ink);\n font-size: 0.9375rem;\n}\n\nfieldset input {\n width: auto;\n margin: 0;\n}\n\nfieldset p {\n margin: 0.25rem 0 0.875rem 1.5rem;\n color: var(--muted);\n font-size: 0.8125rem;\n}\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/site/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Evidence, type LocalKind, type Visibility } from '../core/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* For a site that uses a hosted Verily instance instead of running its own. The site's
|
|
4
4
|
* backend does three things, and this is all of them: it vouches to the instance for its
|
|
@@ -120,10 +120,13 @@ export declare function createSiteClient(options: SiteClientOptions): {
|
|
|
120
120
|
* them with. Only this site can read them. Every id asked for is in the answer, with an
|
|
121
121
|
* empty list for a subject who has none. Revoked and expired records are included, so
|
|
122
122
|
* show one only when its `status` is `verified`. With `cacheMs` set, an answer read
|
|
123
|
-
* within it is reused unless `fresh` asks for a new one.
|
|
123
|
+
* within it is reused unless `fresh` asks for a new one. With `maxAgeMs` set, a
|
|
124
|
+
* verified record whose account was last proved longer ago than that reads as
|
|
125
|
+
* `unconfirmed`.
|
|
124
126
|
*/
|
|
125
|
-
connections(ids: string[], { fresh }?: {
|
|
127
|
+
connections(ids: string[], { fresh, maxAgeMs }?: {
|
|
126
128
|
fresh?: boolean;
|
|
129
|
+
maxAgeMs?: number;
|
|
127
130
|
}): Promise<Record<string, Evidence[]>>;
|
|
128
131
|
/**
|
|
129
132
|
* For the return endpoint: the result a user came back with, if the instance signed it
|
package/dist/site/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withMaxAge } from '../core/index.js';
|
|
1
2
|
/** How many subjects the instance reads in one request. */
|
|
2
3
|
const maxReadIds = 50;
|
|
3
4
|
const encoder = new TextEncoder();
|
|
@@ -127,9 +128,11 @@ export function createSiteClient(options) {
|
|
|
127
128
|
* them with. Only this site can read them. Every id asked for is in the answer, with an
|
|
128
129
|
* empty list for a subject who has none. Revoked and expired records are included, so
|
|
129
130
|
* show one only when its `status` is `verified`. With `cacheMs` set, an answer read
|
|
130
|
-
* within it is reused unless `fresh` asks for a new one.
|
|
131
|
+
* within it is reused unless `fresh` asks for a new one. With `maxAgeMs` set, a
|
|
132
|
+
* verified record whose account was last proved longer ago than that reads as
|
|
133
|
+
* `unconfirmed`.
|
|
131
134
|
*/
|
|
132
|
-
async connections(ids, { fresh = false } = {}) {
|
|
135
|
+
async connections(ids, { fresh = false, maxAgeMs } = {}) {
|
|
133
136
|
// No prototype, so an id such as `__proto__` is an entry like any other.
|
|
134
137
|
const answer = Object.create(null);
|
|
135
138
|
const found = Object.create(null);
|
|
@@ -169,7 +172,7 @@ export function createSiteClient(options) {
|
|
|
169
172
|
}
|
|
170
173
|
}
|
|
171
174
|
for (const [id, name] of names)
|
|
172
|
-
answer[id] = found[name] ?? [];
|
|
175
|
+
answer[id] = (found[name] ?? []).map((e) => withMaxAge(e, maxAgeMs, now));
|
|
173
176
|
return answer;
|
|
174
177
|
},
|
|
175
178
|
/**
|
package/dist/verily.js
CHANGED
|
@@ -9577,6 +9577,7 @@ var Verily = (() => {
|
|
|
9577
9577
|
Refused: () => Refused,
|
|
9578
9578
|
accountsOf: () => accountsOf,
|
|
9579
9579
|
attestationLabel: () => attestationLabel,
|
|
9580
|
+
authenticatedEarlier: () => authenticatedEarlier,
|
|
9580
9581
|
dialogPath: () => dialogPath,
|
|
9581
9582
|
dialogRoute: () => dialogRoute,
|
|
9582
9583
|
expired: () => expired,
|
|
@@ -9593,17 +9594,20 @@ var Verily = (() => {
|
|
|
9593
9594
|
isRedirectProvider: () => isRedirectProvider,
|
|
9594
9595
|
lastProved: () => lastProved,
|
|
9595
9596
|
localSide: () => localSide,
|
|
9597
|
+
parseMaxAge: () => parseMaxAge,
|
|
9596
9598
|
place: () => place,
|
|
9597
9599
|
presentConnections: () => presentConnections,
|
|
9598
9600
|
proofTitle: () => proofTitle,
|
|
9599
9601
|
providerMethod: () => providerMethod,
|
|
9602
|
+
recent: () => recent,
|
|
9600
9603
|
routeOf: () => routeOf,
|
|
9601
9604
|
routePath: () => routePath,
|
|
9602
9605
|
routes: () => routes,
|
|
9603
9606
|
signedBy: () => signedBy,
|
|
9604
9607
|
status: () => status,
|
|
9605
9608
|
statusLabel: () => statusLabel,
|
|
9606
|
-
verifySigned: () => verifySigned
|
|
9609
|
+
verifySigned: () => verifySigned,
|
|
9610
|
+
withMaxAge: () => withMaxAge
|
|
9607
9611
|
});
|
|
9608
9612
|
|
|
9609
9613
|
// src/core/signed.ts
|
|
@@ -9753,6 +9757,21 @@ var Verily = (() => {
|
|
|
9753
9757
|
const main = evidence.attestations.external[0];
|
|
9754
9758
|
return main.artifactUrl ? main.confirmedAt : evidence.authenticatedAt;
|
|
9755
9759
|
}
|
|
9760
|
+
function authenticatedEarlier(evidence) {
|
|
9761
|
+
return evidence.approvedAt - evidence.authenticatedAt > 864e5;
|
|
9762
|
+
}
|
|
9763
|
+
function recent(evidence, maxAgeMs, now) {
|
|
9764
|
+
return now - lastProved(evidence) <= maxAgeMs;
|
|
9765
|
+
}
|
|
9766
|
+
function withMaxAge(evidence, maxAgeMs, now) {
|
|
9767
|
+
return maxAgeMs !== void 0 && evidence.status === "verified" && !recent(evidence, maxAgeMs, now) ? { ...evidence, status: "unconfirmed" } : evidence;
|
|
9768
|
+
}
|
|
9769
|
+
function parseMaxAge(text) {
|
|
9770
|
+
const match = /^\s*(\d+)\s*([smhd]?)\s*$/.exec(text ?? "");
|
|
9771
|
+
if (!match) return void 0;
|
|
9772
|
+
const unit = { "": 1e3, s: 1e3, m: 6e4, h: 36e5, d: 864e5 }[match[2]];
|
|
9773
|
+
return Number(match[1]) * unit;
|
|
9774
|
+
}
|
|
9756
9775
|
function statusLabel(evidence, now) {
|
|
9757
9776
|
if (evidence.status === "revoked") return "Revoked";
|
|
9758
9777
|
if (evidence.status === "retired") return "Retired";
|
|
@@ -10987,7 +11006,7 @@ ${claims(evidence)}`;
|
|
|
10987
11006
|
}
|
|
10988
11007
|
|
|
10989
11008
|
// src/version.ts
|
|
10990
|
-
var version = "v0.
|
|
11009
|
+
var version = "v0.10.0";
|
|
10991
11010
|
|
|
10992
11011
|
// src/browser/evidence-dialog.ts
|
|
10993
11012
|
function stampLink() {
|
|
@@ -11412,7 +11431,11 @@ ${claims(evidence)}`;
|
|
|
11412
11431
|
]);
|
|
11413
11432
|
}
|
|
11414
11433
|
const main = evidence.attestations.external[0];
|
|
11415
|
-
if (
|
|
11434
|
+
if (!retired) {
|
|
11435
|
+
if (main.artifactUrl) dateRows.push(["Last checked", main.confirmedAt]);
|
|
11436
|
+
else if (authenticatedEarlier(evidence))
|
|
11437
|
+
dateRows.push(["Authenticated", evidence.authenticatedAt]);
|
|
11438
|
+
}
|
|
11416
11439
|
for (const [label, time] of dateRows) {
|
|
11417
11440
|
dates.append(node("dt", label), node("dd", moment(time)));
|
|
11418
11441
|
}
|
|
@@ -12228,7 +12251,7 @@ ${record2.external.id}`;
|
|
|
12228
12251
|
}
|
|
12229
12252
|
},
|
|
12230
12253
|
latestGroup.get(element),
|
|
12231
|
-
accounts,
|
|
12254
|
+
(records) => accounts(aged(element, records)),
|
|
12232
12255
|
managers.get(element),
|
|
12233
12256
|
openHosts.has(element)
|
|
12234
12257
|
);
|
|
@@ -12237,9 +12260,20 @@ ${record2.external.id}`;
|
|
|
12237
12260
|
var stackedHosts = /* @__PURE__ */ new WeakSet();
|
|
12238
12261
|
var quietHosts = /* @__PURE__ */ new WeakSet();
|
|
12239
12262
|
var openHosts = /* @__PURE__ */ new WeakSet();
|
|
12263
|
+
var maxAges = /* @__PURE__ */ new WeakMap();
|
|
12264
|
+
function bound(element, maxAgeMs) {
|
|
12265
|
+
if (maxAgeMs === void 0) maxAges.delete(element);
|
|
12266
|
+
else maxAges.set(element, maxAgeMs);
|
|
12267
|
+
}
|
|
12268
|
+
function aged(element, records) {
|
|
12269
|
+
const maxAgeMs = maxAges.get(element);
|
|
12270
|
+
const now = Date.now();
|
|
12271
|
+
return records.map((e2) => withMaxAge(e2, maxAgeMs, now));
|
|
12272
|
+
}
|
|
12240
12273
|
var stackedAccounts = 4;
|
|
12241
12274
|
var peekedAccounts = 8;
|
|
12242
|
-
function arrange(element, { stacked, peek, detailsOpen }) {
|
|
12275
|
+
function arrange(element, { stacked, peek, detailsOpen, maxAgeMs }) {
|
|
12276
|
+
bound(element, maxAgeMs);
|
|
12243
12277
|
if (detailsOpen) openHosts.add(element);
|
|
12244
12278
|
else openHosts.delete(element);
|
|
12245
12279
|
if (stacked) stackedHosts.add(element);
|
|
@@ -12259,7 +12293,7 @@ ${record2.external.id}`;
|
|
|
12259
12293
|
}));
|
|
12260
12294
|
}
|
|
12261
12295
|
function presentGroup(element, records, load2) {
|
|
12262
|
-
const ordered = accounts(records.map(standing));
|
|
12296
|
+
const ordered = accounts(aged(element, records.map(standing)));
|
|
12263
12297
|
const verified = ordered.filter(live);
|
|
12264
12298
|
const lead = verified[0] ?? ordered[0];
|
|
12265
12299
|
latestGroup.set(element, records);
|
|
@@ -12341,8 +12375,8 @@ ${record2.external.id}`;
|
|
|
12341
12375
|
});
|
|
12342
12376
|
if (!response.ok) throw new Error("Verily request unavailable");
|
|
12343
12377
|
const answer = await response.json();
|
|
12344
|
-
const
|
|
12345
|
-
if (
|
|
12378
|
+
const bound2 = response.headers.get("X-Verily-Flow");
|
|
12379
|
+
if (bound2 && record(answer) && typeof answer.id === "string") bindings.set(answer.id, bound2);
|
|
12346
12380
|
return answer;
|
|
12347
12381
|
}
|
|
12348
12382
|
async function request(path, data) {
|
|
@@ -12491,7 +12525,12 @@ ${record2.external.id}`;
|
|
|
12491
12525
|
if (!validEvidence(data)) throw new Error("Invalid evidence response");
|
|
12492
12526
|
return data;
|
|
12493
12527
|
},
|
|
12494
|
-
async mountBadge(element, {
|
|
12528
|
+
async mountBadge(element, {
|
|
12529
|
+
connectionId,
|
|
12530
|
+
evidence,
|
|
12531
|
+
maxAgeMs
|
|
12532
|
+
}) {
|
|
12533
|
+
bound(element, maxAgeMs);
|
|
12495
12534
|
if (!evidence && !badgeShown(element)) renderBadgePending(element);
|
|
12496
12535
|
const current = begin(element);
|
|
12497
12536
|
try {
|
|
@@ -12508,7 +12547,7 @@ ${record2.external.id}`;
|
|
|
12508
12547
|
return [fresh2];
|
|
12509
12548
|
});
|
|
12510
12549
|
const draw = () => {
|
|
12511
|
-
const badge = renderBadge(element, standing(e2));
|
|
12550
|
+
const badge = renderBadge(element, aged(element, [standing(e2)])[0]);
|
|
12512
12551
|
if (badge) opens(element, badge);
|
|
12513
12552
|
};
|
|
12514
12553
|
draw();
|
|
@@ -12527,7 +12566,11 @@ ${record2.external.id}`;
|
|
|
12527
12566
|
async mountBadges(element, { connectionIds, ...arrangement }) {
|
|
12528
12567
|
const ids = [...new Set(connectionIds)];
|
|
12529
12568
|
arrange(element, arrangement);
|
|
12530
|
-
if (ids.length === 1)
|
|
12569
|
+
if (ids.length === 1)
|
|
12570
|
+
return client.mountBadge(element, {
|
|
12571
|
+
connectionId: ids[0],
|
|
12572
|
+
maxAgeMs: arrangement.maxAgeMs
|
|
12573
|
+
});
|
|
12531
12574
|
if (!badgeShown(element)) renderBadgePending(element);
|
|
12532
12575
|
const read2 = async () => {
|
|
12533
12576
|
const answers = await Promise.allSettled(ids.map((id) => client.getConnection(id)));
|
|
@@ -12672,7 +12715,8 @@ ${record2.external.id}`;
|
|
|
12672
12715
|
"connect",
|
|
12673
12716
|
"stacked",
|
|
12674
12717
|
"peek",
|
|
12675
|
-
"details-open"
|
|
12718
|
+
"details-open",
|
|
12719
|
+
"max-age"
|
|
12676
12720
|
];
|
|
12677
12721
|
handed;
|
|
12678
12722
|
/**
|
|
@@ -12727,7 +12771,8 @@ ${record2.external.id}`;
|
|
|
12727
12771
|
const arrangement = {
|
|
12728
12772
|
stacked: this.getAttribute("stacked") !== null,
|
|
12729
12773
|
peek: this.getAttribute("peek") !== "off",
|
|
12730
|
-
detailsOpen: this.getAttribute("details-open") !== null
|
|
12774
|
+
detailsOpen: this.getAttribute("details-open") !== null,
|
|
12775
|
+
maxAgeMs: parseMaxAge(this.getAttribute("max-age"))
|
|
12731
12776
|
};
|
|
12732
12777
|
if (backendUrl && (handoffUrl !== null || this.getAttribute("connect") !== null)) {
|
|
12733
12778
|
const client = init({ backendUrl, handoffUrl: handoffUrl ?? void 0 });
|
|
@@ -12781,7 +12826,12 @@ ${record2.external.id}`;
|
|
|
12781
12826
|
renderBadgePending(this);
|
|
12782
12827
|
return;
|
|
12783
12828
|
}
|
|
12784
|
-
if (backendUrl)
|
|
12829
|
+
if (backendUrl)
|
|
12830
|
+
void init({ backendUrl }).mountBadge(this, {
|
|
12831
|
+
connectionId,
|
|
12832
|
+
evidence,
|
|
12833
|
+
maxAgeMs: arrangement.maxAgeMs
|
|
12834
|
+
});
|
|
12785
12835
|
}
|
|
12786
12836
|
}
|
|
12787
12837
|
);
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED