@bkazemi/verily 0.2.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 +30 -17
- package/dist/browser/badge.js +16 -5
- package/dist/browser/connect-dialog.d.ts +13 -2
- package/dist/browser/connect-dialog.js +19 -9
- package/dist/browser/evidence-dialog.d.ts +19 -1
- package/dist/browser/evidence-dialog.js +165 -18
- package/dist/browser/index.d.ts +10 -1
- package/dist/browser/index.js +58 -14
- 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/provider-mark.js +5 -3
- package/dist/browser/signed.d.ts +4 -0
- package/dist/browser/signed.js +23 -6
- package/dist/core/index.d.ts +39 -6
- package/dist/core/index.js +23 -0
- package/dist/core/signed.d.ts +20 -5
- package/dist/core/signed.js +5 -2
- 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 +106 -42
- 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/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 +213 -36
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/server/service.js
CHANGED
|
@@ -72,15 +72,22 @@ export class VerilyService {
|
|
|
72
72
|
* A public record that stands, signed as of now. Signed when asked for, not when
|
|
73
73
|
* approved, so a signed record says the record stood on the day it was taken and none can be
|
|
74
74
|
* made once it is removed.
|
|
75
|
+
*
|
|
76
|
+
* A retired record is signed too, as what it is: the durable form of the claim that the
|
|
77
|
+
* account was the holder's when it was last proved. It did not stand when it was signed,
|
|
78
|
+
* so it is a version of its own that says so, which a reader of the first refuses.
|
|
75
79
|
*/
|
|
76
80
|
async signed(id) {
|
|
77
81
|
const current = await this.signer();
|
|
78
82
|
if (!current)
|
|
79
83
|
throw new Unavailable();
|
|
80
|
-
const { status, revokedAt: _revokedAt, signedUrl: _signedUrl, ...record } = await this.read(id);
|
|
84
|
+
const { status, revokedAt: _revokedAt, signedUrl: _signedUrl, mark: _mark, retiredAt, ...record } = await this.read(id);
|
|
85
|
+
const document = { type: 'verily-evidence', issuedAt: this.now(), ...record };
|
|
86
|
+
if (status === 'retired')
|
|
87
|
+
return current.sign({ ...document, version: 2, status, retiredAt: retiredAt });
|
|
81
88
|
if (status !== 'verified')
|
|
82
89
|
throw new Unavailable();
|
|
83
|
-
return current.sign({
|
|
90
|
+
return current.sign({ ...document, version: 1 });
|
|
84
91
|
}
|
|
85
92
|
/** What a signed record says, if this instance signed it. */
|
|
86
93
|
async checked(signed) {
|
|
@@ -184,6 +191,9 @@ export class VerilyService {
|
|
|
184
191
|
connection.revokedAt !== undefined ||
|
|
185
192
|
(['visibility', 'renew'].includes(kind) && connection.local.id !== local?.id))
|
|
186
193
|
throw new Unavailable();
|
|
194
|
+
// Who can read a retired record is frozen with the rest of it.
|
|
195
|
+
if (kind === 'visibility' && connection.retiredAt !== undefined)
|
|
196
|
+
throw new Unavailable();
|
|
187
197
|
subject = connection.local;
|
|
188
198
|
if (['renew', 'visibility'].includes(kind))
|
|
189
199
|
account = connection.external;
|
|
@@ -250,6 +260,7 @@ export class VerilyService {
|
|
|
250
260
|
async known(tx, local, provider) {
|
|
251
261
|
const offers = (await tx.list('connections'))
|
|
252
262
|
.filter((c) => c.revokedAt === undefined &&
|
|
263
|
+
c.retiredAt === undefined &&
|
|
253
264
|
c.local.id === local.id &&
|
|
254
265
|
c.provider === provider.id &&
|
|
255
266
|
(c.attestations?.external[0].method ?? 'oauth') !== provider.method)
|
|
@@ -291,6 +302,10 @@ export class VerilyService {
|
|
|
291
302
|
* Removing a link from the external side takes a method whose proof is fresh. A standing
|
|
292
303
|
* proof, such as a link back, is there for anyone to point at: handing one back shows
|
|
293
304
|
* the link exists, not that whoever handed it back holds the account.
|
|
305
|
+
*
|
|
306
|
+
* A retired record is renewed by the method it was first shown by and no other. Another
|
|
307
|
+
* would be listed beneath a main proof nobody has read since, and the record would come
|
|
308
|
+
* back unconfirmed.
|
|
294
309
|
*/
|
|
295
310
|
chosen(kind, choice, connection) {
|
|
296
311
|
let provider;
|
|
@@ -304,6 +319,8 @@ export class VerilyService {
|
|
|
304
319
|
provider = this.resolve(choice.provider, choice.method);
|
|
305
320
|
if (connection && provider.id !== connection.provider)
|
|
306
321
|
throw new Unavailable();
|
|
322
|
+
if (kind === 'renew' && connection && !revives(connection, provider))
|
|
323
|
+
throw new Unavailable();
|
|
307
324
|
if (['revoke', 'share-revoke'].includes(kind) &&
|
|
308
325
|
isArtifactProvider(provider) &&
|
|
309
326
|
provider.expect)
|
|
@@ -750,6 +767,7 @@ export class VerilyService {
|
|
|
750
767
|
throw new Unavailable();
|
|
751
768
|
// The id exists before the record does, because a hosted proof is addressed by it.
|
|
752
769
|
const connectionId = secret();
|
|
770
|
+
const mark = await this.accountMark(tx, flow.local, provider.id, flow.external);
|
|
753
771
|
connection = {
|
|
754
772
|
id: connectionId,
|
|
755
773
|
local: flow.local,
|
|
@@ -766,6 +784,8 @@ export class VerilyService {
|
|
|
766
784
|
external: [this.attestation(flow, provider, connectionId)],
|
|
767
785
|
},
|
|
768
786
|
proof: hosted(provider) ? flow.artifact : undefined,
|
|
787
|
+
// The account's other records say how the holder has it listed, and so does this.
|
|
788
|
+
...(mark ? { mark } : {}),
|
|
769
789
|
};
|
|
770
790
|
}
|
|
771
791
|
else {
|
|
@@ -783,7 +803,20 @@ export class VerilyService {
|
|
|
783
803
|
// subject snapshot refreshes because the holder just approved what it shows.
|
|
784
804
|
if (flow.local.id !== existing.local.id)
|
|
785
805
|
throw new Unavailable();
|
|
806
|
+
// Asked again here: the record may have been retired since the flow started.
|
|
807
|
+
if (!revives(existing, provider))
|
|
808
|
+
throw new Unavailable();
|
|
786
809
|
this.recordMethod(connection, flow, provider);
|
|
810
|
+
if (connection.retiredAt !== undefined) {
|
|
811
|
+
// Live again on the strength of the proof just made and nothing older. A record
|
|
812
|
+
// made for the account while this one sat retired may have been marked since,
|
|
813
|
+
// and this one would otherwise be drawn in its place and hide that choice.
|
|
814
|
+
const mark = await this.accountMark(tx, connection.local, connection.provider, connection.external, connection.id);
|
|
815
|
+
delete connection.retiredAt;
|
|
816
|
+
delete connection.mark;
|
|
817
|
+
if (mark)
|
|
818
|
+
connection.mark = mark;
|
|
819
|
+
}
|
|
787
820
|
}
|
|
788
821
|
else if (flow.kind === 'revoke') {
|
|
789
822
|
connection.revokedAt = this.now();
|
|
@@ -792,6 +825,8 @@ export class VerilyService {
|
|
|
792
825
|
else if (flow.kind === 'visibility') {
|
|
793
826
|
if (allowed && !allowed.includes(visibility))
|
|
794
827
|
throw new Unavailable();
|
|
828
|
+
if (connection.retiredAt !== undefined)
|
|
829
|
+
throw new Unavailable();
|
|
795
830
|
connection.visibility = visibility;
|
|
796
831
|
connection.visibilityApprovedAt = this.now();
|
|
797
832
|
}
|
|
@@ -839,9 +874,11 @@ export class VerilyService {
|
|
|
839
874
|
external: [{ by: 'provider', method: 'oauth', confirmedAt: connection.authenticatedAt }],
|
|
840
875
|
};
|
|
841
876
|
const [main, ...rest] = connection.attestations.external;
|
|
877
|
+
// A retired record is as it stood when it was retired, and nothing is read after that.
|
|
878
|
+
const at = connection.retiredAt ?? this.now();
|
|
842
879
|
return {
|
|
843
880
|
...connection.attestations,
|
|
844
|
-
external: [main, ...rest.filter((a) => fresh(a,
|
|
881
|
+
external: [main, ...rest.filter((a) => fresh(a, at, this.freshness))],
|
|
845
882
|
};
|
|
846
883
|
}
|
|
847
884
|
evidence(connection) {
|
|
@@ -865,10 +902,15 @@ export class VerilyService {
|
|
|
865
902
|
visibilityApprovedAt: connection.visibilityApprovedAt,
|
|
866
903
|
expiresAt: connection.expiresAt,
|
|
867
904
|
revokedAt: connection.revokedAt,
|
|
905
|
+
// How an account is listed says nothing once its record is removed or retired.
|
|
906
|
+
...(connection.mark && !['revoked', 'retired'].includes(standing)
|
|
907
|
+
? { mark: connection.mark }
|
|
908
|
+
: {}),
|
|
909
|
+
...(standing === 'retired' ? { retiredAt: connection.retiredAt } : {}),
|
|
868
910
|
evidenceUrl,
|
|
869
911
|
...(this.options.signingKey !== undefined &&
|
|
870
912
|
connection.visibility === 'public' &&
|
|
871
|
-
|
|
913
|
+
['verified', 'retired'].includes(standing)
|
|
872
914
|
? { signedUrl: `${evidenceUrl}?format=signed` }
|
|
873
915
|
: {}),
|
|
874
916
|
};
|
|
@@ -877,12 +919,14 @@ export class VerilyService {
|
|
|
877
919
|
* The live record a connect flow reproves by another method, if there is one: the same
|
|
878
920
|
* subject linked to the same account on the same provider, first shown some other way.
|
|
879
921
|
* The same method again is a second record, as it always was; renewing is how that
|
|
880
|
-
* one is extended.
|
|
922
|
+
* one is extended. A retired record is never joined: it is frozen, so proving its account
|
|
923
|
+
* another way makes a new record and leaves that one as history behind it.
|
|
881
924
|
*/
|
|
882
925
|
async joins(tx, flow) {
|
|
883
926
|
const provider = this.providerOf(flow);
|
|
884
927
|
return (await tx.list('connections'))
|
|
885
928
|
.filter((c) => c.revokedAt === undefined &&
|
|
929
|
+
c.retiredAt === undefined &&
|
|
886
930
|
c.local.id === flow.local.id &&
|
|
887
931
|
c.provider === provider.id &&
|
|
888
932
|
(c.attestations?.external[0].method ?? 'oauth') !== providerMethod(provider) &&
|
|
@@ -1046,6 +1090,84 @@ export class VerilyService {
|
|
|
1046
1090
|
await this.audit(tx, id, 'revoke', 'local');
|
|
1047
1091
|
});
|
|
1048
1092
|
}
|
|
1093
|
+
/**
|
|
1094
|
+
* The holder's records of one account that are not revoked. A mark belongs to an account,
|
|
1095
|
+
* and an account can stand on several records: shown a second way on a record of its own,
|
|
1096
|
+
* or removed once and connected again. They are told apart as a badge tells them, by
|
|
1097
|
+
* provider and the id the provider gave.
|
|
1098
|
+
*/
|
|
1099
|
+
async account(tx, local, provider, external) {
|
|
1100
|
+
return (await tx.list('connections')).filter((c) => c.revokedAt === undefined &&
|
|
1101
|
+
c.local.id === local.id &&
|
|
1102
|
+
c.provider === provider &&
|
|
1103
|
+
c.external.id === external.id);
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* The mark an account's records carry, for a record about to stand beside them: every
|
|
1107
|
+
* one neither revoked nor retired, lapsed ones included, since those hold a mark that
|
|
1108
|
+
* must survive their own renewal.
|
|
1109
|
+
*/
|
|
1110
|
+
async accountMark(tx, local, provider, external, except) {
|
|
1111
|
+
return (await this.account(tx, local, provider, external)).find((c) => c.id !== except && c.retiredAt === undefined && c.mark !== undefined)?.mark;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Marks an account as the holder would have it listed, or retires it. Names one record
|
|
1115
|
+
* and acts on the account, or a mark on one record could vanish behind a sibling, and
|
|
1116
|
+
* retiring one could leave the account reading as verified through another.
|
|
1117
|
+
*
|
|
1118
|
+
* No fresh proof is asked for. A mark never widens who can read a record or makes one
|
|
1119
|
+
* read as verified, and a holder retiring an account often can no longer prove it.
|
|
1120
|
+
*/
|
|
1121
|
+
async mark(id, local, as) {
|
|
1122
|
+
if (!['preferred', 'current', 'unused', 'retired'].includes(as))
|
|
1123
|
+
throw new Unavailable();
|
|
1124
|
+
await this.transaction(async (tx) => {
|
|
1125
|
+
const named = await this.owned(tx, id, local);
|
|
1126
|
+
if (named.revokedAt !== undefined)
|
|
1127
|
+
throw new Unavailable();
|
|
1128
|
+
// A record already retired is left exactly as it is: its dates are what it stands on.
|
|
1129
|
+
const records = (await this.account(tx, local, named.provider, named.external)).filter((c) => c.retiredAt === undefined);
|
|
1130
|
+
if (as === 'retired') {
|
|
1131
|
+
// Whatever its status: a lapsed one left live could be read again later and bring
|
|
1132
|
+
// the account back. Nothing is revoked.
|
|
1133
|
+
for (const record of records) {
|
|
1134
|
+
record.retiredAt = this.now();
|
|
1135
|
+
delete record.mark;
|
|
1136
|
+
await tx.put('connections', record.id, record);
|
|
1137
|
+
await this.audit(tx, record.id, 'retire', 'local');
|
|
1138
|
+
}
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
// Only a retired account is left, and that is listed as retired and nothing else.
|
|
1142
|
+
if (!records.length)
|
|
1143
|
+
throw new Unavailable();
|
|
1144
|
+
const mark = as === 'current' ? undefined : as;
|
|
1145
|
+
for (const record of records) {
|
|
1146
|
+
if (record.mark === mark)
|
|
1147
|
+
continue;
|
|
1148
|
+
if (mark)
|
|
1149
|
+
record.mark = mark;
|
|
1150
|
+
else
|
|
1151
|
+
delete record.mark;
|
|
1152
|
+
await tx.put('connections', record.id, record);
|
|
1153
|
+
await this.audit(tx, record.id, `mark-${as}`, 'local');
|
|
1154
|
+
}
|
|
1155
|
+
if (as !== 'preferred')
|
|
1156
|
+
return;
|
|
1157
|
+
// One preferred account per holder: picking another moves the mark. Only that mark
|
|
1158
|
+
// is taken from the others, and what they say of being unused is left alone.
|
|
1159
|
+
for (const other of await tx.list('connections')) {
|
|
1160
|
+
if (other.local.id !== local.id ||
|
|
1161
|
+
other.revokedAt !== undefined ||
|
|
1162
|
+
other.mark !== 'preferred' ||
|
|
1163
|
+
records.some((record) => record.id === other.id))
|
|
1164
|
+
continue;
|
|
1165
|
+
delete other.mark;
|
|
1166
|
+
await tx.put('connections', other.id, other);
|
|
1167
|
+
await this.audit(tx, other.id, 'mark-current', 'local');
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1049
1171
|
async share(id, local, revoke = false) {
|
|
1050
1172
|
const token = secret();
|
|
1051
1173
|
return this.transaction(async (tx) => {
|
|
@@ -1103,6 +1225,8 @@ export class VerilyService {
|
|
|
1103
1225
|
* `budget` bounds the reads per run, since providers rate-limit and the alarm this runs
|
|
1104
1226
|
* on is shared. Oldest first, so nothing starves however many connections are waiting.
|
|
1105
1227
|
*
|
|
1228
|
+
* A retired record is never asked about: its proof is no longer expected to be there.
|
|
1229
|
+
*
|
|
1106
1230
|
* Returns how many were confirmed. A connection revoked here is not one of them.
|
|
1107
1231
|
*/
|
|
1108
1232
|
async recheck(budget = 5) {
|
|
@@ -1111,7 +1235,10 @@ export class VerilyService {
|
|
|
1111
1235
|
const interval = this.options.recheckMs ?? 86400000;
|
|
1112
1236
|
const now = this.now();
|
|
1113
1237
|
const due = await this.transaction(async (tx) => (await tx.list('connections'))
|
|
1114
|
-
.filter((c) => c.revokedAt === undefined &&
|
|
1238
|
+
.filter((c) => c.revokedAt === undefined &&
|
|
1239
|
+
c.retiredAt === undefined &&
|
|
1240
|
+
c.expiresAt > now &&
|
|
1241
|
+
c.attestations)
|
|
1115
1242
|
.flatMap((connection) => connection
|
|
1116
1243
|
.attestations.external.map((attestation) => ({
|
|
1117
1244
|
connection,
|
|
@@ -1166,8 +1293,12 @@ export class VerilyService {
|
|
|
1166
1293
|
}
|
|
1167
1294
|
return this.transaction(async (tx) => {
|
|
1168
1295
|
const current = await tx.get('connections', connection.id);
|
|
1169
|
-
// It may have been revoked or reproved while the fetch was in flight.
|
|
1170
|
-
|
|
1296
|
+
// It may have been revoked or reproved while the fetch was in flight. Or retired, and
|
|
1297
|
+
// then an answer written now would move the proof date it was frozen with, or revoke
|
|
1298
|
+
// it on a withdrawal.
|
|
1299
|
+
if (!current?.attestations ||
|
|
1300
|
+
current.revokedAt !== undefined ||
|
|
1301
|
+
current.retiredAt !== undefined)
|
|
1171
1302
|
return 0;
|
|
1172
1303
|
const [first, ...rest] = current.attestations.external;
|
|
1173
1304
|
const held = main ? first : rest.find((a) => a.method === method);
|
|
@@ -1197,7 +1328,11 @@ export class VerilyService {
|
|
|
1197
1328
|
}),
|
|
1198
1329
|
]);
|
|
1199
1330
|
}
|
|
1200
|
-
/**
|
|
1331
|
+
/**
|
|
1332
|
+
* Run periodically. Pending secrets expire immediately; historical evidence defaults to 90
|
|
1333
|
+
* days. A retired record is history its holder asked to keep, so it stays for as long as
|
|
1334
|
+
* it is not revoked, and once revoked goes like any other.
|
|
1335
|
+
*/
|
|
1201
1336
|
async prune(retentionMs = 90 * 86400000) {
|
|
1202
1337
|
if (!Number.isSafeInteger(retentionMs) || retentionMs < 0)
|
|
1203
1338
|
throw new Error('Invalid retention');
|
|
@@ -1211,7 +1346,9 @@ export class VerilyService {
|
|
|
1211
1346
|
await tx.delete('limits', limit.id);
|
|
1212
1347
|
const audit = await tx.list('audit');
|
|
1213
1348
|
for (const connection of await tx.list('connections')) {
|
|
1214
|
-
|
|
1349
|
+
const ended = connection.revokedAt ??
|
|
1350
|
+
(connection.retiredAt === undefined ? connection.expiresAt : Infinity);
|
|
1351
|
+
if (ended + retentionMs <= now) {
|
|
1215
1352
|
await tx.delete('connections', connection.id);
|
|
1216
1353
|
await tx.delete('shares', connection.id);
|
|
1217
1354
|
}
|
|
@@ -1262,6 +1399,15 @@ function connected(connection) {
|
|
|
1262
1399
|
return (connection.connectedAt ??
|
|
1263
1400
|
Math.min(connection.approvedAt, connection.authenticatedAt, connection.visibilityApprovedAt, ...(connection.attestations?.external.map((a) => a.confirmedAt) ?? [])));
|
|
1264
1401
|
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Whether a renewal by this method may go ahead. Any may renew a live record. A retired one
|
|
1404
|
+
* is revived by the method it was first shown by alone, which replaces the proof the
|
|
1405
|
+
* record is judged by.
|
|
1406
|
+
*/
|
|
1407
|
+
function revives(connection, provider) {
|
|
1408
|
+
return (connection.retiredAt === undefined ||
|
|
1409
|
+
(connection.attestations?.external[0].method ?? 'oauth') === providerMethod(provider));
|
|
1410
|
+
}
|
|
1265
1411
|
/**
|
|
1266
1412
|
* Whether two methods named the same account. Where both carry an id their provider
|
|
1267
1413
|
* issued, that id settles it. A method that learns only an address, as a link back does,
|
package/dist/site/index.d.ts
CHANGED
|
@@ -61,7 +61,8 @@ export interface SiteSubject {
|
|
|
61
61
|
export interface SiteResult {
|
|
62
62
|
site: string;
|
|
63
63
|
id: string;
|
|
64
|
-
|
|
64
|
+
/** `mark` is an account listed another way or retired: read its records again to see how. */
|
|
65
|
+
operation: 'connect' | 'renew' | 'visibility' | 'disconnect' | 'mark';
|
|
65
66
|
outcome: 'complete' | 'cancelled' | 'failed';
|
|
66
67
|
/** Present when the outcome is complete. */
|
|
67
68
|
connection?: string;
|