@bkazemi/verily 0.2.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 +23 -16
- package/dist/browser/badge.js +16 -5
- package/dist/browser/connect-dialog.d.ts +13 -2
- package/dist/browser/connect-dialog.js +17 -7
- 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 +29 -3
- 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/index.js +69 -31
- package/dist/server/service.d.ts +42 -3
- package/dist/server/service.js +156 -10
- package/dist/site/index.d.ts +2 -1
- package/dist/verily.js +203 -32
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/browser/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Evidence } from '../core/index.js';
|
|
2
|
+
import { type Listing } from './evidence-dialog.js';
|
|
2
3
|
export type { Evidence } from '../core/index.js';
|
|
3
4
|
export interface Result {
|
|
4
5
|
outcome: 'complete' | 'cancelled' | 'failed';
|
|
@@ -29,7 +30,10 @@ export declare function init({ backendUrl, handoffUrl }: {
|
|
|
29
30
|
* Opens the connect dialog over the current page. Resolves when it closes, with how
|
|
30
31
|
* the last attempt in it ended. Call from a click: a sign-in method opens a window.
|
|
31
32
|
*/
|
|
32
|
-
openConnect(opener?: HTMLElement, back?: () => void, provider?: string
|
|
33
|
+
openConnect(opener?: HTMLElement, back?: () => void, provider?: string, renew?: {
|
|
34
|
+
id: string;
|
|
35
|
+
method?: string;
|
|
36
|
+
}): Promise<Result>;
|
|
33
37
|
/**
|
|
34
38
|
* Draws the pill that opens the connect dialog. The host receives a `verily-result`
|
|
35
39
|
* event, whose detail is the Result, whenever the dialog closes on a new connection.
|
|
@@ -54,6 +58,11 @@ export declare function init({ backendUrl, handoffUrl }: {
|
|
|
54
58
|
connectionIds: string[];
|
|
55
59
|
} & Arrangement): Promise<void>;
|
|
56
60
|
disconnect(id: string): Promise<unknown>;
|
|
61
|
+
/**
|
|
62
|
+
* Says how the holder would have an account listed, or retires it. Names one record
|
|
63
|
+
* and acts on the account: every record of it the holder has.
|
|
64
|
+
*/
|
|
65
|
+
mark(id: string, as: Listing): Promise<unknown>;
|
|
57
66
|
issueShare: (id: string) => Promise<unknown>;
|
|
58
67
|
revokeShare: (id: string) => Promise<unknown>;
|
|
59
68
|
};
|
package/dist/browser/index.js
CHANGED
|
@@ -119,9 +119,11 @@ function arrange(element, { stacked, peek }) {
|
|
|
119
119
|
}
|
|
120
120
|
const live = (evidence) => evidence.status === 'verified' && evidence.expiresAt > Date.now();
|
|
121
121
|
/**
|
|
122
|
-
* A subject's accounts, one entry for each
|
|
123
|
-
*
|
|
124
|
-
*
|
|
122
|
+
* A subject's accounts, one entry for each. The one its holder prefers comes first, then
|
|
123
|
+
* the rest in the order they were connected, earliest first, then those the holder no
|
|
124
|
+
* longer uses, and last those retired. Records are not accounts: the same account can
|
|
125
|
+
* stand on several, shown a second way, or revoked once and connected again. Those are one
|
|
126
|
+
* account and get one card.
|
|
125
127
|
*
|
|
126
128
|
* Where any of an account's records is verified now, the earliest of those speaks for it,
|
|
127
129
|
* and the ways the others show it are listed beneath as other methods. Its lapsed
|
|
@@ -159,14 +161,26 @@ function accounts(records) {
|
|
|
159
161
|
sources: verified.length ? verified : [base],
|
|
160
162
|
};
|
|
161
163
|
})
|
|
162
|
-
.sort((a, b) => a.connectedAt - b.connectedAt);
|
|
164
|
+
.sort((a, b) => place(a) - place(b) || a.connectedAt - b.connectedAt);
|
|
163
165
|
}
|
|
164
166
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
* Where an account is listed among a subject's others, before the order they were
|
|
168
|
+
* connected in. This is all there is to preferring an account: there is no separate pin.
|
|
169
|
+
*/
|
|
170
|
+
function place(account) {
|
|
171
|
+
if (account.status === 'retired')
|
|
172
|
+
return 3;
|
|
173
|
+
return account.mark === 'preferred' ? 0 : account.mark === 'unused' ? 2 : 1;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* One subject's accounts as a single pill: the first of them, then how many more stand
|
|
177
|
+
* behind it. The order is the holder's preferred account, then the order the rest were
|
|
178
|
+
* connected in, which a renewal never changes, then the ones no longer used.
|
|
179
|
+
* Only accounts verified now are counted, and one of those leads when the first has
|
|
180
|
+
* lapsed, so the pill never puts a lapsed account forward while a good one sits behind
|
|
181
|
+
* a number. The lapsed ones are all still there in the dialog, each in its place. A
|
|
182
|
+
* preferred account that has lapsed keeps its mark, and leads again once renewed. A
|
|
183
|
+
* retired account is never counted, and leads only where nothing else is shown.
|
|
170
184
|
*
|
|
171
185
|
* `load` gives the records afresh when the dialog opens and as it stays open.
|
|
172
186
|
*/
|
|
@@ -317,7 +331,9 @@ export function init({ backendUrl, handoffUrl }) {
|
|
|
317
331
|
safeUrl(data.local.profileUrl);
|
|
318
332
|
return data;
|
|
319
333
|
},
|
|
320
|
-
start: async (provider, method) => flowView(await ask('/sessions',
|
|
334
|
+
start: async (provider, method, renew) => flowView(await ask('/sessions', renew
|
|
335
|
+
? { kind: 'renew', connectionId: renew, provider, method }
|
|
336
|
+
: { kind: 'connect', provider, method })),
|
|
321
337
|
read: async (id) => flowView(await ask(`/flows/${encodeURIComponent(id)}?format=json`, undefined, id)),
|
|
322
338
|
submit: async (id, artifact) => flowView(await ask(`/flows/${encodeURIComponent(id)}/submit`, { artifact }, id)),
|
|
323
339
|
async approve(id, visibility, cancel) {
|
|
@@ -362,12 +378,14 @@ export function init({ backendUrl, handoffUrl }) {
|
|
|
362
378
|
* Opens the connect dialog over the current page. Resolves when it closes, with how
|
|
363
379
|
* the last attempt in it ended. Call from a click: a sign-in method opens a window.
|
|
364
380
|
*/
|
|
365
|
-
openConnect(opener = document.body, back, provider
|
|
381
|
+
openConnect(opener = document.body, back, provider,
|
|
382
|
+
/** A record of the holder's to renew, in place of connecting: see `openConnectDialog`. */
|
|
383
|
+
renew) {
|
|
366
384
|
if (remote && !handoffUrl)
|
|
367
385
|
throw new Error('A backend on another origin requires a handoff URL');
|
|
368
386
|
// Each opening is vouched for afresh, for whoever is signed in to this site now.
|
|
369
387
|
session = undefined;
|
|
370
|
-
return openConnectDialog(opener, flows, back, provider);
|
|
388
|
+
return openConnectDialog(opener, flows, back, provider, renew);
|
|
371
389
|
},
|
|
372
390
|
/**
|
|
373
391
|
* Draws the pill that opens the connect dialog. The host receives a `verily-result`
|
|
@@ -530,6 +548,15 @@ export function init({ backendUrl, handoffUrl }) {
|
|
|
530
548
|
session = undefined;
|
|
531
549
|
return ask(`/connections/${encodeURIComponent(id)}/disconnect`, {});
|
|
532
550
|
},
|
|
551
|
+
/**
|
|
552
|
+
* Says how the holder would have an account listed, or retires it. Names one record
|
|
553
|
+
* and acts on the account: every record of it the holder has.
|
|
554
|
+
*/
|
|
555
|
+
mark(id, as) {
|
|
556
|
+
if (remote)
|
|
557
|
+
session = undefined;
|
|
558
|
+
return ask(`/connections/${encodeURIComponent(id)}/mark`, { as });
|
|
559
|
+
},
|
|
533
560
|
issueShare: (id) => request(`/connections/${encodeURIComponent(id)}/share`, {}),
|
|
534
561
|
revokeShare: (id) => request(`/connections/${encodeURIComponent(id)}/share-revoke`, {}),
|
|
535
562
|
};
|
|
@@ -645,7 +672,12 @@ function validEvidence(value) {
|
|
|
645
672
|
['account', 'key', 'page', 'mailbox'].includes(String(value.external.kind))) &&
|
|
646
673
|
(value.revokedAt === undefined ||
|
|
647
674
|
(typeof value.revokedAt === 'number' && Number.isFinite(value.revokedAt))) &&
|
|
648
|
-
['verified', 'unconfirmed', 'expired', 'revoked'].includes(String(value.status)) &&
|
|
675
|
+
['verified', 'unconfirmed', 'expired', 'retired', 'revoked'].includes(String(value.status)) &&
|
|
676
|
+
// A retired record stands on its dates, so one that does not say when is not one.
|
|
677
|
+
(value.status === 'retired'
|
|
678
|
+
? typeof value.retiredAt === 'number' && Number.isFinite(value.retiredAt)
|
|
679
|
+
: value.retiredAt === undefined) &&
|
|
680
|
+
(value.mark === undefined || ['preferred', 'unused'].includes(String(value.mark))) &&
|
|
649
681
|
['public', 'unlisted'].includes(String(value.visibility)) &&
|
|
650
682
|
(value.connectedAt === undefined ||
|
|
651
683
|
(typeof value.connectedAt === 'number' && Number.isFinite(value.connectedAt))) &&
|
|
@@ -766,6 +798,14 @@ if (typeof customElements !== 'undefined' && !customElements.get('verily-badge')
|
|
|
766
798
|
changed(result);
|
|
767
799
|
});
|
|
768
800
|
},
|
|
801
|
+
renew: (id, provider, method) => {
|
|
802
|
+
void client
|
|
803
|
+
.openConnect(this, () => details(this), provider, { id, method })
|
|
804
|
+
.then((result) => {
|
|
805
|
+
if (result.outcome === 'complete')
|
|
806
|
+
changed(result);
|
|
807
|
+
});
|
|
808
|
+
},
|
|
769
809
|
// One at a time, each told as it goes: if a later one fails, the host still
|
|
770
810
|
// hears of those that went and reads its records again.
|
|
771
811
|
remove: async (ids) => {
|
|
@@ -774,6 +814,10 @@ if (typeof customElements !== 'undefined' && !customElements.get('verily-badge')
|
|
|
774
814
|
changed({ outcome: 'removed', connectionId: id });
|
|
775
815
|
}
|
|
776
816
|
},
|
|
817
|
+
mark: async (id, as) => {
|
|
818
|
+
await client.mark(id, as);
|
|
819
|
+
changed({ outcome: 'marked', connectionId: id, as });
|
|
820
|
+
},
|
|
777
821
|
});
|
|
778
822
|
}
|
|
779
823
|
else
|
package/dist/browser/logo.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
/** Where the logotype under a dialog leads: the project, for a reader asking what drew this. */
|
|
2
|
+
export declare const projectUrl = "https://github.com/bkazemi/verily";
|
|
1
3
|
/** The logotype, drawn as nodes: nothing in the badge sets innerHTML. */
|
|
2
4
|
export declare function verilyLogo(): SVGSVGElement;
|
package/dist/browser/logo.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { logoPaths, logoViewBox } from '../logo.js';
|
|
2
|
+
/** Where the logotype under a dialog leads: the project, for a reader asking what drew this. */
|
|
3
|
+
export const projectUrl = 'https://github.com/bkazemi/verily';
|
|
2
4
|
/** The logotype, drawn as nodes: nothing in the badge sets innerHTML. */
|
|
3
5
|
export function verilyLogo() {
|
|
4
6
|
const namespace = 'http://www.w3.org/2000/svg';
|
package/dist/browser/mark.d.ts
CHANGED
|
@@ -15,5 +15,7 @@ export declare function paintMark(svg: SVGSVGElement, tone: MarkTone): void;
|
|
|
15
15
|
export declare function verificationMark(tone?: MarkTone): SVGSVGElement;
|
|
16
16
|
/** The clock on an expired record: a face, and hands at three o'clock. */
|
|
17
17
|
export declare const clockMark: () => SVGSVGElement;
|
|
18
|
+
/** The box on a retired record: a lid, what it holds, and a slot to take it down by. */
|
|
19
|
+
export declare const boxMark: () => SVGSVGElement;
|
|
18
20
|
/** The dash on a record that is revoked or cannot be shown. */
|
|
19
21
|
export declare const dashMark: () => SVGSVGElement;
|
package/dist/browser/mark.js
CHANGED
|
@@ -73,5 +73,11 @@ export const clockMark = () => statusMark('clock', [
|
|
|
73
73
|
['circle', { cx: '8', cy: '8', r: '6.2' }],
|
|
74
74
|
['path', { d: 'M8 4.6V8h2.6' }],
|
|
75
75
|
]);
|
|
76
|
+
/** The box on a retired record: a lid, what it holds, and a slot to take it down by. */
|
|
77
|
+
export const boxMark = () => statusMark('box', [
|
|
78
|
+
['path', { d: 'M2.6 3.8h10.8v2.6H2.6z' }],
|
|
79
|
+
['path', { d: 'M3.7 6.4v5.8h8.6V6.4' }],
|
|
80
|
+
['path', { d: 'M6.6 9h2.8' }],
|
|
81
|
+
]);
|
|
76
82
|
/** The dash on a record that is revoked or cannot be shown. */
|
|
77
83
|
export const dashMark = () => statusMark('dash', [['path', { d: 'M4.4 8h7.2' }]]);
|
package/dist/browser/signed.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export declare function signature(evidence: Evidence): Promise<Signature>;
|
|
|
28
28
|
* it, with only its present state taken from the record as read, so what stands beside the
|
|
29
29
|
* signature mark is what was signed. One whose signed record failed is not shown as verified: the
|
|
30
30
|
* verifier's own signature does not bear it out, so it reads as unconfirmed.
|
|
31
|
+
*
|
|
32
|
+
* The holder's mark is taken from the record as read too, each time, and never from the
|
|
33
|
+
* signed record kept: a mark would otherwise vanish the moment its record checked, and one
|
|
34
|
+
* changed since would go on being shown as it was.
|
|
31
35
|
*/
|
|
32
36
|
export declare function standing<T extends Evidence>(evidence: T): T;
|
|
33
37
|
/**
|
package/dist/browser/signed.js
CHANGED
|
@@ -27,9 +27,12 @@ function canonical(value) {
|
|
|
27
27
|
.map(([key, field]) => `${JSON.stringify(key)}:${canonical(field)}`)
|
|
28
28
|
.join(',')}}`;
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* What a record claims, apart from whether it stands now: all a signed record can bear out.
|
|
32
|
+
* The holder's mark is no claim of the verifier's and is in no signed record.
|
|
33
|
+
*/
|
|
31
34
|
function claims(record) {
|
|
32
|
-
const { status: _status, revokedAt: _revokedAt, signedUrl: _signedUrl, links: _links, type: _type, version: _version, issuedAt: _issuedAt, ...rest } = record;
|
|
35
|
+
const { status: _status, revokedAt: _revokedAt, signedUrl: _signedUrl, mark: _mark, links: _links, type: _type, version: _version, issuedAt: _issuedAt, ...rest } = record;
|
|
33
36
|
return canonical(rest);
|
|
34
37
|
}
|
|
35
38
|
/**
|
|
@@ -124,13 +127,20 @@ async function check(evidence) {
|
|
|
124
127
|
document.provider !== evidence.provider ||
|
|
125
128
|
document.external.id !== evidence.external.id)
|
|
126
129
|
return { state: 'invalid' };
|
|
130
|
+
// A retired record is signed as a version of its own, and a record that stands as the
|
|
131
|
+
// other. One served for the other is the record caught between the two, and no verdict.
|
|
132
|
+
if ((document.version === 2) !== (evidence.status === 'retired'))
|
|
133
|
+
return { state: 'unchecked', why: 'unreadable' };
|
|
127
134
|
// The record as it was signed. Whether it stands now is no part of a signed record, so
|
|
128
|
-
// that
|
|
129
|
-
|
|
135
|
+
// that is kept from the record as it was read, and so is the holder's mark, which is in
|
|
136
|
+
// no signed record: whatever a document says of either is left behind.
|
|
137
|
+
const { type: _type, version: _version, issuedAt: _issuedAt, status: _status, retiredAt: _retiredAt, mark: _mark, ...said } = document;
|
|
130
138
|
const record = {
|
|
131
139
|
...said,
|
|
132
140
|
status: evidence.status,
|
|
133
141
|
...(evidence.revokedAt === undefined ? {} : { revokedAt: evidence.revokedAt }),
|
|
142
|
+
...(document.version === 2 ? { retiredAt: document.retiredAt } : {}),
|
|
143
|
+
...(evidence.mark === undefined ? {} : { mark: evidence.mark }),
|
|
134
144
|
signedUrl: evidence.signedUrl,
|
|
135
145
|
};
|
|
136
146
|
// Signed, but not something a page may draw: it is shown as read, with no mark.
|
|
@@ -178,18 +188,25 @@ export function signature(evidence) {
|
|
|
178
188
|
* it, with only its present state taken from the record as read, so what stands beside the
|
|
179
189
|
* signature mark is what was signed. One whose signed record failed is not shown as verified: the
|
|
180
190
|
* verifier's own signature does not bear it out, so it reads as unconfirmed.
|
|
191
|
+
*
|
|
192
|
+
* The holder's mark is taken from the record as read too, each time, and never from the
|
|
193
|
+
* signed record kept: a mark would otherwise vanish the moment its record checked, and one
|
|
194
|
+
* changed since would go on being shown as it was.
|
|
181
195
|
*/
|
|
182
196
|
export function standing(evidence) {
|
|
183
197
|
if (evidence.signedUrl === undefined)
|
|
184
198
|
return evidence;
|
|
185
199
|
const key = shown(evidence);
|
|
186
200
|
const signed = signedAs.get(key);
|
|
187
|
-
if (signed)
|
|
201
|
+
if (signed) {
|
|
202
|
+
const { mark: _mark, ...said } = signed;
|
|
188
203
|
return {
|
|
189
|
-
...
|
|
204
|
+
...said,
|
|
190
205
|
status: evidence.status,
|
|
191
206
|
...(evidence.revokedAt === undefined ? {} : { revokedAt: evidence.revokedAt }),
|
|
207
|
+
...(evidence.mark === undefined ? {} : { mark: evidence.mark }),
|
|
192
208
|
};
|
|
209
|
+
}
|
|
193
210
|
return evidence.status === 'verified' && failures.has(key)
|
|
194
211
|
? { ...evidence, status: 'unconfirmed' }
|
|
195
212
|
: evidence;
|
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;
|
|
@@ -426,10 +440,17 @@ export interface Evidence {
|
|
|
426
440
|
visibilityApprovedAt: number;
|
|
427
441
|
expiresAt: number;
|
|
428
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;
|
|
429
449
|
evidenceUrl: string;
|
|
430
450
|
/**
|
|
431
451
|
* Where this record can be had signed, on a verifier that signs. Only a public record
|
|
432
|
-
* 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.
|
|
433
454
|
*/
|
|
434
455
|
signedUrl?: string;
|
|
435
456
|
}
|
|
@@ -452,6 +473,11 @@ export declare function status(connection: Connection, now: number, freshness?:
|
|
|
452
473
|
* reverses the moment it reads again.
|
|
453
474
|
*/
|
|
454
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;
|
|
455
481
|
/**
|
|
456
482
|
* The word for a record's state. Evidence can be read after it was issued, so an approval
|
|
457
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/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { attestationLabel, externalLink, externalName, isArtifactProvider, isCodeProvider, isRedirectProvider, localSide, proofTitle, providerMethod, statusLabel, signedBy, } from '../core/index.js';
|
|
1
|
+
import { attestationLabel, 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';
|
|
@@ -245,20 +245,32 @@ function evidencePage(e, base, report, keyId) {
|
|
|
245
245
|
card(names.provider, externalName(e.external), externalLink(e.external),
|
|
246
246
|
// A mailbox's address is its name already, so there is no second identifier.
|
|
247
247
|
e.external.kind === 'mailbox' ? undefined : e.external.id, externalNotes(e.attestations, names)) +
|
|
248
|
-
times(
|
|
249
|
-
[
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
'
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
248
|
+
times(e.status === 'retired'
|
|
249
|
+
? [
|
|
250
|
+
['Approved', e.approvedAt],
|
|
251
|
+
// Two rows, always, so a reader sees any gap between them: an account retired
|
|
252
|
+
// weeks after it was last proved went unproved for those weeks.
|
|
253
|
+
['Last verified', lastProved(e)],
|
|
254
|
+
['Retired', e.retiredAt],
|
|
255
|
+
['Sharing link expires', e.linkExpiresAt],
|
|
256
|
+
]
|
|
257
|
+
: [
|
|
258
|
+
['Approved', e.approvedAt],
|
|
259
|
+
['Authenticated', e.authenticatedAt],
|
|
260
|
+
[
|
|
261
|
+
current ? 'Valid until' : 'Expired on',
|
|
262
|
+
e.status === 'revoked' ? undefined : e.expiresAt,
|
|
263
|
+
],
|
|
264
|
+
['Revoked on', e.revokedAt],
|
|
265
|
+
// Only a method that publishes an artifact drifts; a sign-in does not go stale.
|
|
266
|
+
[
|
|
267
|
+
'Last checked',
|
|
268
|
+
e.attestations.external[0].artifactUrl
|
|
269
|
+
? e.attestations.external[0].confirmedAt
|
|
270
|
+
: undefined,
|
|
271
|
+
],
|
|
272
|
+
['Sharing link expires', e.linkExpiresAt],
|
|
273
|
+
]) +
|
|
262
274
|
`${e.linkExpiresAt ? '<p class="fine">Anyone with this link can view and forward it.</p>' : ''}
|
|
263
275
|
${e.signedUrl ? `<p class="signed" id="signed"><strong>Signed by ${escape(e.verifierName)}</strong>${keyId ? ` with OpenPGP key <a href="${escape(base)}/keys.asc">${escape(keyId)}</a>` : ''}. <a href="${escape(safeUrl(e.signedUrl))}">Download the signed record</a>, which <a href="${escape(base)}/check">can be checked</a> without this page.</p>` : ''}
|
|
264
276
|
<p class="fine">This connection does not establish legal identity, trustworthiness, content authorship, or permanent ownership.</p>
|
|
@@ -275,14 +287,23 @@ const checkForm = '<form method="post"><label>Signed record <textarea name="reco
|
|
|
275
287
|
function checkedPage(d, keyId, base) {
|
|
276
288
|
const names = { site: d.siteName, provider: d.providerName };
|
|
277
289
|
const local = localSide(d.local, d.siteName);
|
|
278
|
-
|
|
290
|
+
// A retired record did not stand when it was signed, and its heading says so first.
|
|
291
|
+
const retired = d.version === 2;
|
|
292
|
+
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>` +
|
|
279
293
|
card(local.heading, local.value, d.local.profileUrl, undefined, attestationNote(d.attestations.local, names)) +
|
|
280
294
|
card(names.provider, externalName(d.external), externalLink(d.external), d.external.kind === 'mailbox' ? undefined : d.external.id, externalNotes(d.attestations, names)) +
|
|
281
|
-
times(
|
|
282
|
-
[
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
295
|
+
times(retired
|
|
296
|
+
? [
|
|
297
|
+
['Signed', d.issuedAt],
|
|
298
|
+
['Approved', d.approvedAt],
|
|
299
|
+
['Last verified', lastProved(d)],
|
|
300
|
+
['Retired', d.retiredAt],
|
|
301
|
+
]
|
|
302
|
+
: [
|
|
303
|
+
['Signed', d.issuedAt],
|
|
304
|
+
['Approved', d.approvedAt],
|
|
305
|
+
['Valid until', d.expiresAt],
|
|
306
|
+
]) +
|
|
286
307
|
`<p class="fine">Signing key <a href="${escape(base)}/keys.asc">${escape(keyId)}</a>.</p>
|
|
287
308
|
<p class="fine"><a href="${escape(safeUrl(d.evidenceUrl))}">See whether this connection still stands</a></p>`);
|
|
288
309
|
}
|
|
@@ -374,13 +395,24 @@ export function createVerily(options) {
|
|
|
374
395
|
/**
|
|
375
396
|
* The methods a page offers. A flow on an existing record stays in its namespace, so
|
|
376
397
|
* where the holder is signed in that record narrows the list; removing a link from the
|
|
377
|
-
* external side refuses a standing proof, which anybody can hand back.
|
|
398
|
+
* external side refuses a standing proof, which anybody can hand back. A retired record
|
|
399
|
+
* is renewed by the method it was first shown by, so that one is offered alone.
|
|
378
400
|
*/
|
|
379
401
|
async function offer(kind, id, user, requested) {
|
|
380
402
|
let namespace = requested ?? undefined;
|
|
381
|
-
|
|
382
|
-
|
|
403
|
+
let only;
|
|
404
|
+
if (user && id) {
|
|
405
|
+
const held = await service.read(id, user);
|
|
406
|
+
namespace = held.provider;
|
|
407
|
+
if (held.status === 'retired') {
|
|
408
|
+
// Who can read a retired record is frozen with the rest of it.
|
|
409
|
+
if (kind === 'visibility')
|
|
410
|
+
throw new Unavailable();
|
|
411
|
+
only = held.attestations.external[0].method;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
383
414
|
const offered = (user ? permitted(user) : service.providers).filter((p) => (namespace === undefined || p.id === namespace) &&
|
|
415
|
+
(only === undefined || providerMethod(p) === only) &&
|
|
384
416
|
!(['revoke', 'share-revoke'].includes(kind) && isArtifactProvider(p) && p.expect));
|
|
385
417
|
if (!offered.length)
|
|
386
418
|
throw new Unavailable();
|
|
@@ -428,8 +460,8 @@ export function createVerily(options) {
|
|
|
428
460
|
profileUrl: user.profileUrl,
|
|
429
461
|
});
|
|
430
462
|
/**
|
|
431
|
-
* A connect flow as the in-page dialog draws it: the same steps the flow pages
|
|
432
|
-
* through, as data, so the holder never has to leave the page they started on.
|
|
463
|
+
* A connect or renew flow as the in-page dialog draws it: the same steps the flow pages
|
|
464
|
+
* walk through, as data, so the holder never has to leave the page they started on.
|
|
433
465
|
*/
|
|
434
466
|
async function flowView(flow, authorizationUrl) {
|
|
435
467
|
const provider = service.providerOf(flow);
|
|
@@ -472,10 +504,10 @@ export function createVerily(options) {
|
|
|
472
504
|
view.connectionId = flow.resultId;
|
|
473
505
|
return view;
|
|
474
506
|
}
|
|
475
|
-
/** A flow the dialog may read: one this holder started, to connect an account. */
|
|
507
|
+
/** A flow the dialog may read: one this holder started, to connect an account or renew one. */
|
|
476
508
|
async function ownFlow(request, id) {
|
|
477
509
|
const flow = await service.flow(id, binding(request));
|
|
478
|
-
if (flow.kind
|
|
510
|
+
if (!['connect', 'renew'].includes(flow.kind) || (await local(request)).id !== flow.local?.id)
|
|
479
511
|
throw new Unavailable();
|
|
480
512
|
return flow;
|
|
481
513
|
}
|
|
@@ -775,7 +807,7 @@ export function createVerily(options) {
|
|
|
775
807
|
}), await context(request, kind),
|
|
776
808
|
// Posted from this origin, which the check on every POST has already held it to.
|
|
777
809
|
true);
|
|
778
|
-
if (asJson &&
|
|
810
|
+
if (asJson && ['connect', 'renew'].includes(kind))
|
|
779
811
|
return json(await flowView(await service.flow(flow.flowId, flow.binding), flow.authorizationUrl), flowCookie(flow.binding));
|
|
780
812
|
return redirect(entry(flow), flowCookie(flow.binding));
|
|
781
813
|
}
|
|
@@ -815,15 +847,21 @@ export function createVerily(options) {
|
|
|
815
847
|
return json(id ? { outcome: 'complete', connectionId: id } : { outcome: 'cancelled' });
|
|
816
848
|
return await ended(await service.flow(flow.id, binding(request)));
|
|
817
849
|
}
|
|
818
|
-
const management = path.match(/^\/connections\/([^/]+)\/(disconnect|share|share-revoke|visibility)$/);
|
|
850
|
+
const management = path.match(/^\/connections\/([^/]+)\/(disconnect|mark|share|share-revoke|visibility)$/);
|
|
819
851
|
if (management) {
|
|
820
852
|
const user = await local(request), id = management[1];
|
|
821
853
|
if (management[2] === 'disconnect') {
|
|
822
854
|
await service.revoke(id, user);
|
|
823
855
|
return json({ ok: true });
|
|
824
856
|
}
|
|
857
|
+
// Acts on the account the record names: every record of it this holder has.
|
|
858
|
+
if (management[2] === 'mark') {
|
|
859
|
+
await service.mark(id, user, data.as);
|
|
860
|
+
return json({ ok: true });
|
|
861
|
+
}
|
|
825
862
|
if (management[2] === 'visibility') {
|
|
826
|
-
await service.read(id, user)
|
|
863
|
+
if ((await service.read(id, user)).status === 'retired')
|
|
864
|
+
throw new Unavailable();
|
|
827
865
|
return json({ url: `${service.baseUrl}/visibility/${id}` });
|
|
828
866
|
}
|
|
829
867
|
return json((await service.share(id, user, management[2] === 'share-revoke')) ?? { ok: true });
|