@bkazemi/verily 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -10
- package/dist/browser/badge.d.ts +4 -1
- package/dist/browser/badge.js +7 -4
- package/dist/browser/connect-dialog.js +10 -6
- package/dist/browser/evidence-dialog.d.ts +17 -3
- package/dist/browser/evidence-dialog.js +279 -29
- package/dist/browser/index.d.ts +7 -1
- package/dist/browser/index.js +52 -64
- package/dist/core/index.d.ts +95 -4
- package/dist/core/index.js +59 -5
- package/dist/core/routes.d.ts +57 -0
- package/dist/core/routes.js +52 -0
- package/dist/server/domain.d.ts +76 -10
- package/dist/server/domain.js +140 -51
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +148 -34
- package/dist/server/openpgp.d.ts +4 -0
- package/dist/server/openpgp.js +10 -1
- package/dist/server/pgp.d.ts +2 -0
- package/dist/server/pgp.js +1 -1
- package/dist/server/service.d.ts +87 -2
- package/dist/server/service.js +248 -25
- package/dist/server/style.d.ts +1 -1
- package/dist/server/style.js +40 -0
- package/dist/site/index.js +2 -2
- package/dist/verily.js +371 -95
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/server/domain.d.ts
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { type ArtifactProvider } from '../core/index.js';
|
|
2
|
-
export interface
|
|
3
|
-
/**
|
|
4
|
-
name?: string;
|
|
5
|
-
/** Replaces the transport. For `dnsProvider()` that is only ever the resolver. */
|
|
2
|
+
export interface DnsOptions {
|
|
3
|
+
/** Replaces the transport, which is only ever asked for the resolver. */
|
|
6
4
|
fetch?: typeof fetch;
|
|
7
5
|
timeoutMs?: number;
|
|
8
6
|
}
|
|
7
|
+
export interface DomainProviderOptions extends DnsOptions {
|
|
8
|
+
/** Shown as "Verify with …". */
|
|
9
|
+
name?: string;
|
|
10
|
+
}
|
|
11
|
+
/** What a domain publishes for Verily, as one lookup found it. */
|
|
12
|
+
export interface Published {
|
|
13
|
+
/** The lookup that was run, which is also a link a reader can open. */
|
|
14
|
+
lookup: string;
|
|
15
|
+
/** The text of every TXT record at `_verily.<domain>` that could be read. */
|
|
16
|
+
values: string[];
|
|
17
|
+
/**
|
|
18
|
+
* Whether the resolver validated the answer with DNSSEC. Only a domain that signs its
|
|
19
|
+
* zone gets this, and without it the answer is as good as the resolver's own lookup.
|
|
20
|
+
*/
|
|
21
|
+
dnssec: boolean;
|
|
22
|
+
}
|
|
9
23
|
export interface WellKnownProviderOptions extends DomainProviderOptions {
|
|
10
24
|
/**
|
|
11
25
|
* Domains this instance will read, lowercase. Omitted means any public host, which is
|
|
@@ -22,8 +36,8 @@ export interface WellKnownProviderOptions extends DomainProviderOptions {
|
|
|
22
36
|
maxBytes?: number;
|
|
23
37
|
}
|
|
24
38
|
/**
|
|
25
|
-
* Proves a domain by a record in its DNS. The holder publishes the local subject's address
|
|
26
|
-
* in a TXT record at `_verily.<domain>` and names the domain; this looks the record up and
|
|
39
|
+
* Proves a domain by a record in its DNS. The holder publishes the local subject's address,
|
|
40
|
+
* or a string minted for them where the subject has none, in a TXT record at `_verily.<domain>` and names the domain; this looks the record up and
|
|
27
41
|
* checks the address is there. It reaches a domain that serves no page of its own, or only
|
|
28
42
|
* forwards somewhere else, which a link back never can.
|
|
29
43
|
*
|
|
@@ -32,14 +46,66 @@ export interface WellKnownProviderOptions extends DomainProviderOptions {
|
|
|
32
46
|
* nothing is said about a parent or a subdomain. A domain changes hands like an account,
|
|
33
47
|
* and takes the record with it or drops it.
|
|
34
48
|
*
|
|
35
|
-
* Like a link back, the proof is a standing statement
|
|
36
|
-
*
|
|
49
|
+
* Like a link back, the proof is a standing statement, so freshness comes from `recheck()`
|
|
50
|
+
* looking it up again and a renewal asks for the record already there. A lookup that could not be made is
|
|
37
51
|
* unread, never absent.
|
|
38
52
|
*/
|
|
39
53
|
export declare function dnsProvider(options?: DomainProviderOptions): ArtifactProvider;
|
|
40
54
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
55
|
+
* Everything a domain publishes at `_verily.<domain>`, and whether the resolver validated
|
|
56
|
+
* it. One name carries every statement a domain makes here, told apart by how each begins:
|
|
57
|
+
* a subject's address or a minted proof, a signing key, a verifier.
|
|
58
|
+
*
|
|
59
|
+
* Throws `Refused` where the lookup gave no answer. A name with no records is an answer.
|
|
60
|
+
*/
|
|
61
|
+
export declare function published(domain: string, options?: DnsOptions): Promise<Published>;
|
|
62
|
+
/**
|
|
63
|
+
* What a verifier publishes at `_verily.<its host>` to name a signing key: one record for
|
|
64
|
+
* each key. A reader holding a signed record and nothing else can then ask the verifier's
|
|
65
|
+
* domain, and not the verifier's server, whether the key that signed it is the domain's.
|
|
66
|
+
*/
|
|
67
|
+
export declare function keyRecord(fingerprint: string): string;
|
|
68
|
+
/** What a site publishes at `_verily.<its host>` to name the verifier that speaks for it. */
|
|
69
|
+
export declare function verifierRecord(host: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* The fingerprints of the signing keys a domain names in its DNS. A signed record names the
|
|
72
|
+
* key that signed it and the verifier's address, so a key found here is one the domain in
|
|
73
|
+
* that address answers for, for as long as the domain is kept and whatever became of the
|
|
74
|
+
* server. Check the signature with `verifySigned()` first: this says whose a key is, and
|
|
75
|
+
* nothing about what it signed.
|
|
76
|
+
*/
|
|
77
|
+
export declare function dnsKeys(domain: string, options?: DnsOptions): Promise<Omit<Published, 'values'> & {
|
|
78
|
+
ids: string[];
|
|
79
|
+
}>;
|
|
80
|
+
/**
|
|
81
|
+
* The hosts of the verifiers a site's domain names in its DNS. A record's local side is
|
|
82
|
+
* the verifier's word for what the site told it, so a verifier named here is one the site
|
|
83
|
+
* itself chose, which a reader can find out without visiting the site.
|
|
84
|
+
*/
|
|
85
|
+
export declare function dnsVerifiers(domain: string, options?: DnsOptions): Promise<Omit<Published, 'values'> & {
|
|
86
|
+
hosts: string[];
|
|
87
|
+
}>;
|
|
88
|
+
/**
|
|
89
|
+
* The fingerprints a domain's records name, in the upper case a key's id is kept in. A
|
|
90
|
+
* fingerprint is hexadecimal, so however it was typed it names the same key.
|
|
91
|
+
*/
|
|
92
|
+
export declare function namedKeys(values: string[]): string[];
|
|
93
|
+
/**
|
|
94
|
+
* The verifier hosts a domain's records name, lowercase as a host compares, each with the
|
|
95
|
+
* record as it is published: that text is what a reader finds at the lookup.
|
|
96
|
+
*/
|
|
97
|
+
export declare function namedVerifiers(values: string[]): {
|
|
98
|
+
host: string;
|
|
99
|
+
record: string;
|
|
100
|
+
}[];
|
|
101
|
+
/**
|
|
102
|
+
* The name a URL's host is looked up under, or nothing where it is not a public domain: an
|
|
103
|
+
* address literal, a single label, a name inside the network.
|
|
104
|
+
*/
|
|
105
|
+
export declare function domainOf(url: string): string | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* Proves a domain by a file it serves. The holder puts the local subject's address, or a
|
|
108
|
+
* string minted for them where the subject has none, on a line of `/.well-known/verily.txt` and names the domain; this reads the file and checks
|
|
43
109
|
* the line is there. It is for a holder who can add a file to a site and cannot reach its
|
|
44
110
|
* DNS or its pages' markup.
|
|
45
111
|
*
|
package/dist/server/domain.js
CHANGED
|
@@ -23,9 +23,13 @@ const label = '_verily';
|
|
|
23
23
|
const path = '/.well-known/verily.txt';
|
|
24
24
|
/** The TXT record type, as the resolver numbers it. */
|
|
25
25
|
const txt = 16;
|
|
26
|
+
/** What a signing key's record says, before the key's fingerprint. */
|
|
27
|
+
const keyPrefix = 'verily-key=';
|
|
28
|
+
/** What a site's record says, before the host of the verifier it chose. */
|
|
29
|
+
const verifierPrefix = 'verily-verifier=';
|
|
26
30
|
/**
|
|
27
|
-
* Proves a domain by a record in its DNS. The holder publishes the local subject's address
|
|
28
|
-
* in a TXT record at `_verily.<domain>` and names the domain; this looks the record up and
|
|
31
|
+
* Proves a domain by a record in its DNS. The holder publishes the local subject's address,
|
|
32
|
+
* or a string minted for them where the subject has none, in a TXT record at `_verily.<domain>` and names the domain; this looks the record up and
|
|
29
33
|
* checks the address is there. It reaches a domain that serves no page of its own, or only
|
|
30
34
|
* forwards somewhere else, which a link back never can.
|
|
31
35
|
*
|
|
@@ -34,12 +38,18 @@ const txt = 16;
|
|
|
34
38
|
* nothing is said about a parent or a subdomain. A domain changes hands like an account,
|
|
35
39
|
* and takes the record with it or drops it.
|
|
36
40
|
*
|
|
37
|
-
* Like a link back, the proof is a standing statement
|
|
38
|
-
*
|
|
41
|
+
* Like a link back, the proof is a standing statement, so freshness comes from `recheck()`
|
|
42
|
+
* looking it up again and a renewal asks for the record already there. A lookup that could not be made is
|
|
39
43
|
* unread, never absent.
|
|
40
44
|
*/
|
|
41
45
|
export function dnsProvider(options = {}) {
|
|
42
|
-
const
|
|
46
|
+
const prove = async ({ artifact, expect }) => {
|
|
47
|
+
const domain = looked(artifact);
|
|
48
|
+
const { values, dnssec } = await published(domain, options);
|
|
49
|
+
if (!values.includes(expect))
|
|
50
|
+
throw new Refused(`No TXT record at ${label}.${domain} has this value`);
|
|
51
|
+
return { account: account(domain), ...(dnssec ? { dnssec } : {}) };
|
|
52
|
+
};
|
|
43
53
|
return {
|
|
44
54
|
...shared(options),
|
|
45
55
|
method: 'dns',
|
|
@@ -53,47 +63,126 @@ export function dnsProvider(options = {}) {
|
|
|
53
63
|
const domain = named(artifact);
|
|
54
64
|
return domain ? lookup(domain) : artifact;
|
|
55
65
|
},
|
|
56
|
-
async
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
66
|
+
verify: async (input) => (await prove(input)).account,
|
|
67
|
+
// The resolver says whether it validated the answer, which is worth keeping with it.
|
|
68
|
+
prove,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Everything a domain publishes at `_verily.<domain>`, and whether the resolver validated
|
|
73
|
+
* it. One name carries every statement a domain makes here, told apart by how each begins:
|
|
74
|
+
* a subject's address or a minted proof, a signing key, a verifier.
|
|
75
|
+
*
|
|
76
|
+
* Throws `Refused` where the lookup gave no answer. A name with no records is an answer.
|
|
77
|
+
*/
|
|
78
|
+
export async function published(domain, options = {}) {
|
|
79
|
+
if (!valid(domain))
|
|
80
|
+
throw new Refused('Not a domain this can look up');
|
|
81
|
+
// Why the resolver could not be asked is about the network this runs in.
|
|
82
|
+
const response = await (options.fetch ?? fetch)(lookup(domain), {
|
|
83
|
+
redirect: 'manual',
|
|
84
|
+
headers: { Accept: 'application/dns-json', 'User-Agent': 'Verily-V0' },
|
|
85
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 15000),
|
|
86
|
+
}).catch(() => {
|
|
87
|
+
throw new Refused('DNS could not be read');
|
|
88
|
+
});
|
|
89
|
+
let answer;
|
|
90
|
+
try {
|
|
91
|
+
if (!response.ok)
|
|
92
|
+
throw new Refused('DNS could not be read');
|
|
93
|
+
const { bytes, truncated } = await readBounded(response, 64 * 1024);
|
|
94
|
+
if (truncated)
|
|
95
|
+
throw new Refused('DNS could not be read');
|
|
96
|
+
answer = parsed(new TextDecoder().decode(bytes));
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await discard(response);
|
|
100
|
+
}
|
|
101
|
+
// A cut-off answer may have left out the record that would have matched.
|
|
102
|
+
if (!isRecord(answer) || answer.TC === true)
|
|
103
|
+
throw new Refused('DNS could not be read');
|
|
104
|
+
// The name not existing is an answer; anything else that is not success is no answer.
|
|
105
|
+
if (answer.Status !== 0 && answer.Status !== 3)
|
|
106
|
+
throw new Refused('DNS could not be read');
|
|
107
|
+
const records = Array.isArray(answer.Answer) ? answer.Answer : [];
|
|
108
|
+
return {
|
|
109
|
+
lookup: lookup(domain),
|
|
110
|
+
values: records
|
|
111
|
+
.map((record) => isRecord(record) && record.type === txt && typeof record.data === 'string'
|
|
112
|
+
? text(record.data)
|
|
113
|
+
: undefined)
|
|
114
|
+
.filter((value) => value !== undefined),
|
|
115
|
+
// The resolver sets this only where every record in the answer validated.
|
|
116
|
+
dnssec: answer.AD === true,
|
|
92
117
|
};
|
|
93
118
|
}
|
|
94
119
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
120
|
+
* What a verifier publishes at `_verily.<its host>` to name a signing key: one record for
|
|
121
|
+
* each key. A reader holding a signed record and nothing else can then ask the verifier's
|
|
122
|
+
* domain, and not the verifier's server, whether the key that signed it is the domain's.
|
|
123
|
+
*/
|
|
124
|
+
export function keyRecord(fingerprint) {
|
|
125
|
+
return `${keyPrefix}${fingerprint}`;
|
|
126
|
+
}
|
|
127
|
+
/** What a site publishes at `_verily.<its host>` to name the verifier that speaks for it. */
|
|
128
|
+
export function verifierRecord(host) {
|
|
129
|
+
return `${verifierPrefix}${host.toLowerCase()}`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The fingerprints of the signing keys a domain names in its DNS. A signed record names the
|
|
133
|
+
* key that signed it and the verifier's address, so a key found here is one the domain in
|
|
134
|
+
* that address answers for, for as long as the domain is kept and whatever became of the
|
|
135
|
+
* server. Check the signature with `verifySigned()` first: this says whose a key is, and
|
|
136
|
+
* nothing about what it signed.
|
|
137
|
+
*/
|
|
138
|
+
export async function dnsKeys(domain, options = {}) {
|
|
139
|
+
const { values, ...found } = await published(named(domain) ?? domain, options);
|
|
140
|
+
return { ...found, ids: namedKeys(values) };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The hosts of the verifiers a site's domain names in its DNS. A record's local side is
|
|
144
|
+
* the verifier's word for what the site told it, so a verifier named here is one the site
|
|
145
|
+
* itself chose, which a reader can find out without visiting the site.
|
|
146
|
+
*/
|
|
147
|
+
export async function dnsVerifiers(domain, options = {}) {
|
|
148
|
+
const { values, ...found } = await published(named(domain) ?? domain, options);
|
|
149
|
+
return { ...found, hosts: namedVerifiers(values).map(({ host }) => host) };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The fingerprints a domain's records name, in the upper case a key's id is kept in. A
|
|
153
|
+
* fingerprint is hexadecimal, so however it was typed it names the same key.
|
|
154
|
+
*/
|
|
155
|
+
export function namedKeys(values) {
|
|
156
|
+
return stated(values, keyPrefix).map((id) => id.toUpperCase());
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The verifier hosts a domain's records name, lowercase as a host compares, each with the
|
|
160
|
+
* record as it is published: that text is what a reader finds at the lookup.
|
|
161
|
+
*/
|
|
162
|
+
export function namedVerifiers(values) {
|
|
163
|
+
return values
|
|
164
|
+
.filter((value) => value.startsWith(verifierPrefix))
|
|
165
|
+
.map((record) => ({ host: record.slice(verifierPrefix.length).trim().toLowerCase(), record }))
|
|
166
|
+
.filter(({ host }) => host);
|
|
167
|
+
}
|
|
168
|
+
/** What follows a prefix in each record that begins with it. */
|
|
169
|
+
function stated(values, prefix) {
|
|
170
|
+
return values
|
|
171
|
+
.filter((value) => value.startsWith(prefix))
|
|
172
|
+
.map((value) => value.slice(prefix.length).trim())
|
|
173
|
+
.filter(Boolean);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The name a URL's host is looked up under, or nothing where it is not a public domain: an
|
|
177
|
+
* address literal, a single label, a name inside the network.
|
|
178
|
+
*/
|
|
179
|
+
export function domainOf(url) {
|
|
180
|
+
const host = URL.parse(url)?.hostname.toLowerCase().replace(/\.$/, '');
|
|
181
|
+
return host !== undefined && valid(host) ? host : undefined;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Proves a domain by a file it serves. The holder puts the local subject's address, or a
|
|
185
|
+
* string minted for them where the subject has none, on a line of `/.well-known/verily.txt` and names the domain; this reads the file and checks
|
|
97
186
|
* the line is there. It is for a holder who can add a file to a site and cannot reach its
|
|
98
187
|
* DNS or its pages' markup.
|
|
99
188
|
*
|
|
@@ -110,7 +199,7 @@ export function wellKnownProvider(options = {}) {
|
|
|
110
199
|
...shared(options),
|
|
111
200
|
method: 'wellknown',
|
|
112
201
|
instructions: (expect) => [
|
|
113
|
-
'Serve a plain text file at this path on your domain, with this
|
|
202
|
+
'Serve a plain text file at this path on your domain, with this on a line of its own, then enter the domain below.',
|
|
114
203
|
{ code: path },
|
|
115
204
|
{ code: expect },
|
|
116
205
|
],
|
|
@@ -144,7 +233,7 @@ export function wellKnownProvider(options = {}) {
|
|
|
144
233
|
if (truncated)
|
|
145
234
|
lines.pop();
|
|
146
235
|
if (!lines.some((line) => line.trim() === expect))
|
|
147
|
-
throw new Refused(truncated ? 'File is too large to read' : 'File has no line
|
|
236
|
+
throw new Refused(truncated ? 'File is too large to read' : 'File has no line with this value');
|
|
148
237
|
}
|
|
149
238
|
finally {
|
|
150
239
|
await discard(response);
|
|
@@ -161,12 +250,12 @@ function shared(options) {
|
|
|
161
250
|
artifact: 'location',
|
|
162
251
|
field: 'Your domain',
|
|
163
252
|
input: 'text',
|
|
164
|
-
// The subject's own address is the entire claim,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
253
|
+
// The subject's own address is the entire claim, where it has one. A subject with no
|
|
254
|
+
// page a reader could open has nothing to name, and is proved by a minted string.
|
|
255
|
+
expect: (local) => local.profileUrl,
|
|
256
|
+
// DNS and a well-known path are read by anyone, unasked. A subject with no public page
|
|
257
|
+
// is one its site keeps private, so the string says nothing of the site either.
|
|
258
|
+
mint: (secret) => `verily-proof=${secret}`,
|
|
170
259
|
// A domain is where it says it is, so a record of one already says where to look.
|
|
171
260
|
known: (account) => (account.kind === 'domain' ? account.id : undefined),
|
|
172
261
|
};
|
package/dist/server/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { discordProvider } from './discord.js';
|
|
|
7
7
|
export { youtubeProvider } from './youtube.js';
|
|
8
8
|
export { githubGistProvider } from './github-gist.js';
|
|
9
9
|
export { linkProvider, githubLinkProvider, type LinkProviderOptions } from './link.js';
|
|
10
|
-
export { dnsProvider, wellKnownProvider, type DomainProviderOptions, type WellKnownProviderOptions, } from './domain.js';
|
|
10
|
+
export { dnsProvider, wellKnownProvider, dnsKeys, dnsVerifiers, keyRecord, verifierRecord, type DnsOptions, type DomainProviderOptions, type Published, type WellKnownProviderOptions, } from './domain.js';
|
|
11
11
|
export { pgpProvider } from './pgp.js';
|
|
12
12
|
export { generateSigningKey, signer } from './signing.js';
|
|
13
13
|
export { emailProvider, type EmailImage, type EmailMessage, type EmailProviderOptions, } from './email.js';
|