@bkazemi/verily 0.1.0 → 0.3.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 +42 -27
- package/dist/browser/badge.js +16 -5
- package/dist/browser/connect-dialog.d.ts +21 -2
- package/dist/browser/connect-dialog.js +28 -13
- package/dist/browser/evidence-dialog.d.ts +19 -1
- package/dist/browser/evidence-dialog.js +163 -16
- package/dist/browser/index.d.ts +10 -1
- package/dist/browser/index.js +57 -13
- package/dist/browser/logo.d.ts +2 -0
- package/dist/browser/logo.js +2 -0
- package/dist/browser/mark.d.ts +2 -0
- package/dist/browser/mark.js +6 -0
- package/dist/browser/signed.d.ts +4 -0
- package/dist/browser/signed.js +23 -6
- package/dist/core/index.d.ts +76 -4
- package/dist/core/index.js +14 -0
- package/dist/core/signed.d.ts +20 -5
- package/dist/core/signed.js +5 -2
- package/dist/server/copy.d.ts +6 -0
- package/dist/server/copy.js +20 -0
- package/dist/server/discord.js +3 -1
- package/dist/server/github.js +4 -2
- package/dist/server/index.js +134 -50
- package/dist/server/link.js +24 -4
- package/dist/server/service.d.ts +92 -6
- package/dist/server/service.js +362 -19
- package/dist/server/youtube.js +3 -1
- package/dist/site/index.d.ts +2 -1
- package/dist/verily.js +212 -37
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/core/index.d.ts
CHANGED
|
@@ -2,9 +2,16 @@ export type Visibility = 'public' | 'unlisted';
|
|
|
2
2
|
/**
|
|
3
3
|
* `unconfirmed` is a published proof that has not been read lately. It is not disproved,
|
|
4
4
|
* and turns back to `verified` the moment the proof reads again. `expired` is an approval
|
|
5
|
-
* that ran out, which only the holder renewing it undoes.
|
|
5
|
+
* that ran out, which only the holder renewing it undoes. `retired` is an account its
|
|
6
|
+
* holder said is finished: the record is kept as it stood at its last proof and never
|
|
7
|
+
* reads as verified again, unless that proof is made afresh.
|
|
6
8
|
*/
|
|
7
|
-
export type Status = 'verified' | 'unconfirmed' | 'expired' | 'revoked';
|
|
9
|
+
export type Status = 'verified' | 'unconfirmed' | 'expired' | 'retired' | 'revoked';
|
|
10
|
+
/**
|
|
11
|
+
* How the holder would have an account listed: the one to lead with, or one they still
|
|
12
|
+
* control and no longer use. The holder's own statement, and no part of what was verified.
|
|
13
|
+
*/
|
|
14
|
+
export type Mark = 'preferred' | 'unused';
|
|
8
15
|
/** What a site links: one of its accounts, a single page, or the site itself. */
|
|
9
16
|
export type LocalKind = 'account' | 'page' | 'site';
|
|
10
17
|
/** id is private and must never be reassigned. reference is durable and public-safe. */
|
|
@@ -106,6 +113,13 @@ export interface Connection {
|
|
|
106
113
|
attestations?: Attestations;
|
|
107
114
|
/** The proof itself, for a method whose artifact this backend publishes rather than reads. */
|
|
108
115
|
proof?: string;
|
|
116
|
+
/** Set and cleared by the holder. Absent is an ordinary current account. */
|
|
117
|
+
mark?: Mark;
|
|
118
|
+
/**
|
|
119
|
+
* When the holder said the account is finished. From then the record is frozen at its
|
|
120
|
+
* last proof: never renewed by another method, never reread, never verified.
|
|
121
|
+
*/
|
|
122
|
+
retiredAt?: number;
|
|
109
123
|
}
|
|
110
124
|
export interface Flow {
|
|
111
125
|
id: string;
|
|
@@ -127,8 +141,30 @@ export interface Flow {
|
|
|
127
141
|
* unguessable and per-flow, so an artifact made for one flow cannot complete another.
|
|
128
142
|
*/
|
|
129
143
|
expect?: string;
|
|
130
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* What the holder handed back: an address to read, or the proof itself. On a flow still
|
|
146
|
+
* waiting it is what they last handed back and had refused, kept so they can put it right.
|
|
147
|
+
*/
|
|
131
148
|
artifact?: string;
|
|
149
|
+
/** How many times what the holder handed back has been refused. */
|
|
150
|
+
tries?: number;
|
|
151
|
+
/**
|
|
152
|
+
* Whether the flow was started by a request shown to come from the holder's own page,
|
|
153
|
+
* and not by an address anyone could have sent their browser to. Only such a flow may
|
|
154
|
+
* be recorded without an approval, which is otherwise the one step a link cannot press.
|
|
155
|
+
*/
|
|
156
|
+
attended?: boolean;
|
|
157
|
+
/**
|
|
158
|
+
* What the holder is offered to hand back, where a record of theirs already names the
|
|
159
|
+
* account. Only an offer: what is verified is whatever they then submit.
|
|
160
|
+
*/
|
|
161
|
+
suggested?: string;
|
|
162
|
+
/**
|
|
163
|
+
* Proofs found already standing when another method named the account, such as a link
|
|
164
|
+
* back on the profile of an account that just signed in. Recorded with the method that
|
|
165
|
+
* found them once the holder approves.
|
|
166
|
+
*/
|
|
167
|
+
standing?: Attestation[];
|
|
132
168
|
/**
|
|
133
169
|
* A message on its way to the address the holder named, carrying a link to press and a
|
|
134
170
|
* code to type. The account is only a claim until one of them comes back, which is why
|
|
@@ -224,6 +260,11 @@ export interface RedirectProvider {
|
|
|
224
260
|
state: string;
|
|
225
261
|
challenge: string;
|
|
226
262
|
redirectUri: string;
|
|
263
|
+
/**
|
|
264
|
+
* The account the flow must show, on a flow the holder of its record started. There is
|
|
265
|
+
* no account to choose then, so the provider need not be made to offer the choice.
|
|
266
|
+
*/
|
|
267
|
+
account?: ExternalAccount;
|
|
227
268
|
}): string;
|
|
228
269
|
authenticate(input: {
|
|
229
270
|
code: string;
|
|
@@ -261,6 +302,25 @@ export interface ArtifactProvider {
|
|
|
261
302
|
* character, and prose that reflows is prose a holder cannot safely copy.
|
|
262
303
|
*/
|
|
263
304
|
instructions(expect: string): Instruction[];
|
|
305
|
+
/**
|
|
306
|
+
* What the holder is asked to hand back, where the method takes something a holder knows
|
|
307
|
+
* by a better name than "an address". `input` says how to ask for it: `text` where it
|
|
308
|
+
* need not be an address at all, such as a username.
|
|
309
|
+
*/
|
|
310
|
+
field?: string;
|
|
311
|
+
input?: 'url' | 'text';
|
|
312
|
+
/**
|
|
313
|
+
* Turns what the holder typed into the address to read, for a method that takes something
|
|
314
|
+
* shorter. What it returns is what is verified and kept as the proof's location, so it
|
|
315
|
+
* must be the whole address. Anything it does not recognise is returned as it came.
|
|
316
|
+
*/
|
|
317
|
+
resolve?(artifact: string): string;
|
|
318
|
+
/**
|
|
319
|
+
* What to hand back for an account the subject is already linked to, where the account
|
|
320
|
+
* alone says so: a profile that carries the link is at the account's own address. The
|
|
321
|
+
* holder is offered it instead of being asked, and may still hand back something else.
|
|
322
|
+
*/
|
|
323
|
+
known?(account: ExternalAccount): string | undefined;
|
|
264
324
|
/**
|
|
265
325
|
* Reads what the holder handed back and returns whose it is. Must confirm it contains
|
|
266
326
|
* `expect`. A `location` provider must also refuse any address outside itself, since
|
|
@@ -380,10 +440,17 @@ export interface Evidence {
|
|
|
380
440
|
visibilityApprovedAt: number;
|
|
381
441
|
expiresAt: number;
|
|
382
442
|
revokedAt?: number;
|
|
443
|
+
/**
|
|
444
|
+
* What the holder says of the account, and when they said it was finished. Both are the
|
|
445
|
+
* holder's statements, beside the attestations and never among them: nothing checked them.
|
|
446
|
+
*/
|
|
447
|
+
mark?: Mark;
|
|
448
|
+
retiredAt?: number;
|
|
383
449
|
evidenceUrl: string;
|
|
384
450
|
/**
|
|
385
451
|
* Where this record can be had signed, on a verifier that signs. Only a public record
|
|
386
|
-
* that stands has a signed form: once saved, a signed record cannot be
|
|
452
|
+
* that stands, or one retired, has a signed form: once saved, a signed record cannot be
|
|
453
|
+
* taken back.
|
|
387
454
|
*/
|
|
388
455
|
signedUrl?: string;
|
|
389
456
|
}
|
|
@@ -406,6 +473,11 @@ export declare function status(connection: Connection, now: number, freshness?:
|
|
|
406
473
|
* reverses the moment it reads again.
|
|
407
474
|
*/
|
|
408
475
|
export declare function fresh(attestation: Attestation, now: number, freshness?: number): boolean;
|
|
476
|
+
/**
|
|
477
|
+
* The date a retired record stands on: when its account was last proved. A sign-in was
|
|
478
|
+
* proved when it happened, and a published proof when it was last read.
|
|
479
|
+
*/
|
|
480
|
+
export declare function lastProved(evidence: Pick<Evidence, 'authenticatedAt' | 'attestations'>): number;
|
|
409
481
|
/**
|
|
410
482
|
* The word for a record's state. Evidence can be read after it was issued, so an approval
|
|
411
483
|
* that has run out since reads as expired whatever status it was issued with.
|
package/dist/core/index.js
CHANGED
|
@@ -35,6 +35,9 @@ export const freshnessMs = 7 * 86400000;
|
|
|
35
35
|
export function status(connection, now, freshness = freshnessMs) {
|
|
36
36
|
if (connection.revokedAt !== undefined)
|
|
37
37
|
return 'revoked';
|
|
38
|
+
// Frozen at its last proof, so neither its expiry nor its freshness is read again.
|
|
39
|
+
if (connection.retiredAt !== undefined)
|
|
40
|
+
return 'retired';
|
|
38
41
|
if (now >= connection.expiresAt)
|
|
39
42
|
return 'expired';
|
|
40
43
|
const main = connection.attestations?.external[0];
|
|
@@ -53,6 +56,14 @@ export function fresh(attestation, now, freshness = freshnessMs) {
|
|
|
53
56
|
freshness === Infinity ||
|
|
54
57
|
now < attestation.confirmedAt + freshness);
|
|
55
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The date a retired record stands on: when its account was last proved. A sign-in was
|
|
61
|
+
* proved when it happened, and a published proof when it was last read.
|
|
62
|
+
*/
|
|
63
|
+
export function lastProved(evidence) {
|
|
64
|
+
const main = evidence.attestations.external[0];
|
|
65
|
+
return main.artifactUrl ? main.confirmedAt : evidence.authenticatedAt;
|
|
66
|
+
}
|
|
56
67
|
/**
|
|
57
68
|
* The word for a record's state. Evidence can be read after it was issued, so an approval
|
|
58
69
|
* that has run out since reads as expired whatever status it was issued with.
|
|
@@ -60,6 +71,9 @@ export function fresh(attestation, now, freshness = freshnessMs) {
|
|
|
60
71
|
export function statusLabel(evidence, now) {
|
|
61
72
|
if (evidence.status === 'revoked')
|
|
62
73
|
return 'Revoked';
|
|
74
|
+
// Before expiry: a retired record's approval has long run out, and that is not its state.
|
|
75
|
+
if (evidence.status === 'retired')
|
|
76
|
+
return 'Retired';
|
|
63
77
|
if (evidence.status === 'expired' || evidence.expiresAt <= now)
|
|
64
78
|
return 'Expired';
|
|
65
79
|
return evidence.status === 'unconfirmed' ? 'Unconfirmed' : 'Verified';
|
package/dist/core/signed.d.ts
CHANGED
|
@@ -11,15 +11,29 @@ export interface VerifierKey {
|
|
|
11
11
|
publicKey: string;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* What
|
|
15
|
-
*
|
|
16
|
-
* to ask.
|
|
14
|
+
* What either version says of a record. The holder's mark is left out: it is about how
|
|
15
|
+
* their accounts are listed, and no part of what was verified.
|
|
17
16
|
*/
|
|
18
|
-
|
|
17
|
+
type Signable = Omit<Evidence, 'status' | 'revokedAt' | 'signedUrl' | 'mark' | 'retiredAt'> & {
|
|
19
18
|
type: 'verily-evidence';
|
|
20
|
-
version: 1;
|
|
21
19
|
issuedAt: number;
|
|
22
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* What a signature covers: the record as it stood at `issuedAt`. Whether it still stands
|
|
23
|
+
* is a live fact no signature holds, so version 1 leaves its status out, means the record
|
|
24
|
+
* stood when it was signed, and gives `evidenceUrl` as where to ask.
|
|
25
|
+
*
|
|
26
|
+
* Version 2 is a retired record, which did not stand when it was signed and says so. It is
|
|
27
|
+
* a version of its own because a reader of version 1 ignores fields it does not know, and
|
|
28
|
+
* would read a retired record signed as version 1 as one that stood.
|
|
29
|
+
*/
|
|
30
|
+
export type SignedDocument = (Signable & {
|
|
31
|
+
version: 1;
|
|
32
|
+
}) | (Signable & {
|
|
33
|
+
version: 2;
|
|
34
|
+
status: 'retired';
|
|
35
|
+
retiredAt: number;
|
|
36
|
+
});
|
|
23
37
|
/**
|
|
24
38
|
* A record that can be checked away from the verifier that served it: the document as
|
|
25
39
|
* text under an OpenPGP cleartext signature, which `gpg --verify` reads as it is.
|
|
@@ -47,3 +61,4 @@ export declare function signedBy(signed: unknown): Promise<string | undefined>;
|
|
|
47
61
|
* of it, so the fingerprint a record names is the one a list holds.
|
|
48
62
|
*/
|
|
49
63
|
export declare function verifySigned(signed: unknown, keys: VerifierKey[]): Promise<SignedDocument | undefined>;
|
|
64
|
+
export {};
|
package/dist/core/signed.js
CHANGED
|
@@ -100,8 +100,11 @@ export async function verifySigned(signed, keys) {
|
|
|
100
100
|
await signatures[0].verified;
|
|
101
101
|
const document = JSON.parse(data);
|
|
102
102
|
return document?.type === 'verily-evidence' &&
|
|
103
|
-
document.
|
|
104
|
-
|
|
103
|
+
typeof document.issuedAt === 'number' &&
|
|
104
|
+
(document.version === 1 ||
|
|
105
|
+
(document.version === 2 &&
|
|
106
|
+
document.status === 'retired' &&
|
|
107
|
+
typeof document.retiredAt === 'number'))
|
|
105
108
|
? document
|
|
106
109
|
: undefined;
|
|
107
110
|
}
|
package/dist/server/copy.d.ts
CHANGED
|
@@ -6,3 +6,9 @@
|
|
|
6
6
|
* Served as a file because these pages allow no inline scripts.
|
|
7
7
|
*/
|
|
8
8
|
export declare const copyScript = "for (const block of document.querySelectorAll('pre')) {\n const code = block.querySelector('code');\n\n if (!code) continue;\n\n const button = document.createElement('button');\n\n button.type = 'button';\n button.className = 'copy';\n button.textContent = 'Copy';\n button.setAttribute('aria-live', 'polite');\n\n button.addEventListener('click', async () => {\n try {\n await navigator.clipboard.writeText(code.textContent);\n button.textContent = 'Copied';\n } catch {\n // No clipboard permission, or an insecure origin. Select it instead, which leaves\n // the holder one keystroke away rather than telling them it worked when it did not.\n // The label stays short so the button never grows over the line it sits on.\n const range = document.createRange();\n const selection = getSelection();\n\n range.selectNodeContents(code);\n selection.removeAllRanges();\n selection.addRange(range);\n button.textContent = 'Selected';\n }\n\n setTimeout(() => (button.textContent = 'Copy'), 2000);\n });\n\n block.prepend(button);\n}\n";
|
|
9
|
+
/**
|
|
10
|
+
* Moves a page that is waiting on a mailed link along once the link has been pressed,
|
|
11
|
+
* wherever it was pressed. Nothing is redrawn until then, so a code half typed is left
|
|
12
|
+
* alone. Without this the page still has its own link to press.
|
|
13
|
+
*/
|
|
14
|
+
export declare const waitScript = "const timer = setInterval(async () => {\n try {\n const response = await fetch(location.pathname + '/phase', { credentials: 'same-origin' });\n\n // Still exchanging means the link was pressed and is being recorded: the page that\n // loaded now would show neither the step before nor the one after.\n if (response.ok && !['pending', 'exchanging'].includes((await response.json()).phase)) {\n clearInterval(timer);\n location.reload();\n }\n } catch {\n // A read that fails says nothing about the flow, which the next one may.\n }\n}, 2000);\n";
|
package/dist/server/copy.js
CHANGED
|
@@ -40,3 +40,23 @@ export const copyScript = `for (const block of document.querySelectorAll('pre'))
|
|
|
40
40
|
block.prepend(button);
|
|
41
41
|
}
|
|
42
42
|
`;
|
|
43
|
+
/**
|
|
44
|
+
* Moves a page that is waiting on a mailed link along once the link has been pressed,
|
|
45
|
+
* wherever it was pressed. Nothing is redrawn until then, so a code half typed is left
|
|
46
|
+
* alone. Without this the page still has its own link to press.
|
|
47
|
+
*/
|
|
48
|
+
export const waitScript = `const timer = setInterval(async () => {
|
|
49
|
+
try {
|
|
50
|
+
const response = await fetch(location.pathname + '/phase', { credentials: 'same-origin' });
|
|
51
|
+
|
|
52
|
+
// Still exchanging means the link was pressed and is being recorded: the page that
|
|
53
|
+
// loaded now would show neither the step before nor the one after.
|
|
54
|
+
if (response.ok && !['pending', 'exchanging'].includes((await response.json()).phase)) {
|
|
55
|
+
clearInterval(timer);
|
|
56
|
+
location.reload();
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// A read that fails says nothing about the flow, which the next one may.
|
|
60
|
+
}
|
|
61
|
+
}, 2000);
|
|
62
|
+
`;
|
package/dist/server/discord.js
CHANGED
|
@@ -10,7 +10,9 @@ export function discordProvider(options) {
|
|
|
10
10
|
authorizationUrl({ state, challenge, redirectUri }) {
|
|
11
11
|
const url = new URL('https://discord.com/oauth2/authorize');
|
|
12
12
|
// Discord has no account chooser. Forcing the consent screen at least shows the holder
|
|
13
|
-
// which account they are signed in as before it is linked.
|
|
13
|
+
// which account they are signed in as before it is linked. It is forced on a flow on
|
|
14
|
+
// a record too: the holder may have withdrawn the grant since, and asking silently
|
|
15
|
+
// for one that is gone fails with no way for them to give it again.
|
|
14
16
|
url.search = new URLSearchParams({
|
|
15
17
|
response_type: 'code',
|
|
16
18
|
client_id: options.clientId,
|
package/dist/server/github.js
CHANGED
|
@@ -7,8 +7,10 @@ export function githubProvider(options) {
|
|
|
7
7
|
id: 'github',
|
|
8
8
|
name: 'GitHub',
|
|
9
9
|
method: 'oauth',
|
|
10
|
-
authorizationUrl({ state, challenge, redirectUri }) {
|
|
10
|
+
authorizationUrl({ state, challenge, redirectUri, account }) {
|
|
11
11
|
const url = new URL('https://github.com/login/oauth/authorize');
|
|
12
|
+
// A new link is to whichever account the holder picks. A flow on a record already
|
|
13
|
+
// names its account, so GitHub is asked for that one and not made to offer a choice.
|
|
12
14
|
url.search = new URLSearchParams({
|
|
13
15
|
client_id: options.clientId,
|
|
14
16
|
redirect_uri: redirectUri,
|
|
@@ -16,7 +18,7 @@ export function githubProvider(options) {
|
|
|
16
18
|
code_challenge: challenge,
|
|
17
19
|
code_challenge_method: 'S256',
|
|
18
20
|
scope: '',
|
|
19
|
-
prompt: 'select_account',
|
|
21
|
+
...(account ? { login: account.handle } : { prompt: 'select_account' }),
|
|
20
22
|
}).toString();
|
|
21
23
|
return url.href;
|
|
22
24
|
},
|