@bkazemi/verily 0.1.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/LICENSE +21 -0
- package/README.md +503 -0
- package/dist/browser/badge.d.ts +39 -0
- package/dist/browser/badge.js +440 -0
- package/dist/browser/connect-dialog.d.ts +83 -0
- package/dist/browser/connect-dialog.js +514 -0
- package/dist/browser/evidence-dialog.d.ts +59 -0
- package/dist/browser/evidence-dialog.js +541 -0
- package/dist/browser/index.d.ts +59 -0
- package/dist/browser/index.js +809 -0
- package/dist/browser/logo.d.ts +2 -0
- package/dist/browser/logo.js +17 -0
- package/dist/browser/mark.d.ts +19 -0
- package/dist/browser/mark.js +77 -0
- package/dist/browser/provider-mark.d.ts +6 -0
- package/dist/browser/provider-mark.js +99 -0
- package/dist/browser/signed.d.ts +40 -0
- package/dist/browser/signed.js +219 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.js +3 -0
- package/dist/core/index.d.ts +454 -0
- package/dist/core/index.js +128 -0
- package/dist/core/signed.d.ts +49 -0
- package/dist/core/signed.js +113 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/logo.d.ts +21 -0
- package/dist/logo.js +38 -0
- package/dist/postgres/index.d.ts +12 -0
- package/dist/postgres/index.js +76 -0
- package/dist/server/addresses.d.ts +2 -0
- package/dist/server/addresses.js +61 -0
- package/dist/server/body.d.ts +11 -0
- package/dist/server/body.js +49 -0
- package/dist/server/copy.d.ts +8 -0
- package/dist/server/copy.js +42 -0
- package/dist/server/discord.d.ts +7 -0
- package/dist/server/discord.js +69 -0
- package/dist/server/email-logo.d.ts +11 -0
- package/dist/server/email-logo.js +122 -0
- package/dist/server/email.d.ts +46 -0
- package/dist/server/email.js +145 -0
- package/dist/server/escape.d.ts +2 -0
- package/dist/server/escape.js +2 -0
- package/dist/server/github-gist.d.ts +11 -0
- package/dist/server/github-gist.js +68 -0
- package/dist/server/github.d.ts +7 -0
- package/dist/server/github.js +65 -0
- package/dist/server/index.d.ts +70 -0
- package/dist/server/index.js +862 -0
- package/dist/server/link.d.ts +64 -0
- package/dist/server/link.js +458 -0
- package/dist/server/openpgp.d.ts +49 -0
- package/dist/server/openpgp.js +158 -0
- package/dist/server/pgp.d.ts +29 -0
- package/dist/server/pgp.js +198 -0
- package/dist/server/public-fetch.d.ts +23 -0
- package/dist/server/public-fetch.js +62 -0
- package/dist/server/resend.d.ts +13 -0
- package/dist/server/resend.js +37 -0
- package/dist/server/service.d.ts +311 -0
- package/dist/server/service.js +1178 -0
- package/dist/server/signing.d.ts +16 -0
- package/dist/server/signing.js +56 -0
- package/dist/server/style.d.ts +17 -0
- package/dist/server/style.js +244 -0
- package/dist/server/wkd.d.ts +7 -0
- package/dist/server/wkd.js +65 -0
- package/dist/server/youtube.d.ts +11 -0
- package/dist/server/youtube.js +99 -0
- package/dist/site/index.d.ts +139 -0
- package/dist/site/index.js +239 -0
- package/dist/verily.js +11907 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.js +10 -0
- package/docs/licenses/gpl-3.0.txt +232 -0
- package/docs/licenses/octicons.txt +21 -0
- package/docs/licenses/openpgp.txt +172 -0
- package/docs/licenses/rubik.txt +93 -0
- package/docs/licenses/simple-icons.txt +8 -0
- package/package.json +86 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { logoPaths, logoViewBox } from '../logo.js';
|
|
2
|
+
/** The logotype, drawn as nodes: nothing in the badge sets innerHTML. */
|
|
3
|
+
export function verilyLogo() {
|
|
4
|
+
const namespace = 'http://www.w3.org/2000/svg';
|
|
5
|
+
const svg = document.createElementNS(namespace, 'svg');
|
|
6
|
+
svg.setAttribute('viewBox', logoViewBox);
|
|
7
|
+
svg.setAttribute('class', 'logo');
|
|
8
|
+
svg.setAttribute('role', 'img');
|
|
9
|
+
svg.setAttribute('aria-label', 'Verily');
|
|
10
|
+
for (const { d, fill } of logoPaths) {
|
|
11
|
+
const path = document.createElementNS(namespace, 'path');
|
|
12
|
+
path.setAttribute('d', d);
|
|
13
|
+
path.setAttribute('fill', fill);
|
|
14
|
+
svg.append(path);
|
|
15
|
+
}
|
|
16
|
+
return svg;
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How sure the mark is: `current` for a verification that holds, `inactive` for one that
|
|
3
|
+
* does not, and `pending` for one nobody has confirmed yet, which claims nothing and is
|
|
4
|
+
* drawn in the surrounding text colour.
|
|
5
|
+
*/
|
|
6
|
+
export type MarkTone = 'current' | 'inactive' | 'pending';
|
|
7
|
+
/**
|
|
8
|
+
* The check draws itself in from its left end once the verification holds: when a pill's
|
|
9
|
+
* mark first becomes `current`, and each time the dialog opens on a current one.
|
|
10
|
+
*/
|
|
11
|
+
export declare const markStyles = "\n @media (prefers-reduced-motion: no-preference) {\n .mark.current path:last-child { animation: verily-draw .8s cubic-bezier(.65, 0, .35, 1) .1s backwards; }\n }\n @keyframes verily-draw { from { stroke-dasharray: 0 176; } to { stroke-dasharray: 108 176; } }\n";
|
|
12
|
+
/** Repaints a mark in place, so answering a check never replaces the drawing. */
|
|
13
|
+
export declare function paintMark(svg: SVGSVGElement, tone: MarkTone): void;
|
|
14
|
+
/** V2: the green check fills its section of the symmetric red V. */
|
|
15
|
+
export declare function verificationMark(tone?: MarkTone): SVGSVGElement;
|
|
16
|
+
/** The clock on an expired record: a face, and hands at three o'clock. */
|
|
17
|
+
export declare const clockMark: () => SVGSVGElement;
|
|
18
|
+
/** The dash on a record that is revoked or cannot be shown. */
|
|
19
|
+
export declare const dashMark: () => SVGSVGElement;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const tones = {
|
|
2
|
+
current: ['#D3444C', '#149766'],
|
|
3
|
+
inactive: ['#149766', '#D3444C'],
|
|
4
|
+
pending: ['currentColor', 'currentColor'],
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* The check draws itself in from its left end once the verification holds: when a pill's
|
|
8
|
+
* mark first becomes `current`, and each time the dialog opens on a current one.
|
|
9
|
+
*/
|
|
10
|
+
export const markStyles = `
|
|
11
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
12
|
+
.mark.current path:last-child { animation: verily-draw .8s cubic-bezier(.65, 0, .35, 1) .1s backwards; }
|
|
13
|
+
}
|
|
14
|
+
@keyframes verily-draw { from { stroke-dasharray: 0 176; } to { stroke-dasharray: 108 176; } }
|
|
15
|
+
`;
|
|
16
|
+
/** Repaints a mark in place, so answering a check never replaces the drawing. */
|
|
17
|
+
export function paintMark(svg, tone) {
|
|
18
|
+
const colors = tones[tone];
|
|
19
|
+
svg.setAttribute('class', `mark ${tone}`);
|
|
20
|
+
for (const [layer, path] of [...svg.children].entries())
|
|
21
|
+
path.setAttribute('stroke', colors[layer] ?? colors[0]);
|
|
22
|
+
}
|
|
23
|
+
/** V2: the green check fills its section of the symmetric red V. */
|
|
24
|
+
export function verificationMark(tone = 'current') {
|
|
25
|
+
const namespace = 'http://www.w3.org/2000/svg';
|
|
26
|
+
const svg = document.createElementNS(namespace, 'svg');
|
|
27
|
+
svg.setAttribute('viewBox', '-4 -4 264 264');
|
|
28
|
+
svg.setAttribute('fill', 'none');
|
|
29
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
30
|
+
svg.setAttribute('focusable', 'false');
|
|
31
|
+
for (const layer of [0, 1]) {
|
|
32
|
+
const path = document.createElementNS(namespace, 'path');
|
|
33
|
+
path.setAttribute('d', 'M40 36 128 220 216 36');
|
|
34
|
+
path.setAttribute('stroke-width', '32');
|
|
35
|
+
path.setAttribute('stroke-linejoin', 'miter');
|
|
36
|
+
if (layer === 1) {
|
|
37
|
+
path.setAttribute('pathLength', '176');
|
|
38
|
+
path.setAttribute('stroke-dasharray', '108 176');
|
|
39
|
+
path.setAttribute('stroke-dashoffset', '-54');
|
|
40
|
+
}
|
|
41
|
+
svg.append(path);
|
|
42
|
+
}
|
|
43
|
+
paintMark(svg, tone);
|
|
44
|
+
return svg;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A small drawing inside a status circle. Drawn, not typed: a character sits where its
|
|
48
|
+
* font's metrics put it, which is never quite the middle of the circle around it, and
|
|
49
|
+
* differently so on each platform. A drawing is centred by its own geometry, in the
|
|
50
|
+
* surrounding colour. Each one is drawn about the middle of the same sixteen-unit box.
|
|
51
|
+
*/
|
|
52
|
+
function statusMark(name, shapes) {
|
|
53
|
+
const namespace = 'http://www.w3.org/2000/svg';
|
|
54
|
+
const svg = document.createElementNS(namespace, 'svg');
|
|
55
|
+
svg.setAttribute('viewBox', '0 0 16 16');
|
|
56
|
+
svg.setAttribute('fill', 'none');
|
|
57
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
58
|
+
svg.setAttribute('stroke-width', '1.6');
|
|
59
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
60
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
61
|
+
svg.setAttribute('class', `glyph ${name}`);
|
|
62
|
+
svg.setAttribute('focusable', 'false');
|
|
63
|
+
for (const [tag, attributes] of shapes) {
|
|
64
|
+
const shape = document.createElementNS(namespace, tag);
|
|
65
|
+
for (const [key, value] of Object.entries(attributes))
|
|
66
|
+
shape.setAttribute(key, value);
|
|
67
|
+
svg.append(shape);
|
|
68
|
+
}
|
|
69
|
+
return svg;
|
|
70
|
+
}
|
|
71
|
+
/** The clock on an expired record: a face, and hands at three o'clock. */
|
|
72
|
+
export const clockMark = () => statusMark('clock', [
|
|
73
|
+
['circle', { cx: '8', cy: '8', r: '6.2' }],
|
|
74
|
+
['path', { d: 'M8 4.6V8h2.6' }],
|
|
75
|
+
]);
|
|
76
|
+
/** The dash on a record that is revoked or cannot be shown. */
|
|
77
|
+
export const dashMark = () => statusMark('dash', [['path', { d: 'M4.4 8h7.2' }]]);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mark for a provider, or nothing where there is none. Returning nothing rather than
|
|
3
|
+
* the provider's name keeps this to one job: a caller that also writes the name would
|
|
4
|
+
* otherwise print it twice, and only the caller knows where the name belongs.
|
|
5
|
+
*/
|
|
6
|
+
export declare function providerMark(provider: string, method?: string): SVGSVGElement | undefined;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const namespace = 'http://www.w3.org/2000/svg';
|
|
2
|
+
function mark() {
|
|
3
|
+
const svg = document.createElementNS(namespace, 'svg');
|
|
4
|
+
svg.setAttribute('viewBox', '0 0 16 16');
|
|
5
|
+
svg.setAttribute('class', 'provider');
|
|
6
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
7
|
+
svg.setAttribute('focusable', 'false');
|
|
8
|
+
return svg;
|
|
9
|
+
}
|
|
10
|
+
/** GitHub mark from Primer Octicons (MIT); license in docs/licenses/octicons.txt. */
|
|
11
|
+
function github() {
|
|
12
|
+
const svg = mark();
|
|
13
|
+
const path = document.createElementNS(namespace, 'path');
|
|
14
|
+
svg.setAttribute('fill', 'currentColor');
|
|
15
|
+
path.setAttribute('d', 'M6.766 11.328c-2.063-.25-3.516-1.734-3.516-3.656 0-.781.281-1.625.75-2.188-.203-.515-.172-1.609.063-2.062.625-.078 1.468.25 1.968.703.594-.187 1.219-.281 1.985-.281.765 0 1.39.094 1.953.265.484-.437 1.344-.765 1.969-.687.218.422.25 1.515.046 2.047.5.593.766 1.39.766 2.203 0 1.922-1.453 3.375-3.547 3.64.531.344.89 1.094.89 1.954v1.625c0 .468.391.734.86.547C13.781 14.359 16 11.53 16 8.03 16 3.61 12.406 0 7.984 0 3.563 0 0 3.61 0 8.031a7.88 7.88 0 0 0 5.172 7.422c.422.156.828-.125.828-.547v-1.25c-.219.094-.5.156-.75.156-1.031 0-1.64-.562-2.078-1.609-.172-.422-.36-.672-.719-.719-.187-.015-.25-.093-.25-.187 0-.188.313-.328.625-.328.453 0 .844.281 1.25.86.313.452.64.655 1.031.655s.641-.14 1-.5c.266-.265.47-.5.657-.656');
|
|
16
|
+
svg.append(path);
|
|
17
|
+
return svg;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Discord mark from Simple Icons (CC0); notice in docs/licenses/simple-icons.txt. The mark
|
|
21
|
+
* itself is Discord's trademark, used here only to say an account is a Discord account.
|
|
22
|
+
*/
|
|
23
|
+
function discord() {
|
|
24
|
+
const svg = mark();
|
|
25
|
+
const path = document.createElementNS(namespace, 'path');
|
|
26
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
27
|
+
svg.setAttribute('fill', 'currentColor');
|
|
28
|
+
path.setAttribute('d', 'M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z');
|
|
29
|
+
svg.append(path);
|
|
30
|
+
return svg;
|
|
31
|
+
}
|
|
32
|
+
/** YouTube mark from Simple Icons (CC0); notice in docs/licenses/simple-icons.txt. */
|
|
33
|
+
function youtube() {
|
|
34
|
+
const svg = mark();
|
|
35
|
+
const path = document.createElementNS(namespace, 'path');
|
|
36
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
37
|
+
svg.setAttribute('fill', 'currentColor');
|
|
38
|
+
path.setAttribute('d', 'M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z');
|
|
39
|
+
svg.append(path);
|
|
40
|
+
return svg;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A key, drawn here rather than taken from anywhere. It is not the OpenPGP logo: that mark
|
|
44
|
+
* is somebody's to license and this repository does not ship artwork it cannot account for.
|
|
45
|
+
* A key is also the truer picture, since what was proved is control of one, not membership
|
|
46
|
+
* of an organisation.
|
|
47
|
+
*/
|
|
48
|
+
function key() {
|
|
49
|
+
return lines([
|
|
50
|
+
'M7.4 8.6a3.3 3.3 0 1 0-4.7 4.7 3.3 3.3 0 0 0 4.7-4.7Z',
|
|
51
|
+
'M7.4 8.6 14 2',
|
|
52
|
+
'M11.2 4.8l1.6 1.6',
|
|
53
|
+
'M9.4 6.6l1.6 1.6',
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
/** A globe, drawn here like the key, for a link back from a page on the open web. */
|
|
57
|
+
function globe() {
|
|
58
|
+
return lines([
|
|
59
|
+
'M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Z',
|
|
60
|
+
'M1.5 8h13',
|
|
61
|
+
'M8 1.5C6.1 3.3 5.1 5.5 5.1 8s1 4.7 2.9 6.5',
|
|
62
|
+
'M8 1.5c1.9 1.8 2.9 4 2.9 6.5s-1 4.7-2.9 6.5',
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
/** An envelope, drawn here like the key, for a mailbox that read a code sent to it. */
|
|
66
|
+
function envelope() {
|
|
67
|
+
return lines(['M2 3.5h12v9H2z', 'M2.4 4l5.6 4.6L13.6 4']);
|
|
68
|
+
}
|
|
69
|
+
/** A mark drawn as rounded strokes in the text colour. */
|
|
70
|
+
function lines(paths) {
|
|
71
|
+
const svg = mark();
|
|
72
|
+
svg.setAttribute('fill', 'none');
|
|
73
|
+
for (const d of paths) {
|
|
74
|
+
const path = document.createElementNS(namespace, 'path');
|
|
75
|
+
path.setAttribute('d', d);
|
|
76
|
+
path.setAttribute('stroke', 'currentColor');
|
|
77
|
+
path.setAttribute('stroke-width', '1.6');
|
|
78
|
+
path.setAttribute('stroke-linecap', 'round');
|
|
79
|
+
path.setAttribute('stroke-linejoin', 'round');
|
|
80
|
+
svg.append(path);
|
|
81
|
+
}
|
|
82
|
+
return svg;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The mark for a provider, or nothing where there is none. Returning nothing rather than
|
|
86
|
+
* the provider's name keeps this to one job: a caller that also writes the name would
|
|
87
|
+
* otherwise print it twice, and only the caller knows where the name belongs.
|
|
88
|
+
*/
|
|
89
|
+
export function providerMark(provider, method) {
|
|
90
|
+
const marks = {
|
|
91
|
+
github,
|
|
92
|
+
discord,
|
|
93
|
+
youtube,
|
|
94
|
+
openpgp: key,
|
|
95
|
+
email: envelope,
|
|
96
|
+
};
|
|
97
|
+
// A site names its own link-back providers, so those fall back on the method's mark.
|
|
98
|
+
return marks[provider]?.() ?? (method === 'backlink' ? globe() : undefined);
|
|
99
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Evidence } from '../core/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* What came of checking a record against its signed record in this browser: the same
|
|
4
|
+
* record as its verifier serves it with a signature.
|
|
5
|
+
*
|
|
6
|
+
* `valid` is a signed record of this very record, signed by one of its verifier's
|
|
7
|
+
* published keys. The record is then drawn from the signed one, so what stands beside the
|
|
8
|
+
* mark is what was signed. `invalid` is a signed record that was read and no published key
|
|
9
|
+
* signed, or one signed over some other record. `unchecked` is no verdict, and says why:
|
|
10
|
+
* the check ran out of time, or the signed record or the keys could not be read. Either is
|
|
11
|
+
* worth trying again.
|
|
12
|
+
*/
|
|
13
|
+
export type Signature = {
|
|
14
|
+
state: 'valid';
|
|
15
|
+
keyId: string;
|
|
16
|
+
} | {
|
|
17
|
+
state: 'invalid';
|
|
18
|
+
} | {
|
|
19
|
+
state: 'unchecked';
|
|
20
|
+
why: 'timeout' | 'unreadable';
|
|
21
|
+
};
|
|
22
|
+
/** Sets the test a signed record's contents must pass before a record is drawn from them. */
|
|
23
|
+
export declare function drawSignedWith(test: (record: unknown) => record is Evidence): void;
|
|
24
|
+
/** Checks a record, as shown, against its signed record and its verifier's published keys. */
|
|
25
|
+
export declare function signature(evidence: Evidence): Promise<Signature>;
|
|
26
|
+
/**
|
|
27
|
+
* The record as it may be shown. One whose signed record checked is shown as that has
|
|
28
|
+
* it, with only its present state taken from the record as read, so what stands beside the
|
|
29
|
+
* signature mark is what was signed. One whose signed record failed is not shown as verified: the
|
|
30
|
+
* verifier's own signature does not bear it out, so it reads as unconfirmed.
|
|
31
|
+
*/
|
|
32
|
+
export declare function standing<T extends Evidence>(evidence: T): T;
|
|
33
|
+
/**
|
|
34
|
+
* Checks each signed record given and calls back when a check changes how one is to be
|
|
35
|
+
* drawn, so whoever drew them can draw them again. A record whose check has already come
|
|
36
|
+
* back is not asked about twice.
|
|
37
|
+
*/
|
|
38
|
+
export declare function watchSigned(records: Evidence[], changed: () => void): void;
|
|
39
|
+
/** Tells a listener whenever a check changes how a record is to be drawn. Returns how to stop. */
|
|
40
|
+
export declare function onSignatureResult(listener: () => void): () => void;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { signedBy, verifySigned } from '../core/index.js';
|
|
2
|
+
/** One check per record as shown while it holds, since a card is drawn again at every read. */
|
|
3
|
+
const checks = new Map();
|
|
4
|
+
/** Each verifier's keys, read once for all of its records. */
|
|
5
|
+
const published = new Map();
|
|
6
|
+
/** What each record's signed record says, once it has checked: the record is drawn from this. */
|
|
7
|
+
const signedAs = new Map();
|
|
8
|
+
/** The records, as shown, whose signed records failed. */
|
|
9
|
+
const failures = new Set();
|
|
10
|
+
/** Who is told when a check changes how a record is to be drawn. */
|
|
11
|
+
const listeners = new Set();
|
|
12
|
+
/** Whether a record may be drawn at all, as whoever draws records decides it. */
|
|
13
|
+
let drawable = (record) => false;
|
|
14
|
+
/** Sets the test a signed record's contents must pass before a record is drawn from them. */
|
|
15
|
+
export function drawSignedWith(test) {
|
|
16
|
+
drawable = test;
|
|
17
|
+
}
|
|
18
|
+
/** A value written the same way whatever order its fields were set in. */
|
|
19
|
+
function canonical(value) {
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
22
|
+
if (value === null || typeof value !== 'object')
|
|
23
|
+
return JSON.stringify(value) ?? 'null';
|
|
24
|
+
return `{${Object.entries(value)
|
|
25
|
+
.filter(([, field]) => field !== undefined)
|
|
26
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
27
|
+
.map(([key, field]) => `${JSON.stringify(key)}:${canonical(field)}`)
|
|
28
|
+
.join(',')}}`;
|
|
29
|
+
}
|
|
30
|
+
/** What a record claims, apart from whether it stands now: all a signed record can bear out. */
|
|
31
|
+
function claims(record) {
|
|
32
|
+
const { status: _status, revokedAt: _revokedAt, signedUrl: _signedUrl, links: _links, type: _type, version: _version, issuedAt: _issuedAt, ...rest } = record;
|
|
33
|
+
return canonical(rest);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* What a verdict is kept under: the signed record's address and everything the record shown
|
|
37
|
+
* claims.
|
|
38
|
+
* Another record at the same address, or the same one once it reads differently, is checked
|
|
39
|
+
* for itself and never given the first one's verdict.
|
|
40
|
+
*/
|
|
41
|
+
const shown = (evidence) => `${evidence.signedUrl}\n${claims(evidence)}`;
|
|
42
|
+
/**
|
|
43
|
+
* How long a signed record or a key list is waited for. A card is veiled and out of reach
|
|
44
|
+
* while its check is out, so a verifier that never answers must not hold it there: past
|
|
45
|
+
* this the check is no verdict, and the record is shown as it was read.
|
|
46
|
+
*/
|
|
47
|
+
const checkLimitMs = 5000;
|
|
48
|
+
/** What a read gives when it ran out of time, as against one that failed. */
|
|
49
|
+
const late = Symbol('late');
|
|
50
|
+
/** Reads something, giving nothing if it fails and `late` if it takes longer than the limit. */
|
|
51
|
+
function limited(read) {
|
|
52
|
+
const stop = typeof AbortController === 'undefined' ? undefined : new AbortController();
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
stop?.abort();
|
|
56
|
+
resolve(late);
|
|
57
|
+
}, checkLimitMs);
|
|
58
|
+
void read(stop?.signal)
|
|
59
|
+
.catch(() => undefined)
|
|
60
|
+
.then((value) => {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
resolve(value);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function keysAt(url, afresh = false) {
|
|
67
|
+
let reading = afresh ? undefined : published.get(url);
|
|
68
|
+
if (!reading) {
|
|
69
|
+
const read = limited(async (signal) => {
|
|
70
|
+
const listed = await fetch(url, { signal });
|
|
71
|
+
return listed.ok ? (await listed.json()).keys : undefined;
|
|
72
|
+
});
|
|
73
|
+
reading = read;
|
|
74
|
+
published.set(url, read);
|
|
75
|
+
// Keys that could not be read are asked for again by the next check.
|
|
76
|
+
void read.then((keys) => {
|
|
77
|
+
if (!Array.isArray(keys) && published.get(url) === read)
|
|
78
|
+
published.delete(url);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return reading;
|
|
82
|
+
}
|
|
83
|
+
async function check(evidence) {
|
|
84
|
+
try {
|
|
85
|
+
// Both are read from where the record itself lives, never from an address the record
|
|
86
|
+
// names for the purpose: whoever handed the record over could name a signed record and
|
|
87
|
+
// keys of their own. The verifier's keys sit beside its records, `<base>/keys` for
|
|
88
|
+
// `<base>/connections/<id>`, and a signed record anywhere else is not this record's.
|
|
89
|
+
const expected = `${evidence.evidenceUrl}?format=signed`;
|
|
90
|
+
if (evidence.signedUrl !== expected)
|
|
91
|
+
return { state: 'invalid' };
|
|
92
|
+
const keysUrl = new URL('../keys', evidence.evidenceUrl).href;
|
|
93
|
+
const [signed, held] = await Promise.all([
|
|
94
|
+
limited(async (signal) => {
|
|
95
|
+
const served = await fetch(expected, { signal });
|
|
96
|
+
return served.ok ? await served.text() : undefined;
|
|
97
|
+
}),
|
|
98
|
+
keysAt(keysUrl),
|
|
99
|
+
]);
|
|
100
|
+
if (signed === late || held === late)
|
|
101
|
+
return { state: 'unchecked', why: 'timeout' };
|
|
102
|
+
if (signed === undefined || !Array.isArray(held))
|
|
103
|
+
return { state: 'unchecked', why: 'unreadable' };
|
|
104
|
+
let keys = held;
|
|
105
|
+
const by = await signedBy(signed);
|
|
106
|
+
// Something else was served in a signed record's place, which is no verdict on this one.
|
|
107
|
+
if (by === undefined)
|
|
108
|
+
return { state: 'unchecked', why: 'unreadable' };
|
|
109
|
+
// A verifier may have changed keys since its list was read. A key this list does not
|
|
110
|
+
// have is looked for in the list as it is now, before the record is called unsigned.
|
|
111
|
+
if (!held.some((key) => key?.id === by)) {
|
|
112
|
+
keys = await keysAt(keysUrl, true);
|
|
113
|
+
if (!Array.isArray(keys))
|
|
114
|
+
return { state: 'unchecked', why: keys === late ? 'timeout' : 'unreadable' };
|
|
115
|
+
}
|
|
116
|
+
const document = await verifySigned(signed, keys);
|
|
117
|
+
// A good signature over some other record says nothing about this one.
|
|
118
|
+
if (!document ||
|
|
119
|
+
document.id !== evidence.id ||
|
|
120
|
+
document.verifierName !== evidence.verifierName ||
|
|
121
|
+
document.evidenceUrl !== evidence.evidenceUrl ||
|
|
122
|
+
document.siteName !== evidence.siteName ||
|
|
123
|
+
document.local.reference !== evidence.local.reference ||
|
|
124
|
+
document.provider !== evidence.provider ||
|
|
125
|
+
document.external.id !== evidence.external.id)
|
|
126
|
+
return { state: 'invalid' };
|
|
127
|
+
// The record as it was signed. Whether it stands now is no part of a signed record, so
|
|
128
|
+
// that alone is kept from the record as it was read.
|
|
129
|
+
const { type: _type, version: _version, issuedAt: _issuedAt, ...said } = document;
|
|
130
|
+
const record = {
|
|
131
|
+
...said,
|
|
132
|
+
status: evidence.status,
|
|
133
|
+
...(evidence.revokedAt === undefined ? {} : { revokedAt: evidence.revokedAt }),
|
|
134
|
+
signedUrl: evidence.signedUrl,
|
|
135
|
+
};
|
|
136
|
+
// Signed, but not something a page may draw: it is shown as read, with no mark.
|
|
137
|
+
return drawable(record)
|
|
138
|
+
? { state: 'valid', keyId: by, record }
|
|
139
|
+
: { state: 'unchecked', why: 'unreadable' };
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return { state: 'unchecked', why: 'unreadable' };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Checks a record, as shown, against its signed record and its verifier's published keys. */
|
|
146
|
+
export function signature(evidence) {
|
|
147
|
+
const key = shown(evidence);
|
|
148
|
+
let checking = checks.get(key);
|
|
149
|
+
if (!checking) {
|
|
150
|
+
const checked = check(evidence);
|
|
151
|
+
checking = checked.then(({ record: _record, ...result }) => result);
|
|
152
|
+
checks.set(key, checking);
|
|
153
|
+
void checked.then((result) => {
|
|
154
|
+
// Nothing was learned, so the next card drawn asks again.
|
|
155
|
+
if (result.state === 'unchecked')
|
|
156
|
+
checks.delete(key);
|
|
157
|
+
if (result.state === 'invalid')
|
|
158
|
+
failures.add(key);
|
|
159
|
+
if (result.record) {
|
|
160
|
+
signedAs.set(key, result.record);
|
|
161
|
+
// Drawn from the signed record, it is asked about again as that has it. That is the
|
|
162
|
+
// same signed record and the same answer, so it is not read a second time.
|
|
163
|
+
const drawn = shown(result.record);
|
|
164
|
+
signedAs.set(drawn, result.record);
|
|
165
|
+
checks.set(drawn, checking);
|
|
166
|
+
}
|
|
167
|
+
// Told only where the record is now to be drawn some other way than it was read.
|
|
168
|
+
if (result.state === 'invalid' ||
|
|
169
|
+
(result.record && claims(result.record) !== claims(evidence)))
|
|
170
|
+
for (const listener of [...listeners])
|
|
171
|
+
listener();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return checking;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The record as it may be shown. One whose signed record checked is shown as that has
|
|
178
|
+
* it, with only its present state taken from the record as read, so what stands beside the
|
|
179
|
+
* signature mark is what was signed. One whose signed record failed is not shown as verified: the
|
|
180
|
+
* verifier's own signature does not bear it out, so it reads as unconfirmed.
|
|
181
|
+
*/
|
|
182
|
+
export function standing(evidence) {
|
|
183
|
+
if (evidence.signedUrl === undefined)
|
|
184
|
+
return evidence;
|
|
185
|
+
const key = shown(evidence);
|
|
186
|
+
const signed = signedAs.get(key);
|
|
187
|
+
if (signed)
|
|
188
|
+
return {
|
|
189
|
+
...signed,
|
|
190
|
+
status: evidence.status,
|
|
191
|
+
...(evidence.revokedAt === undefined ? {} : { revokedAt: evidence.revokedAt }),
|
|
192
|
+
};
|
|
193
|
+
return evidence.status === 'verified' && failures.has(key)
|
|
194
|
+
? { ...evidence, status: 'unconfirmed' }
|
|
195
|
+
: evidence;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Checks each signed record given and calls back when a check changes how one is to be
|
|
199
|
+
* drawn, so whoever drew them can draw them again. A record whose check has already come
|
|
200
|
+
* back is not asked about twice.
|
|
201
|
+
*/
|
|
202
|
+
export function watchSigned(records, changed) {
|
|
203
|
+
for (const record of records) {
|
|
204
|
+
if (record.signedUrl === undefined)
|
|
205
|
+
continue;
|
|
206
|
+
const key = shown(record);
|
|
207
|
+
if (failures.has(key) || signedAs.has(key))
|
|
208
|
+
continue;
|
|
209
|
+
void signature(record).then((result) => {
|
|
210
|
+
if (result.state === 'invalid' || claims(standing(record)) !== claims(record))
|
|
211
|
+
changed();
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Tells a listener whenever a check changes how a record is to be drawn. Returns how to stop. */
|
|
216
|
+
export function onSignatureResult(listener) {
|
|
217
|
+
listeners.add(listener);
|
|
218
|
+
return () => void listeners.delete(listener);
|
|
219
|
+
}
|
package/dist/browser.js
ADDED