@bkazemi/verily 0.3.0 → 0.4.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 +9 -3
- package/dist/browser/connect-dialog.js +2 -2
- package/dist/browser/evidence-dialog.js +3 -3
- package/dist/browser/index.js +1 -1
- package/dist/browser/provider-mark.js +5 -3
- package/dist/core/index.d.ts +10 -3
- package/dist/core/index.js +9 -0
- package/dist/server/domain.d.ts +51 -0
- package/dist/server/domain.js +285 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +38 -12
- package/dist/server/link.js +1 -16
- package/dist/server/public-fetch.d.ts +10 -0
- package/dist/server/public-fetch.js +15 -0
- package/dist/verily.js +12 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -145,7 +145,7 @@ import { init } from '@bkazemi/verily'; // also registers <verily-badge>
|
|
|
145
145
|
const client = init({ backendUrl: '/api/verily' });
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
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.
|
|
148
|
+
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.4.0/dist/verily.js`. The script defines a global `Verily`.
|
|
149
149
|
|
|
150
150
|
Then place the badge wherever the account appears:
|
|
151
151
|
|
|
@@ -208,6 +208,8 @@ import {
|
|
|
208
208
|
githubGistProvider,
|
|
209
209
|
linkProvider,
|
|
210
210
|
githubLinkProvider,
|
|
211
|
+
dnsProvider,
|
|
212
|
+
wellKnownProvider,
|
|
211
213
|
pgpProvider,
|
|
212
214
|
} from '@bkazemi/verily';
|
|
213
215
|
```
|
|
@@ -220,10 +222,12 @@ import {
|
|
|
220
222
|
| `githubGistProvider()` | publishing a public gist containing a line Verily gives them. | None. |
|
|
221
223
|
| `linkProvider()` | adding a `rel="me"` link to their profile on a page they control. | The local account needs a `profileUrl`. |
|
|
222
224
|
| `githubLinkProvider()` | putting their profile URL in the website field of their GitHub profile, then entering their GitHub username. | The local account needs a `profileUrl`. |
|
|
225
|
+
| `dnsProvider()` | adding a TXT record to their domain's DNS, then entering the domain. | The local account needs a `profileUrl`. |
|
|
226
|
+
| `wellKnownProvider()` | serving a text file at a fixed path on their domain, then entering the domain. | The local account needs a `profileUrl`. |
|
|
223
227
|
| `pgpProvider()` | signing a line Verily gives them with their OpenPGP key, then pasting it and the key. | None. |
|
|
224
228
|
| `emailProvider()` | pressing a button, or entering a code, that Verily mails to their address. | A function that sends one message. |
|
|
225
229
|
|
|
226
|
-
All methods except the sign-ins and the mailed code let the user publish the proof in their own time and come back. A proof that does not check out can be put right and handed back on the same step, up to five times, so a mistyped address never means publishing or signing again. Gists
|
|
230
|
+
All methods except the sign-ins and the mailed code let the user publish the proof in their own time and come back. A proof that does not check out can be put right and handed back on the same step, up to five times, so a mistyped address never means publishing or signing again. Gists, link-backs, DNS records and files can be taken down later, so Verily re-reads them on a schedule. A PGP signature is kept by Verily and published at `<baseUrl>/connections/<id>/proof`.
|
|
227
231
|
|
|
228
232
|
**Several at once.** List more than one, and `/verify` offers each method as its own button:
|
|
229
233
|
|
|
@@ -239,6 +243,8 @@ Proving the same account a second way adds that proof to the existing record ins
|
|
|
239
243
|
|
|
240
244
|
**Link-backs.** Many people already have one, since GitHub and Mastodon mark profile links `rel="me"`. By default the page may be on any public host, and the account is named by the page's address. This mode needs Node, because Verily checks every connection it makes to stop the page's address from pointing inside your network. Pass `hosts` to read only certain hosts (required on Cloudflare Workers), and `profile` to name the account by handle, as `githubLinkProvider()` does for github.com. If you pass your own `fetch`, it replaces that network check, so it must enforce the same rule itself. Only real `<a>` and `<link>` elements in HTML pages, or a `Link:` header, count; [`src/server/link.ts`](src/server/link.ts) has the exact rules.
|
|
241
245
|
|
|
246
|
+
**Domains.** `dnsProvider()` proves a domain that serves no page of its own, or only forwards elsewhere. The user adds a TXT record named `_verily` whose value is their `profileUrl`, and enters the domain. Verily looks up `_verily.<domain>` through `https://dns.google/resolve`, a fixed resolver, so it runs on Cloudflare Workers, and the evidence links to that lookup for a reader to repeat. `wellKnownProvider()` reads `https://<domain>/.well-known/verily.txt` instead and looks for the `profileUrl` on a line of its own. It fetches a host the user names, so it follows the link-back's rules: Node, or `hosts`. Both name the account by the exact domain entered, and say nothing about its subdomains. List both and a domain proved one way picks up the other if it is already there.
|
|
247
|
+
|
|
242
248
|
**Your own method.** A provider is an object, and [`src/core/index.ts`](src/core/index.ts) documents each shape. An artifact provider may also set `field` and `input` to name what it asks for, `resolve` to turn something short such as a username into the address to read, and `known` to say where to look for an account already on record, which is what lets a proof be found without asking. A sign-in provider's `authorizationUrl` is passed `account` on a renewal or visibility change, so it can ask for that account by name.
|
|
243
249
|
|
|
244
250
|
If you start flows by calling `service.start()` yourself, pass `true` as its last argument only where you have shown the request came from the user's own page, as a same-origin POST does. Without it the flow reads nothing at its start and always ends on an approval.
|
|
@@ -293,7 +299,7 @@ A static site only needs the script and the badge markup; the backend runs somew
|
|
|
293
299
|
4. Add the script and the badge to your site:
|
|
294
300
|
|
|
295
301
|
```html
|
|
296
|
-
<script src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.
|
|
302
|
+
<script src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.4.0/dist/verily.js" defer></script>
|
|
297
303
|
<verily-badge
|
|
298
304
|
backend-url="https://your-backend.example/api/verily"
|
|
299
305
|
connection-id="CONNECTION_ID"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { externalLink, externalName } from '../core/index.js';
|
|
1
|
+
import { externalId, externalLink, externalName } from '../core/index.js';
|
|
2
2
|
import { accountCard, linkMark, node, outward, stampLink, styles } from './evidence-dialog.js';
|
|
3
3
|
import { providerMark } from './provider-mark.js';
|
|
4
4
|
import { version } from '../version.js';
|
|
@@ -433,7 +433,7 @@ renew) {
|
|
|
433
433
|
const logo = providerMark(flow.provider.id, flow.provider.method);
|
|
434
434
|
const external = accountCard(logo
|
|
435
435
|
? [logo, document.createTextNode(flow.provider.name)]
|
|
436
|
-
: [document.createTextNode(flow.provider.name)], externalName(flow.external), flow.external
|
|
436
|
+
: [document.createTextNode(flow.provider.name)], externalName(flow.external), externalId(flow.external), externalLink(flow.external));
|
|
437
437
|
const parts = [subjectCard(flow.local), linkMark(), external];
|
|
438
438
|
const choice = node('fieldset');
|
|
439
439
|
const visibilityText = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { attestationLabel, externalLink, externalName, lastProved, localSide, proofTitle, statusLabel, } from '../core/index.js';
|
|
1
|
+
import { attestationLabel, externalId, externalLink, externalName, lastProved, localSide, proofTitle, statusLabel, } from '../core/index.js';
|
|
2
2
|
import { markStyles, verificationMark } from './mark.js';
|
|
3
3
|
import { providerMark } from './provider-mark.js';
|
|
4
4
|
import { onSignatureResult, signature, standing } from './signed.js';
|
|
@@ -384,8 +384,8 @@ function externalCard(evidence, manage, later = () => { }) {
|
|
|
384
384
|
// The mark and the name, or just the name. A mark that falls back to writing the name
|
|
385
385
|
// would print it twice here, since this heading writes it either way.
|
|
386
386
|
logo ? [logo, document.createTextNode(provider)] : [document.createTextNode(provider)], externalName(evidence.external),
|
|
387
|
-
// A mailbox's address is its name already, so there is no second identifier.
|
|
388
|
-
evidence.external
|
|
387
|
+
// A mailbox's address or a domain is its name already, so there is no second identifier.
|
|
388
|
+
externalId(evidence.external), externalLink(evidence.external),
|
|
389
389
|
// Status first, then how it was shown, then when. The proof explains the state
|
|
390
390
|
// above it, so it cannot sit before that state has been given.
|
|
391
391
|
summary, ...attestationNote(main, names), ...evidence.attestations.external.slice(1).flatMap((a) => attestationNote(a, names, true)),
|
package/dist/browser/index.js
CHANGED
|
@@ -669,7 +669,7 @@ function validEvidence(value) {
|
|
|
669
669
|
validAttestations(value.attestations) &&
|
|
670
670
|
(value.local.kind === undefined || typeof value.local.kind === 'string') &&
|
|
671
671
|
(value.external.kind === undefined ||
|
|
672
|
-
['account', 'key', 'page', 'mailbox'].includes(String(value.external.kind))) &&
|
|
672
|
+
['account', 'key', 'page', 'mailbox', 'domain'].includes(String(value.external.kind))) &&
|
|
673
673
|
(value.revokedAt === undefined ||
|
|
674
674
|
(typeof value.revokedAt === 'number' && Number.isFinite(value.revokedAt))) &&
|
|
675
675
|
['verified', 'unconfirmed', 'expired', 'retired', 'revoked'].includes(String(value.status)) &&
|
|
@@ -53,7 +53,7 @@ function key() {
|
|
|
53
53
|
'M9.4 6.6l1.6 1.6',
|
|
54
54
|
]);
|
|
55
55
|
}
|
|
56
|
-
/** A globe, drawn here like the key, for a
|
|
56
|
+
/** A globe, drawn here like the key, for a page or a domain on the open web. */
|
|
57
57
|
function globe() {
|
|
58
58
|
return lines([
|
|
59
59
|
'M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13Z',
|
|
@@ -94,6 +94,8 @@ export function providerMark(provider, method) {
|
|
|
94
94
|
openpgp: key,
|
|
95
95
|
email: envelope,
|
|
96
96
|
};
|
|
97
|
-
// A site names its own link-back providers, so those fall back on the method's mark
|
|
98
|
-
|
|
97
|
+
// A site names its own link-back providers, so those fall back on the method's mark,
|
|
98
|
+
// which a domain shown by its DNS or by a file shares.
|
|
99
|
+
return (marks[provider]?.() ??
|
|
100
|
+
(['backlink', 'dns', 'wellknown'].includes(method ?? '') ? globe() : undefined));
|
|
99
101
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -37,9 +37,11 @@ export interface ExternalAccount {
|
|
|
37
37
|
* A page is an address that was read and nothing more: a method that only fetches a
|
|
38
38
|
* document learns what it says, never whose account the address is. A mailbox is an
|
|
39
39
|
* account in all but how it is written: its domain issued it and can hand it to somebody
|
|
40
|
-
* else, but its address is already its whole name and it has no profile to link to.
|
|
40
|
+
* else, but its address is already its whole name and it has no profile to link to. A
|
|
41
|
+
* domain is likewise issued and can be handed over, and is named by itself: what was
|
|
42
|
+
* shown is control of the name, whatever is or is not served at it.
|
|
41
43
|
*/
|
|
42
|
-
kind?: 'account' | 'key' | 'page' | 'mailbox';
|
|
44
|
+
kind?: 'account' | 'key' | 'page' | 'mailbox' | 'domain';
|
|
43
45
|
handle: string;
|
|
44
46
|
/** Where a reader goes to see the subject. For a mailbox, its `mailto:` address. */
|
|
45
47
|
profileUrl: string;
|
|
@@ -54,7 +56,7 @@ export interface ExternalAccount {
|
|
|
54
56
|
* source could improve on it. A sign-in and a mailed code happen once, between the holder
|
|
55
57
|
* and this backend. The rest publish an artifact a reader can fetch.
|
|
56
58
|
*/
|
|
57
|
-
export type Method = 'declared' | 'oauth' | 'gist' | 'backlink' | 'signature' | 'code';
|
|
59
|
+
export type Method = 'declared' | 'oauth' | 'gist' | 'backlink' | 'dns' | 'wellknown' | 'signature' | 'code';
|
|
58
60
|
/** Who vouches for one side: this backend from its own records, or the account's provider. */
|
|
59
61
|
export type Attester = 'backend' | 'provider';
|
|
60
62
|
/**
|
|
@@ -514,6 +516,11 @@ export declare function attestationLabel(method: string, names: {
|
|
|
514
516
|
site: string;
|
|
515
517
|
provider: string;
|
|
516
518
|
}): string | undefined;
|
|
519
|
+
/**
|
|
520
|
+
* A subject's second identifier, shown beneath its name, or nothing where the name is
|
|
521
|
+
* already all of it: a mailbox's address and a domain are each their own id.
|
|
522
|
+
*/
|
|
523
|
+
export declare function externalId(external: ExternalAccount): string | undefined;
|
|
517
524
|
/**
|
|
518
525
|
* Where a subject's name may link, or nothing where it has no page of its own. A mailbox's
|
|
519
526
|
* address is `mailto:`, which opens a message to it and shows a reader nothing.
|
package/dist/core/index.js
CHANGED
|
@@ -121,10 +121,19 @@ export function attestationLabel(method, names) {
|
|
|
121
121
|
oauth: `Signed in with ${names.provider}`,
|
|
122
122
|
gist: `Published a proof on ${names.provider}`,
|
|
123
123
|
backlink: `Linked back to ${names.site}`,
|
|
124
|
+
dns: `Named ${names.site} in a DNS record`,
|
|
125
|
+
wellknown: `Named ${names.site} in a file it serves`,
|
|
124
126
|
signature: 'Proved with a signature',
|
|
125
127
|
code: 'Entered a code sent to this address',
|
|
126
128
|
}[method];
|
|
127
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* A subject's second identifier, shown beneath its name, or nothing where the name is
|
|
132
|
+
* already all of it: a mailbox's address and a domain are each their own id.
|
|
133
|
+
*/
|
|
134
|
+
export function externalId(external) {
|
|
135
|
+
return external.kind === 'mailbox' || external.kind === 'domain' ? undefined : external.id;
|
|
136
|
+
}
|
|
128
137
|
/**
|
|
129
138
|
* Where a subject's name may link, or nothing where it has no page of its own. A mailbox's
|
|
130
139
|
* address is `mailto:`, which opens a message to it and shows a reader nothing.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ArtifactProvider } from '../core/index.js';
|
|
2
|
+
export interface DomainProviderOptions {
|
|
3
|
+
/** Shown as "Verify with …". */
|
|
4
|
+
name?: string;
|
|
5
|
+
/** Replaces the transport. For `dnsProvider()` that is only ever the resolver. */
|
|
6
|
+
fetch?: typeof fetch;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface WellKnownProviderOptions extends DomainProviderOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Domains this instance will read, lowercase. Omitted means any public host, which is
|
|
12
|
+
* the point of the method and needs Node: see `linkProvider()`.
|
|
13
|
+
*/
|
|
14
|
+
hosts?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Replaces the transport, and with it the check that keeps an instance reading any host
|
|
17
|
+
* out of the network it runs in. Pass one only where it enforces that itself, such as
|
|
18
|
+
* through an egress proxy, or where `hosts` already confines it.
|
|
19
|
+
*/
|
|
20
|
+
fetch?: typeof fetch;
|
|
21
|
+
/** How much of the file is read before giving up on finding the line. */
|
|
22
|
+
maxBytes?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
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
|
|
27
|
+
* checks the address is there. It reaches a domain that serves no page of its own, or only
|
|
28
|
+
* forwards somewhere else, which a link back never can.
|
|
29
|
+
*
|
|
30
|
+
* What it establishes is that whoever wrote the record could write that domain's DNS. That
|
|
31
|
+
* is the domain's holder or somebody they delegated to, and it is the exact name proved:
|
|
32
|
+
* nothing is said about a parent or a subdomain. A domain changes hands like an account,
|
|
33
|
+
* and takes the record with it or drops it.
|
|
34
|
+
*
|
|
35
|
+
* Like a link back, the proof is a standing statement and not a per-flow token, so
|
|
36
|
+
* freshness comes from `recheck()` looking it up again. A lookup that could not be made is
|
|
37
|
+
* unread, never absent.
|
|
38
|
+
*/
|
|
39
|
+
export declare function dnsProvider(options?: DomainProviderOptions): ArtifactProvider;
|
|
40
|
+
/**
|
|
41
|
+
* Proves a domain by a file it serves. The holder puts the local subject's address on a
|
|
42
|
+
* line of `/.well-known/verily.txt` and names the domain; this reads the file and checks
|
|
43
|
+
* the line is there. It is for a holder who can add a file to a site and cannot reach its
|
|
44
|
+
* DNS or its pages' markup.
|
|
45
|
+
*
|
|
46
|
+
* The domain names the host that is fetched, so the read is the web key directory's: HTTPS
|
|
47
|
+
* on the default port, a public address, no redirect followed, a deadline and a bounded
|
|
48
|
+
* read. Only `text/plain` is read, because a line in anything else may be an example of a
|
|
49
|
+
* line and not one.
|
|
50
|
+
*/
|
|
51
|
+
export declare function wellKnownProvider(options?: WellKnownProviderOptions): ArtifactProvider;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { MIMEType } from 'node:util';
|
|
2
|
+
import { Refused, } from '../core/index.js';
|
|
3
|
+
import { discard, readBounded } from './body.js';
|
|
4
|
+
import { transport } from './public-fetch.js';
|
|
5
|
+
/**
|
|
6
|
+
* Domain suffixes that must never be fetched. The holder names the domain, so it names the
|
|
7
|
+
* host, and these are the names that can resolve to something inside the network running
|
|
8
|
+
* this. Reserved suffixes that resolve nowhere are left alone: asking about them is
|
|
9
|
+
* pointless rather than dangerous, and they are what tests use.
|
|
10
|
+
*/
|
|
11
|
+
const reserved = ['local', 'internal', 'localhost', 'home', 'lan', 'corp', 'intranet'];
|
|
12
|
+
const name = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
|
|
13
|
+
/**
|
|
14
|
+
* The one resolver every lookup goes to. Fixed, because the holder chooses the name and
|
|
15
|
+
* nothing else: a resolver they could choose would answer whatever they liked. It is asked
|
|
16
|
+
* over HTTPS because a Worker has no resolver of its own, and at an address that answers a
|
|
17
|
+
* plain GET with JSON, so the lookup that was run is also a link a reader can open.
|
|
18
|
+
*/
|
|
19
|
+
const resolver = 'https://dns.google/resolve';
|
|
20
|
+
/** The label the record is published under, so it sits apart from the domain's own records. */
|
|
21
|
+
const label = '_verily';
|
|
22
|
+
/** Where on a domain the file is served. */
|
|
23
|
+
const path = '/.well-known/verily.txt';
|
|
24
|
+
/** The TXT record type, as the resolver numbers it. */
|
|
25
|
+
const txt = 16;
|
|
26
|
+
/**
|
|
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
|
|
29
|
+
* checks the address is there. It reaches a domain that serves no page of its own, or only
|
|
30
|
+
* forwards somewhere else, which a link back never can.
|
|
31
|
+
*
|
|
32
|
+
* What it establishes is that whoever wrote the record could write that domain's DNS. That
|
|
33
|
+
* is the domain's holder or somebody they delegated to, and it is the exact name proved:
|
|
34
|
+
* nothing is said about a parent or a subdomain. A domain changes hands like an account,
|
|
35
|
+
* and takes the record with it or drops it.
|
|
36
|
+
*
|
|
37
|
+
* Like a link back, the proof is a standing statement and not a per-flow token, so
|
|
38
|
+
* freshness comes from `recheck()` looking it up again. A lookup that could not be made is
|
|
39
|
+
* unread, never absent.
|
|
40
|
+
*/
|
|
41
|
+
export function dnsProvider(options = {}) {
|
|
42
|
+
const request = options.fetch ?? fetch;
|
|
43
|
+
return {
|
|
44
|
+
...shared(options),
|
|
45
|
+
method: 'dns',
|
|
46
|
+
instructions: (expect) => [
|
|
47
|
+
'Add a TXT record to your domain’s DNS with this name and value, then enter the domain below.',
|
|
48
|
+
{ code: label },
|
|
49
|
+
{ code: expect },
|
|
50
|
+
'A new record can take a few minutes to be seen.',
|
|
51
|
+
],
|
|
52
|
+
resolve(artifact) {
|
|
53
|
+
const domain = named(artifact);
|
|
54
|
+
return domain ? lookup(domain) : artifact;
|
|
55
|
+
},
|
|
56
|
+
async verify({ artifact, expect }) {
|
|
57
|
+
const domain = looked(artifact);
|
|
58
|
+
// Why the resolver could not be asked is about the network this runs in.
|
|
59
|
+
const response = await request(lookup(domain), {
|
|
60
|
+
redirect: 'manual',
|
|
61
|
+
headers: { Accept: 'application/dns-json', 'User-Agent': 'Verily-V0' },
|
|
62
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 15000),
|
|
63
|
+
}).catch(() => {
|
|
64
|
+
throw new Refused('DNS could not be read');
|
|
65
|
+
});
|
|
66
|
+
let answer;
|
|
67
|
+
try {
|
|
68
|
+
if (!response.ok)
|
|
69
|
+
throw new Refused('DNS could not be read');
|
|
70
|
+
const { bytes, truncated } = await readBounded(response, 64 * 1024);
|
|
71
|
+
if (truncated)
|
|
72
|
+
throw new Refused('DNS could not be read');
|
|
73
|
+
answer = parsed(new TextDecoder().decode(bytes));
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await discard(response);
|
|
77
|
+
}
|
|
78
|
+
// A cut-off answer may have left out the record that would have matched.
|
|
79
|
+
if (!isRecord(answer) || answer.TC === true)
|
|
80
|
+
throw new Refused('DNS could not be read');
|
|
81
|
+
// The name not existing is an answer; anything else that is not success is no answer.
|
|
82
|
+
if (answer.Status !== 0 && answer.Status !== 3)
|
|
83
|
+
throw new Refused('DNS could not be read');
|
|
84
|
+
const records = Array.isArray(answer.Answer) ? answer.Answer : [];
|
|
85
|
+
if (!records.some((record) => isRecord(record) &&
|
|
86
|
+
record.type === txt &&
|
|
87
|
+
typeof record.data === 'string' &&
|
|
88
|
+
text(record.data) === expect))
|
|
89
|
+
throw new Refused(`No TXT record at ${label}.${domain} names this subject`);
|
|
90
|
+
return account(domain);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Proves a domain by a file it serves. The holder puts the local subject's address on a
|
|
96
|
+
* line of `/.well-known/verily.txt` and names the domain; this reads the file and checks
|
|
97
|
+
* the line is there. It is for a holder who can add a file to a site and cannot reach its
|
|
98
|
+
* DNS or its pages' markup.
|
|
99
|
+
*
|
|
100
|
+
* The domain names the host that is fetched, so the read is the web key directory's: HTTPS
|
|
101
|
+
* on the default port, a public address, no redirect followed, a deadline and a bounded
|
|
102
|
+
* read. Only `text/plain` is read, because a line in anything else may be an example of a
|
|
103
|
+
* line and not one.
|
|
104
|
+
*/
|
|
105
|
+
export function wellKnownProvider(options = {}) {
|
|
106
|
+
const request = transport(options);
|
|
107
|
+
const hosts = options.hosts?.map((host) => host.toLowerCase());
|
|
108
|
+
const maxBytes = options.maxBytes ?? 64 * 1024;
|
|
109
|
+
return {
|
|
110
|
+
...shared(options),
|
|
111
|
+
method: 'wellknown',
|
|
112
|
+
instructions: (expect) => [
|
|
113
|
+
'Serve a plain text file at this path on your domain, with this address on a line of its own, then enter the domain below.',
|
|
114
|
+
{ code: path },
|
|
115
|
+
{ code: expect },
|
|
116
|
+
],
|
|
117
|
+
resolve(artifact) {
|
|
118
|
+
const domain = named(artifact);
|
|
119
|
+
return domain ? `https://${domain}${path}` : artifact;
|
|
120
|
+
},
|
|
121
|
+
async verify({ artifact, expect }) {
|
|
122
|
+
const domain = served(artifact);
|
|
123
|
+
if (hosts && !hosts.includes(domain))
|
|
124
|
+
throw new Refused('Not a host this instance reads');
|
|
125
|
+
const response = await request(`https://${domain}${path}`, {
|
|
126
|
+
// Workers refuse 'error', so a redirect comes back as a response and is refused below.
|
|
127
|
+
redirect: 'manual',
|
|
128
|
+
headers: { Accept: 'text/plain', 'User-Agent': 'Verily-V0' },
|
|
129
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 15000),
|
|
130
|
+
}).catch(() => {
|
|
131
|
+
throw new Refused('File could not be reached');
|
|
132
|
+
});
|
|
133
|
+
try {
|
|
134
|
+
// The holder named this domain, so a redirect would take the check somewhere else.
|
|
135
|
+
if (response.type === 'opaqueredirect' || (response.status >= 300 && response.status < 400))
|
|
136
|
+
throw new Refused('File redirects, and a redirect is not followed');
|
|
137
|
+
if (!response.ok)
|
|
138
|
+
throw new Refused('File unavailable');
|
|
139
|
+
if (mediaType(response.headers.get('content-type')) !== 'text/plain')
|
|
140
|
+
throw new Refused('File is not plain text');
|
|
141
|
+
const { bytes, truncated } = await readBounded(response, maxBytes);
|
|
142
|
+
const lines = new TextDecoder().decode(bytes).split(/\r?\n/);
|
|
143
|
+
// The last line of a file cut short may be the start of some longer address.
|
|
144
|
+
if (truncated)
|
|
145
|
+
lines.pop();
|
|
146
|
+
if (!lines.some((line) => line.trim() === expect))
|
|
147
|
+
throw new Refused(truncated ? 'File is too large to read' : 'File has no line naming this subject');
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
await discard(response);
|
|
151
|
+
}
|
|
152
|
+
return account(domain);
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** What both methods say of themselves: they name the same thing, and differ in how. */
|
|
157
|
+
function shared(options) {
|
|
158
|
+
return {
|
|
159
|
+
id: 'domain',
|
|
160
|
+
name: options.name ?? 'Domain',
|
|
161
|
+
artifact: 'location',
|
|
162
|
+
field: 'Your domain',
|
|
163
|
+
input: 'text',
|
|
164
|
+
// The subject's own address is the entire claim, so there is nothing per-flow to mint.
|
|
165
|
+
expect(local) {
|
|
166
|
+
if (!local.profileUrl)
|
|
167
|
+
throw new Error('A domain proof needs a local subject with a profileUrl to name');
|
|
168
|
+
return local.profileUrl;
|
|
169
|
+
},
|
|
170
|
+
// A domain is where it says it is, so a record of one already says where to look.
|
|
171
|
+
known: (account) => (account.kind === 'domain' ? account.id : undefined),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* A domain, named by itself. The id is the name as DNS compares it: lowercase, and in
|
|
176
|
+
* punycode where it was typed in another script.
|
|
177
|
+
*/
|
|
178
|
+
function account(domain) {
|
|
179
|
+
return { id: domain, kind: 'domain', handle: domain, profileUrl: `https://${domain}/` };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The domain a holder typed, or nothing where it is not one. Taken bare or as the address
|
|
183
|
+
* of its front page, since both are how a domain is written, and never with a path: a path
|
|
184
|
+
* is somebody's page, and this proves no page.
|
|
185
|
+
*/
|
|
186
|
+
function named(typed) {
|
|
187
|
+
const trimmed = typed.trim();
|
|
188
|
+
if (trimmed.length > 300)
|
|
189
|
+
return undefined;
|
|
190
|
+
const url = URL.parse(/^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`);
|
|
191
|
+
if (!url || url.username || url.password || url.port || url.search || url.hash)
|
|
192
|
+
return undefined;
|
|
193
|
+
if (url.pathname !== '/')
|
|
194
|
+
return undefined;
|
|
195
|
+
const domain = url.hostname.toLowerCase().replace(/\.$/, '');
|
|
196
|
+
return valid(domain) ? domain : undefined;
|
|
197
|
+
}
|
|
198
|
+
/** A dotted public-looking name, as DNS limits one, and not an address literal. */
|
|
199
|
+
function valid(domain) {
|
|
200
|
+
return (domain.length <= 253 - label.length - 1 &&
|
|
201
|
+
name.test(domain) &&
|
|
202
|
+
domain.split('.').every((part) => part.length <= 63) &&
|
|
203
|
+
!/^[\d.]+$/.test(domain) &&
|
|
204
|
+
!reserved.includes(domain.split('.').pop()));
|
|
205
|
+
}
|
|
206
|
+
/** The lookup for a domain's record: the address that is fetched, and kept as the proof's. */
|
|
207
|
+
function lookup(domain) {
|
|
208
|
+
return `${resolver}?name=${label}.${domain}&type=TXT`;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The domain a lookup address asks about, once it is exactly a lookup this would make. The
|
|
212
|
+
* address comes back from the holder or from a record, and anything else is somewhere this
|
|
213
|
+
* has no business fetching.
|
|
214
|
+
*/
|
|
215
|
+
function looked(artifact) {
|
|
216
|
+
const url = artifact.length <= 600 ? URL.parse(artifact) : null;
|
|
217
|
+
const asked = url?.searchParams.get('name') ?? '';
|
|
218
|
+
const domain = asked.startsWith(`${label}.`) ? asked.slice(label.length + 1) : '';
|
|
219
|
+
if (!url || !valid(domain) || url.href !== lookup(domain))
|
|
220
|
+
throw new Refused('Not a domain this can look up');
|
|
221
|
+
return domain;
|
|
222
|
+
}
|
|
223
|
+
/** The domain a file address is on, once it is exactly where this would read one. */
|
|
224
|
+
function served(artifact) {
|
|
225
|
+
const url = artifact.length <= 600 ? URL.parse(artifact) : null;
|
|
226
|
+
const domain = url?.hostname.toLowerCase() ?? '';
|
|
227
|
+
if (!url || !valid(domain) || url.href !== `https://${domain}${path}`)
|
|
228
|
+
throw new Refused('Not a domain this can read');
|
|
229
|
+
return domain;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* A TXT record's text. A record is one or more strings joined end to end, and a resolver
|
|
233
|
+
* writes them either already joined or each in quotes with a backslash before anything
|
|
234
|
+
* awkward. One this cannot read is no text at all, since a guess could read as a match.
|
|
235
|
+
*/
|
|
236
|
+
function text(data) {
|
|
237
|
+
if (!data.startsWith('"'))
|
|
238
|
+
return data.trim();
|
|
239
|
+
let out = '';
|
|
240
|
+
let at = 0;
|
|
241
|
+
while (at < data.length) {
|
|
242
|
+
if (data[at] === ' ') {
|
|
243
|
+
at++;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (data[at] !== '"')
|
|
247
|
+
return undefined;
|
|
248
|
+
at++;
|
|
249
|
+
while (at < data.length && data[at] !== '"') {
|
|
250
|
+
if (data[at] === '\\') {
|
|
251
|
+
const code = /^\d{3}/.exec(data.slice(at + 1))?.[0];
|
|
252
|
+
out += code ? String.fromCharCode(Number(code)) : (data[at + 1] ?? '');
|
|
253
|
+
at += code ? 4 : 2;
|
|
254
|
+
}
|
|
255
|
+
else
|
|
256
|
+
out += data[at++];
|
|
257
|
+
}
|
|
258
|
+
if (data[at] !== '"')
|
|
259
|
+
return undefined;
|
|
260
|
+
at++;
|
|
261
|
+
}
|
|
262
|
+
return out.trim();
|
|
263
|
+
}
|
|
264
|
+
function parsed(body) {
|
|
265
|
+
try {
|
|
266
|
+
return JSON.parse(body);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/** A `Content-Type`'s essence, or nothing where it is missing or malformed. */
|
|
273
|
+
function mediaType(header) {
|
|
274
|
+
if (header === null)
|
|
275
|
+
return undefined;
|
|
276
|
+
try {
|
|
277
|
+
return new MIMEType(header).essence;
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function isRecord(value) {
|
|
284
|
+
return value !== null && typeof value === 'object';
|
|
285
|
+
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -7,6 +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
11
|
export { pgpProvider } from './pgp.js';
|
|
11
12
|
export { generateSigningKey, signer } from './signing.js';
|
|
12
13
|
export { emailProvider, type EmailImage, type EmailMessage, type EmailProviderOptions, } from './email.js';
|
package/dist/server/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { attestationLabel, externalLink, externalName, isArtifactProvider, isCodeProvider, isRedirectProvider, lastProved, localSide, proofTitle, providerMethod, statusLabel, signedBy, } from '../core/index.js';
|
|
1
|
+
import { attestationLabel, externalId, externalLink, externalName, isArtifactProvider, isCodeProvider, isRedirectProvider, lastProved, localSide, proofTitle, providerMethod, statusLabel, signedBy, } from '../core/index.js';
|
|
2
2
|
import { artifactAttempts, codeAttempts, VerilyService, Unavailable, } from './service.js';
|
|
3
3
|
import { copyScript, waitScript } from './copy.js';
|
|
4
4
|
import { escape } from './escape.js';
|
|
@@ -10,6 +10,7 @@ export { discordProvider } from './discord.js';
|
|
|
10
10
|
export { youtubeProvider } from './youtube.js';
|
|
11
11
|
export { githubGistProvider } from './github-gist.js';
|
|
12
12
|
export { linkProvider, githubLinkProvider } from './link.js';
|
|
13
|
+
export { dnsProvider, wellKnownProvider, } from './domain.js';
|
|
13
14
|
export { pgpProvider } from './pgp.js';
|
|
14
15
|
export { generateSigningKey, signer } from './signing.js';
|
|
15
16
|
export { emailProvider, } from './email.js';
|
|
@@ -98,6 +99,8 @@ function methodAction(provider) {
|
|
|
98
99
|
oauth: `Sign in with ${provider.name}`,
|
|
99
100
|
gist: `Publish a proof on ${provider.name}`,
|
|
100
101
|
backlink: `Link back from ${provider.name}`,
|
|
102
|
+
dns: 'Add a DNS record',
|
|
103
|
+
wellknown: 'Serve a file',
|
|
101
104
|
signature: `Sign with ${provider.name}`,
|
|
102
105
|
code: `Confirm by ${provider.name.toLowerCase()}`,
|
|
103
106
|
};
|
|
@@ -118,9 +121,13 @@ function artifactField(provider) {
|
|
|
118
121
|
* standing: it goes on the record with this one, and they did not ask for it by name.
|
|
119
122
|
*/
|
|
120
123
|
function standingNote(flow, site) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
:
|
|
124
|
+
const found = {
|
|
125
|
+
backlink: `This account already links back to ${site}.`,
|
|
126
|
+
dns: `This domain already names ${site} in a DNS record.`,
|
|
127
|
+
wellknown: `This domain already names ${site} in a file it serves.`,
|
|
128
|
+
};
|
|
129
|
+
const note = flow.standing?.map((a) => found[a.method]).find((said) => said !== undefined);
|
|
130
|
+
return note ? `${note} Confirming records that too.` : undefined;
|
|
124
131
|
}
|
|
125
132
|
/**
|
|
126
133
|
* Said above a proof that was handed back and refused, while the flow still waits for it:
|
|
@@ -134,9 +141,13 @@ function refusedNote(flow) {
|
|
|
134
141
|
}
|
|
135
142
|
/** Said beside every artifact method: what the holder publishes is public by design. */
|
|
136
143
|
function artifactNote(provider) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
: 'This
|
|
144
|
+
const notes = {
|
|
145
|
+
backlink: 'This link is public, as is the page you point at. Anyone reading either one can follow it here.',
|
|
146
|
+
dns: 'This record is public, as is the page it names. Anyone reading either one can follow it here.',
|
|
147
|
+
wellknown: 'This file is public, as is the page it names. Anyone reading either one can follow it here.',
|
|
148
|
+
};
|
|
149
|
+
return (notes[provider.method] ??
|
|
150
|
+
'This line is public, as is whatever published it. Publish nothing else alongside it.');
|
|
140
151
|
}
|
|
141
152
|
/**
|
|
142
153
|
* Where a mailed code stands, for whichever renderer asks the holder for the next thing:
|
|
@@ -161,6 +172,21 @@ const codeNote = "We'll email this address to confirm it's yours. The address is
|
|
|
161
172
|
function providerNames(providers) {
|
|
162
173
|
return [...new Set(providers.map((p) => p.name))].join(' or ');
|
|
163
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* What the holder would be linking, for the sentence that asks them to: an account with a
|
|
177
|
+
* sign-in provider, a key, an address, a domain. Each is named once, however many ways it
|
|
178
|
+
* can be shown.
|
|
179
|
+
*/
|
|
180
|
+
function heldNames(providers) {
|
|
181
|
+
const held = (p) => ({
|
|
182
|
+
signature: `${p.name} key`,
|
|
183
|
+
code: `${p.name.toLowerCase()} address`,
|
|
184
|
+
dns: 'domain',
|
|
185
|
+
wellknown: 'domain',
|
|
186
|
+
})[providerMethod(p)] ?? `${p.name} account`;
|
|
187
|
+
const names = [...new Set(providers.map(held))];
|
|
188
|
+
return names.length > 1 ? `${names.slice(0, -1).join(', ')} or ${names.at(-1)}` : names[0];
|
|
189
|
+
}
|
|
164
190
|
/** Seconds are the finest thing a record measured in days can mean; milliseconds are noise. */
|
|
165
191
|
const moment = (time) => new Date(time).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
166
192
|
/**
|
|
@@ -243,8 +269,8 @@ function evidencePage(e, base, report, keyId) {
|
|
|
243
269
|
// identifier stays: that one is the provider's word, not the site's own wording.
|
|
244
270
|
card(local.heading, local.value, e.local.profileUrl, undefined, attestationNote(e.attestations.local, names)) +
|
|
245
271
|
card(names.provider, externalName(e.external), externalLink(e.external),
|
|
246
|
-
// A mailbox's address is its name already, so there is no second identifier.
|
|
247
|
-
e.external
|
|
272
|
+
// A mailbox's address or a domain is its name already, so there is no second identifier.
|
|
273
|
+
externalId(e.external), externalNotes(e.attestations, names)) +
|
|
248
274
|
times(e.status === 'retired'
|
|
249
275
|
? [
|
|
250
276
|
['Approved', e.approvedAt],
|
|
@@ -291,7 +317,7 @@ function checkedPage(d, keyId, base) {
|
|
|
291
317
|
const retired = d.version === 2;
|
|
292
318
|
return page(base, retired ? 'Retired connection' : 'Signed record', `<p>${escape(d.verifierName)} signed this record. It shows what stood when it was signed, not what stands now.</p>` +
|
|
293
319
|
card(local.heading, local.value, d.local.profileUrl, undefined, attestationNote(d.attestations.local, names)) +
|
|
294
|
-
card(names.provider, externalName(d.external), externalLink(d.external), d.external
|
|
320
|
+
card(names.provider, externalName(d.external), externalLink(d.external), externalId(d.external), externalNotes(d.attestations, names)) +
|
|
295
321
|
times(retired
|
|
296
322
|
? [
|
|
297
323
|
['Signed', d.issuedAt],
|
|
@@ -592,7 +618,7 @@ export function createVerily(options) {
|
|
|
592
618
|
${user
|
|
593
619
|
? card(subject(user).heading, subject(user).value, user.profileUrl)
|
|
594
620
|
: '<p>Authenticate with the matching external account to review and remove this link.</p>'}
|
|
595
|
-
<p>Confirm the connection between this ${escape(subjectNoun(user?.kind) ?? 'site')} and your ${escape(
|
|
621
|
+
<p>Confirm the connection between this ${escape(subjectNoun(user?.kind) ?? 'site')} and your ${escape(heldNames(offered))}.</p>
|
|
596
622
|
${kind === 'renew' && offered.length > 1 ? '<p class="fine">Renewing another way adds that method beneath the one this connection was first shown by.</p>' : ''}
|
|
597
623
|
${forms}`));
|
|
598
624
|
}
|
|
@@ -685,7 +711,7 @@ export function createVerily(options) {
|
|
|
685
711
|
? 'Add to connection'
|
|
686
712
|
: 'Confirm connection', `
|
|
687
713
|
${card(subject(flow.local).heading, subject(flow.local).value, flow.local.profileUrl)}
|
|
688
|
-
${card(provider.name, externalName(flow.external), externalLink(flow.external), flow.external
|
|
714
|
+
${card(provider.name, externalName(flow.external), externalLink(flow.external), externalId(flow.external))}
|
|
689
715
|
${joined ? `<p>This account is already linked here. Confirming adds this method to that connection, beneath the one it was first shown by, and it stays ${escape(joined.visibility)}.</p>` : ''}
|
|
690
716
|
${found ? `<p>${escape(found)}</p>` : ''}
|
|
691
717
|
<p class="fine">${escape(service.siteOf(flow.local))} receives the result. Verified via ${escape(options.verifierName)}.</p>
|
package/dist/server/link.js
CHANGED
|
@@ -4,7 +4,7 @@ import { MIMEType } from 'node:util';
|
|
|
4
4
|
import { html, parse } from 'parse5';
|
|
5
5
|
import { Refused, } from '../core/index.js';
|
|
6
6
|
import { discard, readBounded } from './body.js';
|
|
7
|
-
import {
|
|
7
|
+
import { transport } from './public-fetch.js';
|
|
8
8
|
/**
|
|
9
9
|
* Domain suffixes that must never be fetched. The holder names the address, so it names
|
|
10
10
|
* the host, and these are the names that can resolve to something inside the network
|
|
@@ -134,21 +134,6 @@ async function backlinked(response, page, subject, maxBytes) {
|
|
|
134
134
|
// Not finding it in part of a page is not the same as it not being there.
|
|
135
135
|
throw new Refused(truncated ? 'Page is too large to read' : 'Page has no rel="me" link to this subject');
|
|
136
136
|
}
|
|
137
|
-
/**
|
|
138
|
-
* How pages are fetched. A deployment that named its hosts has vouched for where they
|
|
139
|
-
* resolve, so the platform's own fetch will do. One that reads any host has vouched for
|
|
140
|
-
* nothing, and the holder picks the name: every address it resolves to is checked, on the
|
|
141
|
-
* connection that uses it, which only Node lets this do.
|
|
142
|
-
*/
|
|
143
|
-
function transport(options) {
|
|
144
|
-
if (options.fetch)
|
|
145
|
-
return options.fetch;
|
|
146
|
-
if (options.hosts)
|
|
147
|
-
return (url, init) => fetch(url, init);
|
|
148
|
-
if (!pinnable())
|
|
149
|
-
throw new Error('Reading any host needs Node, which lets it check where a name resolves; name the hosts to read instead');
|
|
150
|
-
return publicFetch();
|
|
151
|
-
}
|
|
152
137
|
/**
|
|
153
138
|
* The address the holder handed back, once it is one this may fetch. They choose it, so
|
|
154
139
|
* an unguarded fetch here is an open proxy: HTTPS only, a public-looking name rather than
|
|
@@ -19,5 +19,15 @@ export type Fetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
|
19
19
|
* `allowed` decides which addresses may be connected to, and is only for tests.
|
|
20
20
|
*/
|
|
21
21
|
export declare function publicFetch(resolve?: Resolve, allowed?: (address: string) => boolean): Fetch;
|
|
22
|
+
/**
|
|
23
|
+
* How an address the holder named is fetched. A deployment that named its hosts has
|
|
24
|
+
* vouched for where they resolve, so the platform's own fetch will do. One that reads any
|
|
25
|
+
* host has vouched for nothing, and the holder picks the name: every address it resolves
|
|
26
|
+
* to is checked, on the connection that uses it, which only Node lets this do.
|
|
27
|
+
*/
|
|
28
|
+
export declare function transport(options: {
|
|
29
|
+
fetch?: typeof fetch;
|
|
30
|
+
hosts?: string[];
|
|
31
|
+
}): Fetch;
|
|
22
32
|
/** Whether this runtime is Node, the only one that lets `publicFetch()` pin its lookup. */
|
|
23
33
|
export declare function pinnable(): boolean;
|
|
@@ -37,6 +37,21 @@ export function publicFetch(resolve = system, allowed) {
|
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* How an address the holder named is fetched. A deployment that named its hosts has
|
|
42
|
+
* vouched for where they resolve, so the platform's own fetch will do. One that reads any
|
|
43
|
+
* host has vouched for nothing, and the holder picks the name: every address it resolves
|
|
44
|
+
* to is checked, on the connection that uses it, which only Node lets this do.
|
|
45
|
+
*/
|
|
46
|
+
export function transport(options) {
|
|
47
|
+
if (options.fetch)
|
|
48
|
+
return options.fetch;
|
|
49
|
+
if (options.hosts)
|
|
50
|
+
return (url, init) => fetch(url, init);
|
|
51
|
+
if (!pinnable())
|
|
52
|
+
throw new Error('Reading any host needs Node, which lets it check where a name resolves; name the hosts to read instead');
|
|
53
|
+
return publicFetch();
|
|
54
|
+
}
|
|
40
55
|
/** A lookup that answers only with addresses that are all permitted, or not at all. */
|
|
41
56
|
function pinned(resolve, permitted) {
|
|
42
57
|
return (hostname, options, callback) => {
|
package/dist/verily.js
CHANGED
|
@@ -9575,6 +9575,7 @@ var Verily = (() => {
|
|
|
9575
9575
|
__export(browser_exports, {
|
|
9576
9576
|
Refused: () => Refused,
|
|
9577
9577
|
attestationLabel: () => attestationLabel,
|
|
9578
|
+
externalId: () => externalId,
|
|
9578
9579
|
externalLink: () => externalLink,
|
|
9579
9580
|
externalName: () => externalName,
|
|
9580
9581
|
fresh: () => fresh,
|
|
@@ -9715,10 +9716,15 @@ var Verily = (() => {
|
|
|
9715
9716
|
oauth: `Signed in with ${names.provider}`,
|
|
9716
9717
|
gist: `Published a proof on ${names.provider}`,
|
|
9717
9718
|
backlink: `Linked back to ${names.site}`,
|
|
9719
|
+
dns: `Named ${names.site} in a DNS record`,
|
|
9720
|
+
wellknown: `Named ${names.site} in a file it serves`,
|
|
9718
9721
|
signature: "Proved with a signature",
|
|
9719
9722
|
code: "Entered a code sent to this address"
|
|
9720
9723
|
}[method];
|
|
9721
9724
|
}
|
|
9725
|
+
function externalId(external) {
|
|
9726
|
+
return external.kind === "mailbox" || external.kind === "domain" ? void 0 : external.id;
|
|
9727
|
+
}
|
|
9722
9728
|
function externalLink(external) {
|
|
9723
9729
|
return external.kind === "mailbox" ? void 0 : external.profileUrl;
|
|
9724
9730
|
}
|
|
@@ -9882,7 +9888,7 @@ var Verily = (() => {
|
|
|
9882
9888
|
openpgp: key,
|
|
9883
9889
|
email: envelope
|
|
9884
9890
|
};
|
|
9885
|
-
return marks2[provider]?.() ?? (method
|
|
9891
|
+
return marks2[provider]?.() ?? (["backlink", "dns", "wellknown"].includes(method ?? "") ? globe() : void 0);
|
|
9886
9892
|
}
|
|
9887
9893
|
|
|
9888
9894
|
// src/browser/badge.ts
|
|
@@ -10428,7 +10434,7 @@ ${claims(evidence)}`;
|
|
|
10428
10434
|
}
|
|
10429
10435
|
|
|
10430
10436
|
// src/version.ts
|
|
10431
|
-
var version = "v0.
|
|
10437
|
+
var version = "v0.4.0";
|
|
10432
10438
|
|
|
10433
10439
|
// src/browser/evidence-dialog.ts
|
|
10434
10440
|
function stampLink() {
|
|
@@ -10737,8 +10743,8 @@ ${claims(evidence)}`;
|
|
|
10737
10743
|
// would print it twice here, since this heading writes it either way.
|
|
10738
10744
|
logo2 ? [logo2, document.createTextNode(provider)] : [document.createTextNode(provider)],
|
|
10739
10745
|
externalName(evidence.external),
|
|
10740
|
-
// A mailbox's address is its name already, so there is no second identifier.
|
|
10741
|
-
evidence.external
|
|
10746
|
+
// A mailbox's address or a domain is its name already, so there is no second identifier.
|
|
10747
|
+
externalId(evidence.external),
|
|
10742
10748
|
externalLink(evidence.external),
|
|
10743
10749
|
// Status first, then how it was shown, then when. The proof explains the state
|
|
10744
10750
|
// above it, so it cannot sit before that state has been given.
|
|
@@ -11353,7 +11359,7 @@ ${claims(evidence)}`;
|
|
|
11353
11359
|
const external = accountCard(
|
|
11354
11360
|
logo2 ? [logo2, document.createTextNode(flow.provider.name)] : [document.createTextNode(flow.provider.name)],
|
|
11355
11361
|
externalName(flow.external),
|
|
11356
|
-
flow.external
|
|
11362
|
+
externalId(flow.external),
|
|
11357
11363
|
externalLink(flow.external)
|
|
11358
11364
|
);
|
|
11359
11365
|
const parts = [subjectCard(flow.local), linkMark(), external];
|
|
@@ -11914,7 +11920,7 @@ ${claims(evidence)}`;
|
|
|
11914
11920
|
(k2) => typeof value.external[k2] === "string"
|
|
11915
11921
|
) && (value.local.profileUrl === void 0 || typeof value.local.profileUrl === "string") && // A signed record lives beside the record it was made from and nowhere else, since the
|
|
11916
11922
|
// keys it is checked against are read from where the record lives.
|
|
11917
|
-
(value.signedUrl === void 0 || value.signedUrl === `${value.evidenceUrl}?format=signed`) && validAttestations(value.attestations) && (value.local.kind === void 0 || typeof value.local.kind === "string") && (value.external.kind === void 0 || ["account", "key", "page", "mailbox"].includes(String(value.external.kind))) && (value.revokedAt === void 0 || typeof value.revokedAt === "number" && Number.isFinite(value.revokedAt)) && ["verified", "unconfirmed", "expired", "retired", "revoked"].includes(String(value.status)) && // A retired record stands on its dates, so one that does not say when is not one.
|
|
11923
|
+
(value.signedUrl === void 0 || value.signedUrl === `${value.evidenceUrl}?format=signed`) && validAttestations(value.attestations) && (value.local.kind === void 0 || typeof value.local.kind === "string") && (value.external.kind === void 0 || ["account", "key", "page", "mailbox", "domain"].includes(String(value.external.kind))) && (value.revokedAt === void 0 || typeof value.revokedAt === "number" && Number.isFinite(value.revokedAt)) && ["verified", "unconfirmed", "expired", "retired", "revoked"].includes(String(value.status)) && // A retired record stands on its dates, so one that does not say when is not one.
|
|
11918
11924
|
(value.status === "retired" ? typeof value.retiredAt === "number" && Number.isFinite(value.retiredAt) : value.retiredAt === void 0) && (value.mark === void 0 || ["preferred", "unused"].includes(String(value.mark))) && ["public", "unlisted"].includes(String(value.visibility)) && (value.connectedAt === void 0 || typeof value.connectedAt === "number" && Number.isFinite(value.connectedAt)) && ["authenticatedAt", "approvedAt", "expiresAt"].every(
|
|
11919
11925
|
(k2) => typeof value[k2] === "number" && Number.isFinite(value[k2])
|
|
11920
11926
|
);
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED