@interop/wallet-core 0.101.0 → 0.103.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 +35 -19
- package/dist/audiences/audienceGrant.d.ts +49 -0
- package/dist/audiences/audienceGrant.d.ts.map +1 -0
- package/dist/audiences/audienceGrant.js +46 -0
- package/dist/audiences/audienceGrant.js.map +1 -0
- package/dist/audiences/index.d.ts +10 -0
- package/dist/audiences/index.d.ts.map +1 -0
- package/dist/audiences/index.js +10 -0
- package/dist/audiences/index.js.map +1 -0
- package/dist/clientAnnex/gc.d.ts +107 -25
- package/dist/clientAnnex/gc.d.ts.map +1 -1
- package/dist/clientAnnex/gc.js +258 -89
- package/dist/clientAnnex/gc.js.map +1 -1
- package/dist/clientAnnex/grantRevocation.d.ts +0 -10
- package/dist/clientAnnex/grantRevocation.d.ts.map +1 -1
- package/dist/clientAnnex/grantRevocation.js +1 -23
- package/dist/clientAnnex/grantRevocation.js.map +1 -1
- package/dist/clientAnnex/heal.d.ts +32 -0
- package/dist/clientAnnex/heal.d.ts.map +1 -1
- package/dist/clientAnnex/heal.js +52 -50
- package/dist/clientAnnex/heal.js.map +1 -1
- package/dist/clientAnnex/index.d.ts +2 -2
- package/dist/clientAnnex/index.d.ts.map +1 -1
- package/dist/clientAnnex/index.js +2 -2
- package/dist/clientAnnex/index.js.map +1 -1
- package/dist/clientAnnex/log.d.ts +20 -2
- package/dist/clientAnnex/log.d.ts.map +1 -1
- package/dist/clientAnnex/log.js +30 -4
- package/dist/clientAnnex/log.js.map +1 -1
- package/dist/clients/rosterPolicy.d.ts.map +1 -1
- package/dist/clients/rosterPolicy.js +3 -1
- package/dist/clients/rosterPolicy.js.map +1 -1
- package/dist/connections/didKey.d.ts +27 -0
- package/dist/connections/didKey.d.ts.map +1 -1
- package/dist/connections/didKey.js +42 -6
- package/dist/connections/didKey.js.map +1 -1
- package/dist/connections/entry.d.ts +95 -13
- package/dist/connections/entry.d.ts.map +1 -1
- package/dist/connections/entry.js +102 -16
- package/dist/connections/entry.js.map +1 -1
- package/dist/connections/grants.d.ts +225 -3
- package/dist/connections/grants.d.ts.map +1 -1
- package/dist/connections/grants.js +293 -8
- package/dist/connections/grants.js.map +1 -1
- package/dist/connections/index.d.ts +15 -8
- package/dist/connections/index.d.ts.map +1 -1
- package/dist/connections/index.js +13 -6
- package/dist/connections/index.js.map +1 -1
- package/dist/connections/read.d.ts.map +1 -1
- package/dist/connections/read.js +2 -1
- package/dist/connections/read.js.map +1 -1
- package/dist/connections/resourceId.d.ts +33 -0
- package/dist/connections/resourceId.d.ts.map +1 -1
- package/dist/connections/resourceId.js +88 -8
- package/dist/connections/resourceId.js.map +1 -1
- package/dist/connections/upsert.d.ts +219 -7
- package/dist/connections/upsert.d.ts.map +1 -1
- package/dist/connections/upsert.js +628 -41
- package/dist/connections/upsert.js.map +1 -1
- package/dist/connections/writers.js +2 -1
- package/dist/connections/writers.js.map +1 -1
- package/dist/descriptors/index.d.ts +5 -0
- package/dist/descriptors/index.d.ts.map +1 -1
- package/dist/descriptors/index.js +5 -0
- package/dist/descriptors/index.js.map +1 -1
- package/dist/descriptors/logSource.d.ts +9 -4
- package/dist/descriptors/logSource.d.ts.map +1 -1
- package/dist/descriptors/logSource.js +13 -6
- package/dist/descriptors/logSource.js.map +1 -1
- package/dist/jsonObject.d.ts +16 -0
- package/dist/jsonObject.d.ts.map +1 -0
- package/dist/jsonObject.js +18 -0
- package/dist/jsonObject.js.map +1 -0
- package/dist/keys/index.d.ts +13 -9
- package/dist/keys/index.d.ts.map +1 -1
- package/dist/keys/index.js +11 -7
- package/dist/keys/index.js.map +1 -1
- package/dist/keys/userKeyCascade.d.ts +46 -0
- package/dist/keys/userKeyCascade.d.ts.map +1 -1
- package/dist/keys/userKeyCascade.js +153 -65
- package/dist/keys/userKeyCascade.js.map +1 -1
- package/dist/keys/userKeyRoster.d.ts +60 -7
- package/dist/keys/userKeyRoster.d.ts.map +1 -1
- package/dist/keys/userKeyRoster.js +165 -47
- package/dist/keys/userKeyRoster.js.map +1 -1
- package/dist/keys/userKeyRosterCascade.d.ts.map +1 -1
- package/dist/keys/userKeyRosterCascade.js +1 -0
- package/dist/keys/userKeyRosterCascade.js.map +1 -1
- package/dist/menders/ids.d.ts +1 -1
- package/dist/menders/ids.d.ts.map +1 -1
- package/dist/menders/ids.js +20 -4
- package/dist/menders/ids.js.map +1 -1
- package/dist/space/collections.d.ts +26 -6
- package/dist/space/collections.d.ts.map +1 -1
- package/dist/space/collections.js +31 -7
- package/dist/space/collections.js.map +1 -1
- package/dist/space/inboxMessage.d.ts +47 -0
- package/dist/space/inboxMessage.d.ts.map +1 -0
- package/dist/space/inboxMessage.js +86 -0
- package/dist/space/inboxMessage.js.map +1 -0
- package/dist/space/index.d.ts +7 -2
- package/dist/space/index.d.ts.map +1 -1
- package/dist/space/index.js +6 -2
- package/dist/space/index.js.map +1 -1
- package/dist/sync/contactsConflict.d.ts +23 -10
- package/dist/sync/contactsConflict.d.ts.map +1 -1
- package/dist/sync/contactsConflict.js +40 -21
- package/dist/sync/contactsConflict.js.map +1 -1
- package/dist/sync/index.d.ts +2 -2
- package/dist/sync/index.js +2 -2
- package/dist/webvh/didWebProjection.d.ts +38 -5
- package/dist/webvh/didWebProjection.d.ts.map +1 -1
- package/dist/webvh/didWebProjection.js +35 -22
- package/dist/webvh/didWebProjection.js.map +1 -1
- package/dist/webvh/index.d.ts +4 -3
- package/dist/webvh/index.d.ts.map +1 -1
- package/dist/webvh/index.js +4 -3
- package/dist/webvh/index.js.map +1 -1
- package/dist/webvh/standingZcap.d.ts +19 -2
- package/dist/webvh/standingZcap.d.ts.map +1 -1
- package/dist/webvh/standingZcap.js +31 -3
- package/dist/webvh/standingZcap.js.map +1 -1
- package/package.json +12 -6
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* A capability to record, read by the codec, or a `TypeError` naming the
|
|
6
|
+
* write when it lacks an id, a controller, or an invocation target.
|
|
7
|
+
*
|
|
8
|
+
* @param options {object}
|
|
9
|
+
* @param options.value {object}
|
|
10
|
+
* @param options.what {string} the write's name, for the message
|
|
11
|
+
* @returns {ConnectionZcap}
|
|
12
|
+
*/
|
|
13
|
+
function requiredConnectionZcap({ value, what }) {
|
|
14
|
+
const zcap = parseConnectionZcap(value);
|
|
15
|
+
if (zcap === undefined) {
|
|
16
|
+
throw new TypeError(`A ${what} must carry a capability with an id, a controller, and an ` +
|
|
17
|
+
'invocation target.');
|
|
18
|
+
}
|
|
19
|
+
return zcap;
|
|
20
|
+
}
|
|
4
21
|
/**
|
|
5
22
|
* The upsert helpers every flow writes a party's entry through:
|
|
6
23
|
* `recordGrants` at consent, `removeGrants` at an unshare or a torn
|
|
7
24
|
* consent's rollback, `retireConnection` when a relationship ends,
|
|
8
|
-
* `unretireConnection` when one resumes without a consent,
|
|
9
|
-
* `setConnectionLabel` for the user's rename and the enrollment-time name
|
|
25
|
+
* `unretireConnection` when one resumes without a consent,
|
|
26
|
+
* `setConnectionLabel` for the user's rename and the enrollment-time name,
|
|
27
|
+
* and the agent-connection helpers over `grantsReceived` and `outbox`:
|
|
28
|
+
* `recordReceivedGrants`, `recordRenewedGrants` (the renewal's one write),
|
|
29
|
+
* `settleOutboxItem`, `pruneSupersededGrants`, `clearReceivedGrants`, and
|
|
30
|
+
* `markDeclined`.
|
|
10
31
|
*
|
|
11
32
|
* Each runs `writeConnection`, the one bounded compare-and-swap loop at a
|
|
12
33
|
* party's resource id, which the writer arm (`writers.ts`) runs too. It reads
|
|
@@ -22,15 +43,18 @@
|
|
|
22
43
|
*/
|
|
23
44
|
import { NotSupportedError } from '@interop/was-client';
|
|
24
45
|
import { errorNameOf } from '../errorName.js';
|
|
46
|
+
import { isJsonObject } from '../jsonObject.js';
|
|
25
47
|
import { normalizeDisplayName } from '../labelText.js';
|
|
48
|
+
import { REVOCATION_CLOCK_SKEW_MS } from '../webvh/index.js';
|
|
26
49
|
import { signingKeyMultibaseOfDid } from './didKey.js';
|
|
27
|
-
import { CONNECTION_ENTRY_VERSION, GRANT_KINDS, isWritableConnectionEntry, newConnectionEntry, parseConnectionEntry, parseConnectionZcap } from './entry.js';
|
|
50
|
+
import { CONNECTION_ENTRY_VERSION, GRANT_KINDS, RECEIVED_GRANT_KINDS, isWritableConnectionEntry, newConnectionEntry, parseConnectionEntry, parseConnectionZcap } from './entry.js';
|
|
28
51
|
import { ConnectionKindMismatchError } from './errors.js';
|
|
29
|
-
import { connectionGrants, isTargetInSpace } from './grants.js';
|
|
52
|
+
import { allowsAction, connectionGrants, grantScopeKey, latestIndexPerScope, isTargetInSpace, liveInboxChannel, receivedGrantLapsed, receivedGrants } from './grants.js';
|
|
30
53
|
import { claimedResourceId } from './read.js';
|
|
31
|
-
import { connectionResourceId } from './resourceId.js';
|
|
54
|
+
import { connectionResourceId, verifyConnectionSeedTag } from './resourceId.js';
|
|
32
55
|
const PRECONDITION_FAILED_ERROR_NAME = 'PreconditionFailedError';
|
|
33
56
|
const WRITE_ATTEMPTS = 3;
|
|
57
|
+
const STRICT_ISO_8601 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
|
|
34
58
|
/**
|
|
35
59
|
* Whether a store write lost a race. Matched by name, since the store is an
|
|
36
60
|
* injected seam that may raise from a second copy of was-client.
|
|
@@ -188,24 +212,19 @@ function assertKind({ entry, kind }) {
|
|
|
188
212
|
}
|
|
189
213
|
}
|
|
190
214
|
/**
|
|
191
|
-
*
|
|
192
|
-
* well-formed, its `controller` the party's DID, its `invocationTarget`
|
|
193
|
-
* this Space
|
|
194
|
-
*
|
|
195
|
-
* revocation index will see.
|
|
215
|
+
* A capability the wallet delegated, checked strictly: its known members
|
|
216
|
+
* well-formed, its `controller` the party's DID, its `invocationTarget`
|
|
217
|
+
* under this Space. The target check is the same one every reader applies,
|
|
218
|
+
* so a grant that is stored is a grant the revocation index will see.
|
|
196
219
|
*
|
|
197
220
|
* @param options {object}
|
|
198
221
|
* @param options.did {string}
|
|
199
222
|
* @param options.spaceUrl {string}
|
|
200
|
-
* @param options.
|
|
223
|
+
* @param options.zcap {object}
|
|
201
224
|
* @returns {ConnectionZcap}
|
|
202
225
|
*/
|
|
203
|
-
function
|
|
204
|
-
const zcap =
|
|
205
|
-
if (zcap === undefined) {
|
|
206
|
-
throw new TypeError('A grant to record must carry a capability with an id, a controller, ' +
|
|
207
|
-
'and an invocation target.');
|
|
208
|
-
}
|
|
226
|
+
function checkedDelegatedZcap({ did, spaceUrl, zcap: value }) {
|
|
227
|
+
const zcap = requiredConnectionZcap({ value, what: 'grant to record' });
|
|
209
228
|
if (zcap.controller !== did) {
|
|
210
229
|
throw new TypeError(`A grant to record must be delegated to the party "${did}"; its ` +
|
|
211
230
|
`controller is "${zcap.controller}".`);
|
|
@@ -214,19 +233,182 @@ function checkedGrant({ did, spaceUrl, grant }) {
|
|
|
214
233
|
throw new TypeError('A grant to record must target this Space; its invocation target is ' +
|
|
215
234
|
`"${zcap.invocationTarget}".`);
|
|
216
235
|
}
|
|
236
|
+
return zcap;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The capability of one grant to record: {@link checkedDelegatedZcap}, and
|
|
240
|
+
* its kind one this build writes.
|
|
241
|
+
*
|
|
242
|
+
* @param options {object}
|
|
243
|
+
* @param options.did {string}
|
|
244
|
+
* @param options.spaceUrl {string}
|
|
245
|
+
* @param options.grant {object}
|
|
246
|
+
* @returns {ConnectionZcap}
|
|
247
|
+
*/
|
|
248
|
+
function checkedGrant({ did, spaceUrl, grant }) {
|
|
249
|
+
const zcap = checkedDelegatedZcap({ did, spaceUrl, zcap: grant.zcap });
|
|
217
250
|
if (!GRANT_KINDS.includes(grant.grantKind)) {
|
|
218
251
|
throw new TypeError(`Unknown grant kind "${grant.grantKind}".`);
|
|
219
252
|
}
|
|
220
253
|
return zcap;
|
|
221
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* A capability's `expires` as epoch milliseconds. The codec has already
|
|
257
|
+
* checked the member is present and parses.
|
|
258
|
+
*
|
|
259
|
+
* @param zcap {ConnectionZcap}
|
|
260
|
+
* @returns {number}
|
|
261
|
+
*/
|
|
262
|
+
function expiresAtOf(zcap) {
|
|
263
|
+
return Date.parse(zcap.expires);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Whether a capability's `expires` is past by more than the revocation clock
|
|
267
|
+
* skew.
|
|
268
|
+
*
|
|
269
|
+
* @param options {object}
|
|
270
|
+
* @param options.zcap {ConnectionZcap}
|
|
271
|
+
* @param options.now {Date}
|
|
272
|
+
* @returns {boolean}
|
|
273
|
+
*/
|
|
274
|
+
function expiredBeyondSkew({ zcap, now }) {
|
|
275
|
+
return expiresAtOf(zcap) + REVOCATION_CLOCK_SKEW_MS <= now.getTime();
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* The index of the latest record in each scope of a list of grant records
|
|
279
|
+
* ({@link latestIndexPerScope} over their capabilities).
|
|
280
|
+
*
|
|
281
|
+
* @param options {object}
|
|
282
|
+
* @param options.grants {ConnectionGrantRecord[]}
|
|
283
|
+
* @returns {Map<string, number>} scope key to the latest record's index
|
|
284
|
+
*/
|
|
285
|
+
function latestPerScope({ grants }) {
|
|
286
|
+
return latestIndexPerScope({ zcaps: grants.map(grant => grant.zcap) });
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* The capabilities an envelope carries (`message.object.zcaps`), or
|
|
290
|
+
* `undefined` when it carries no such list.
|
|
291
|
+
*
|
|
292
|
+
* @param message {object} the envelope, verbatim
|
|
293
|
+
* @returns {unknown[] | undefined}
|
|
294
|
+
*/
|
|
295
|
+
function envelopeZcaps(message) {
|
|
296
|
+
const { object } = message;
|
|
297
|
+
if (!isJsonObject(object) || !Array.isArray(object.zcaps)) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
return object.zcaps;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Checks an envelope a write queues beside the records it carries: its
|
|
304
|
+
* `object.zcaps` must be a non-empty list whose zcap ids are exactly
|
|
305
|
+
* `zcapIds`, so a reused message cannot queue a capability the write does
|
|
306
|
+
* not record. Refuses with a `TypeError`.
|
|
307
|
+
*
|
|
308
|
+
* @param options {object}
|
|
309
|
+
* @param options.message {object} the envelope, verbatim
|
|
310
|
+
* @param options.zcapIds {Iterable<string>} the ids the write records
|
|
311
|
+
* @param options.what {string} the write's name, for the message
|
|
312
|
+
*/
|
|
313
|
+
function assertEnvelopeCarries({ message, zcapIds, what }) {
|
|
314
|
+
const zcaps = envelopeZcaps(message);
|
|
315
|
+
if (zcaps === undefined || zcaps.length === 0) {
|
|
316
|
+
throw new TypeError(`A ${what} message must carry a non-empty \`object.zcaps\` list.`);
|
|
317
|
+
}
|
|
318
|
+
const messageIds = new Set(zcaps.map(value => parseConnectionZcap(value)?.id));
|
|
319
|
+
const recordedIds = new Set(zcapIds);
|
|
320
|
+
if (messageIds.size !== recordedIds.size ||
|
|
321
|
+
[...messageIds].some(id => id === undefined || !recordedIds.has(id))) {
|
|
322
|
+
throw new TypeError(`A ${what} message's zcaps must be exactly the recorded zcaps.`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Refuses a `seed` without its `seedTag` (or the reverse) and a tag that does
|
|
327
|
+
* not verify under `hmacKey` with a `TypeError`, before anything is read.
|
|
328
|
+
* A write carrying neither passes.
|
|
329
|
+
*
|
|
330
|
+
* @param options {object}
|
|
331
|
+
* @param options.hmacKey {ConnectionIdKey}
|
|
332
|
+
* @param [options.seed] {string}
|
|
333
|
+
* @param [options.seedTag] {string}
|
|
334
|
+
* @returns {Promise<void>}
|
|
335
|
+
*/
|
|
336
|
+
async function assertPairwiseSeed({ hmacKey, seed, seedTag }) {
|
|
337
|
+
if (seed === undefined && seedTag === undefined) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (seed === undefined || seedTag === undefined) {
|
|
341
|
+
throw new TypeError('A connection seed is written with its tag, or not.');
|
|
342
|
+
}
|
|
343
|
+
if (!(await verifyConnectionSeedTag({ hmacKey, seed, seedTag }))) {
|
|
344
|
+
throw new TypeError("The connection seed's tag does not verify under this directory's key.");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* The outbox after queueing `message`: the pending items with every zcap
|
|
349
|
+
* `supersedes` admits removed from their envelopes (an envelope left with
|
|
350
|
+
* none is dropped), then the new envelope stamped `createdAt: stamp`. A
|
|
351
|
+
* zcap the codec cannot parse is kept.
|
|
352
|
+
*
|
|
353
|
+
* @param options {object}
|
|
354
|
+
* @param options.outbox {Array<ConnectionOutboxItem>} the pending items
|
|
355
|
+
* @param options.message {object} the envelope to queue, verbatim
|
|
356
|
+
* @param options.stamp {string} the queueing time, ISO 8601 UTC
|
|
357
|
+
* @param options.supersedes {Function} whether the new envelope supersedes
|
|
358
|
+
* a pending zcap
|
|
359
|
+
* @returns {Array<ConnectionOutboxItem>}
|
|
360
|
+
*/
|
|
361
|
+
function supersedeAndQueueEnvelope({ outbox, message, stamp, supersedes }) {
|
|
362
|
+
const queued = [];
|
|
363
|
+
for (const item of outbox) {
|
|
364
|
+
const zcaps = envelopeZcaps(item.message);
|
|
365
|
+
if (zcaps === undefined) {
|
|
366
|
+
queued.push(item);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const kept = zcaps.filter(value => {
|
|
370
|
+
const zcap = parseConnectionZcap(value);
|
|
371
|
+
return zcap === undefined || !supersedes(zcap);
|
|
372
|
+
});
|
|
373
|
+
if (kept.length === zcaps.length) {
|
|
374
|
+
queued.push(item);
|
|
375
|
+
}
|
|
376
|
+
else if (kept.length > 0) {
|
|
377
|
+
const itemObject = item.message.object;
|
|
378
|
+
queued.push({
|
|
379
|
+
...item,
|
|
380
|
+
message: { ...item.message, object: { ...itemObject, zcaps: kept } }
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
queued.push({ message, createdAt: stamp });
|
|
385
|
+
return queued;
|
|
386
|
+
}
|
|
222
387
|
/**
|
|
223
388
|
* Records a consent on the party's entry: creates the entry, or merges into
|
|
224
389
|
* it. Each new grant is added by its capability id (one already recorded is
|
|
225
390
|
* left as stored), `name` is set when it passes the display-name rule (left
|
|
226
391
|
* as it was otherwise), `origin`, `url`, and `appKey` are set when given,
|
|
227
|
-
* `retired`
|
|
228
|
-
* only on create, and `label` is never touched.
|
|
229
|
-
*
|
|
392
|
+
* `retired` and `declined` are cleared, and `lastSeen` moves to `now`.
|
|
393
|
+
* `firstSeen` is set only on create, and `label` is never touched. Every
|
|
394
|
+
* grant of one call carries the same `grantedAt`. A zero-grant consent
|
|
395
|
+
* writes too, so an App Connect app with no grants is indexed by its key.
|
|
396
|
+
*
|
|
397
|
+
* A `message` (an envelope carrying exactly the grants of this call) is
|
|
398
|
+
* queued on `outbox` in the same write, stamped `now`, so a restore that
|
|
399
|
+
* records fresh grants and owes the party their envelope has no tear between
|
|
400
|
+
* the two. Each zcap of this call is first removed from every pending
|
|
401
|
+
* envelope that carries it (an envelope left with none is dropped), so no
|
|
402
|
+
* two pending envelopes name one zcap id. A consent torn after its write
|
|
403
|
+
* lands and re-run therefore leaves one pending envelope, the re-run's, and
|
|
404
|
+
* a grant recorded earlier with no envelope is told on the first call that
|
|
405
|
+
* brings one.
|
|
406
|
+
*
|
|
407
|
+
* A `seed` and its `seedTag` (both or neither) are written when the entry
|
|
408
|
+
* carries no seed, and left as stored when it carries the same one. An entry
|
|
409
|
+
* carrying a different seed is refused with an `Error`, since the seed is
|
|
410
|
+
* written once. A tag that does not verify under `hmacKey` is refused with a
|
|
411
|
+
* `TypeError` before anything is read.
|
|
230
412
|
*
|
|
231
413
|
* Refuses an entry of another kind with {@link ConnectionKindMismatchError},
|
|
232
414
|
* and a grant delegated to another party or targeting another Space with a
|
|
@@ -246,10 +428,16 @@ function checkedGrant({ did, spaceUrl, grant }) {
|
|
|
246
428
|
* @param [options.origin] {string} the attested Web origin (App Connect)
|
|
247
429
|
* @param [options.url] {string} the canonical app URL (App Connect)
|
|
248
430
|
* @param [options.appKey] {string} the app-key Resource id (App Connect)
|
|
431
|
+
* @param [options.seed] {string} the wallet's pairwise seed toward the
|
|
432
|
+
* party, base64url with no padding
|
|
433
|
+
* @param [options.seedTag] {string} the seed's tag (`connectionSeedTag`)
|
|
434
|
+
* @param [options.message] {object} an envelope to queue on `outbox` with
|
|
435
|
+
* the consent, carrying exactly the grants of this call
|
|
249
436
|
* @param [options.now] {Date}
|
|
250
437
|
* @returns {Promise<ConnectionWriteResult>}
|
|
251
438
|
*/
|
|
252
|
-
export async function recordGrants({ store, hmacKey, did, kind, spaceUrl, grants, name, origin, url, appKey, now = new Date() }) {
|
|
439
|
+
export async function recordGrants({ store, hmacKey, did, kind, spaceUrl, grants, name, origin, url, appKey, seed, seedTag, message, now = new Date() }) {
|
|
440
|
+
await assertPairwiseSeed({ hmacKey, seed, seedTag });
|
|
253
441
|
const stamp = now.toISOString();
|
|
254
442
|
const recorded = [];
|
|
255
443
|
const seen = new Set();
|
|
@@ -264,6 +452,13 @@ export async function recordGrants({ store, hmacKey, did, kind, spaceUrl, grants
|
|
|
264
452
|
recorded.push({ zcap, grantKind: grant.grantKind, grantedAt: stamp });
|
|
265
453
|
}
|
|
266
454
|
}
|
|
455
|
+
if (message !== undefined) {
|
|
456
|
+
assertEnvelopeCarries({
|
|
457
|
+
message,
|
|
458
|
+
zcapIds: recorded.map(grant => grant.zcap.id),
|
|
459
|
+
what: 'consent'
|
|
460
|
+
});
|
|
461
|
+
}
|
|
267
462
|
const normalizedName = name === undefined ? undefined : normalizeDisplayName({ value: name });
|
|
268
463
|
const declared = {
|
|
269
464
|
...(normalizedName !== undefined && { name: normalizedName }),
|
|
@@ -276,27 +471,48 @@ export async function recordGrants({ store, hmacKey, did, kind, spaceUrl, grants
|
|
|
276
471
|
hmacKey,
|
|
277
472
|
did,
|
|
278
473
|
change(current) {
|
|
474
|
+
const stored = current?.entry;
|
|
475
|
+
const held = new Set(stored?.grants.map(grant => grant.zcap.id));
|
|
476
|
+
let body;
|
|
279
477
|
if (current === undefined) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
478
|
+
body = {
|
|
479
|
+
...newConnectionEntry({ kind, did, now }),
|
|
480
|
+
...declared,
|
|
481
|
+
grants: recorded
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
assertKind({ entry: current.entry, kind });
|
|
486
|
+
body = {
|
|
487
|
+
...current.body,
|
|
488
|
+
...declared,
|
|
489
|
+
lastSeen: stamp,
|
|
490
|
+
grants: [
|
|
491
|
+
...current.entry.grants,
|
|
492
|
+
...recorded.filter(grant => !held.has(grant.zcap.id))
|
|
493
|
+
]
|
|
494
|
+
};
|
|
495
|
+
delete body.retired;
|
|
496
|
+
delete body.declined;
|
|
497
|
+
}
|
|
498
|
+
if (seed !== undefined && stored?.seed === undefined) {
|
|
499
|
+
body = { ...body, seed, seedTag };
|
|
500
|
+
}
|
|
501
|
+
else if (seed !== undefined && stored?.seed !== seed) {
|
|
502
|
+
throw new Error('The connections entry already carries a different pairwise seed; ' +
|
|
503
|
+
'a seed is written once.');
|
|
504
|
+
}
|
|
505
|
+
if (message !== undefined) {
|
|
506
|
+
body = {
|
|
507
|
+
...body,
|
|
508
|
+
outbox: supersedeAndQueueEnvelope({
|
|
509
|
+
outbox: stored?.outbox ?? [],
|
|
510
|
+
message,
|
|
511
|
+
stamp,
|
|
512
|
+
supersedes: zcap => seen.has(zcap.id)
|
|
513
|
+
})
|
|
286
514
|
};
|
|
287
515
|
}
|
|
288
|
-
assertKind({ entry: current.entry, kind });
|
|
289
|
-
const held = new Set(current.entry.grants.map(grant => grant.zcap.id));
|
|
290
|
-
const body = {
|
|
291
|
-
...current.body,
|
|
292
|
-
...declared,
|
|
293
|
-
lastSeen: stamp,
|
|
294
|
-
grants: [
|
|
295
|
-
...current.entry.grants,
|
|
296
|
-
...recorded.filter(grant => !held.has(grant.zcap.id))
|
|
297
|
-
]
|
|
298
|
-
};
|
|
299
|
-
delete body.retired;
|
|
300
516
|
return { body };
|
|
301
517
|
}
|
|
302
518
|
});
|
|
@@ -334,8 +550,9 @@ export async function removeGrants({ store, hmacKey, did, zcapIds }) {
|
|
|
334
550
|
}
|
|
335
551
|
/**
|
|
336
552
|
* Retires the party's entry, of any kind, once a revocation has handled its
|
|
337
|
-
* grants: empties `grants`,
|
|
338
|
-
* `label`, `origin`, `url`, `firstSeen`,
|
|
553
|
+
* grants: empties `grants`, `grantsReceived`, and `outbox`, drops `appKey`,
|
|
554
|
+
* and stamps `retired`. `name`, `label`, `origin`, `url`, `firstSeen`,
|
|
555
|
+
* `lastSeen`, `writers`, `seed`, `seedTag`, and `declined` stay, so a
|
|
339
556
|
* departed party keeps resolving. An entry already retired with nothing left
|
|
340
557
|
* to empty writes nothing, and an absent entry writes nothing.
|
|
341
558
|
*
|
|
@@ -381,12 +598,16 @@ export async function retireConnection({ store, hmacKey, did, handledZcapIds, sp
|
|
|
381
598
|
const { entry } = current;
|
|
382
599
|
if (entry.retired !== undefined &&
|
|
383
600
|
entry.grants.length === 0 &&
|
|
601
|
+
entry.grantsReceived.length === 0 &&
|
|
602
|
+
entry.outbox.length === 0 &&
|
|
384
603
|
entry.appKey === undefined) {
|
|
385
604
|
return { stop: 'unchanged' };
|
|
386
605
|
}
|
|
387
606
|
const body = {
|
|
388
607
|
...current.body,
|
|
389
608
|
grants: [],
|
|
609
|
+
grantsReceived: [],
|
|
610
|
+
outbox: [],
|
|
390
611
|
retired: entry.retired ?? now.toISOString()
|
|
391
612
|
};
|
|
392
613
|
delete body.appKey;
|
|
@@ -515,4 +736,370 @@ export async function setConnectionLabel({ store, hmacKey, did, kind, label, nam
|
|
|
515
736
|
}
|
|
516
737
|
});
|
|
517
738
|
}
|
|
739
|
+
/**
|
|
740
|
+
* Records the capabilities a party handed the wallet on its entry's
|
|
741
|
+
* `grantsReceived`. Each new record is merged in turn: one whose capability
|
|
742
|
+
* id is already held is left as stored, so the drain's re-run is a no-op;
|
|
743
|
+
* one with the `grantKind` and `invocationTarget` of a held record replaces
|
|
744
|
+
* it when its `expires` is strictly later or when the held record has
|
|
745
|
+
* lapsed as of `now` (at the earlier of its `expires` and `receivedAt` plus
|
|
746
|
+
* `channelMaxLifetimeMs`), and leaves the held record otherwise; any other
|
|
747
|
+
* is appended. Every record whose `expires`
|
|
748
|
+
* is past by more than the revocation clock skew is then dropped. A new
|
|
749
|
+
* record carries `receivedAt: now`.
|
|
750
|
+
*
|
|
751
|
+
* Each grant is checked strictly before anything is read, refused with a
|
|
752
|
+
* `TypeError`: its known members well-formed, its `controller` the wallet's
|
|
753
|
+
* pairwise DID toward the party, its `invocationTarget` outside this Space,
|
|
754
|
+
* its kind one of `RECEIVED_GRANT_KINDS`, an `'inbox'` grant allowing POST,
|
|
755
|
+
* and an `expires` in strict ISO 8601 form, with any number of fractional
|
|
756
|
+
* second digits (a format one engine's `Date.parse` accepts and another's
|
|
757
|
+
* refuses would make the entry unparseable there). A retired entry is refused
|
|
758
|
+
* with an `Error`, and an absent entry writes nothing.
|
|
759
|
+
*
|
|
760
|
+
* @param options {object}
|
|
761
|
+
* @param options.store {ConnectionsStore}
|
|
762
|
+
* @param options.hmacKey {ConnectionIdKey}
|
|
763
|
+
* @param options.did {string} the party's DID
|
|
764
|
+
* @param options.pairwiseDid {string} the wallet's pairwise did:key toward
|
|
765
|
+
* the party, every received grant's controller
|
|
766
|
+
* @param options.spaceUrl {string} this Space's container URL, which no
|
|
767
|
+
* received grant may target
|
|
768
|
+
* @param options.grants {Array<object>} `{ zcap, grantKind }` per grant
|
|
769
|
+
* @param options.channelMaxLifetimeMs {number} how long after `receivedAt`
|
|
770
|
+
* a received record lapses at the latest
|
|
771
|
+
* @param [options.now] {Date}
|
|
772
|
+
* @returns {Promise<ConnectionWriteResult>}
|
|
773
|
+
*/
|
|
774
|
+
export async function recordReceivedGrants({ store, hmacKey, did, pairwiseDid, spaceUrl, grants, channelMaxLifetimeMs, now = new Date() }) {
|
|
775
|
+
const stamp = now.toISOString();
|
|
776
|
+
const incoming = [];
|
|
777
|
+
for (const grant of grants) {
|
|
778
|
+
const zcap = requiredConnectionZcap({
|
|
779
|
+
value: grant.zcap,
|
|
780
|
+
what: 'received grant'
|
|
781
|
+
});
|
|
782
|
+
if (zcap.controller !== pairwiseDid) {
|
|
783
|
+
throw new TypeError(`A received grant must be delegated to the pairwise DID ` +
|
|
784
|
+
`"${pairwiseDid}"; its controller is "${zcap.controller}".`);
|
|
785
|
+
}
|
|
786
|
+
if (isTargetInSpace({ target: zcap.invocationTarget, spaceUrl })) {
|
|
787
|
+
throw new TypeError('A received grant must target the party, not this Space; its ' +
|
|
788
|
+
`invocation target is "${zcap.invocationTarget}".`);
|
|
789
|
+
}
|
|
790
|
+
if (!RECEIVED_GRANT_KINDS.includes(grant.grantKind)) {
|
|
791
|
+
throw new TypeError(`Unknown received grant kind "${grant.grantKind}".`);
|
|
792
|
+
}
|
|
793
|
+
if (grant.grantKind === 'inbox' &&
|
|
794
|
+
!allowsAction({ allowedAction: zcap.allowedAction, action: 'POST' })) {
|
|
795
|
+
throw new TypeError('An inbox grant must allow POST.');
|
|
796
|
+
}
|
|
797
|
+
if (!STRICT_ISO_8601.test(zcap.expires)) {
|
|
798
|
+
throw new TypeError("A received grant's `expires` must be an ISO 8601 date-time; got " +
|
|
799
|
+
`"${zcap.expires}".`);
|
|
800
|
+
}
|
|
801
|
+
incoming.push({ zcap, grantKind: grant.grantKind, receivedAt: stamp });
|
|
802
|
+
}
|
|
803
|
+
return writePartyConnection({
|
|
804
|
+
store,
|
|
805
|
+
hmacKey,
|
|
806
|
+
did,
|
|
807
|
+
change(current) {
|
|
808
|
+
if (current === undefined) {
|
|
809
|
+
return { stop: 'absent' };
|
|
810
|
+
}
|
|
811
|
+
if (current.entry.retired !== undefined) {
|
|
812
|
+
throw new Error('The connections entry is retired; it records no received grants.');
|
|
813
|
+
}
|
|
814
|
+
const merged = [...current.entry.grantsReceived];
|
|
815
|
+
for (const record of incoming) {
|
|
816
|
+
if (merged.some(held => held.zcap.id === record.zcap.id)) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
const sameChannel = merged.findIndex(held => held.grantKind === record.grantKind &&
|
|
820
|
+
held.zcap.invocationTarget === record.zcap.invocationTarget);
|
|
821
|
+
if (sameChannel === -1) {
|
|
822
|
+
merged.push(record);
|
|
823
|
+
}
|
|
824
|
+
else if (expiresAtOf(record.zcap) > expiresAtOf(merged[sameChannel].zcap) ||
|
|
825
|
+
receivedGrantLapsed({
|
|
826
|
+
grant: {
|
|
827
|
+
expires: merged[sameChannel].zcap.expires,
|
|
828
|
+
receivedAt: merged[sameChannel].receivedAt
|
|
829
|
+
},
|
|
830
|
+
now,
|
|
831
|
+
maxLifetimeMs: channelMaxLifetimeMs
|
|
832
|
+
})) {
|
|
833
|
+
merged[sameChannel] = record;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const kept = merged.filter(record => !expiredBeyondSkew({ zcap: record.zcap, now }));
|
|
837
|
+
if (kept.length === current.entry.grantsReceived.length &&
|
|
838
|
+
kept.every((record, index) => record === current.entry.grantsReceived[index])) {
|
|
839
|
+
return { stop: 'unchanged' };
|
|
840
|
+
}
|
|
841
|
+
return { body: { ...current.body, grantsReceived: kept } };
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* The renewal's pivot: appends the renewed grant records and queues the
|
|
847
|
+
* envelope that carries them to the party, in one compare-and-swap. Each
|
|
848
|
+
* renewed record keeps its source's `grantKind` and `grantedAt` and carries
|
|
849
|
+
* `renewedAt: now`; the source record stays as stored. On `outbox`, every
|
|
850
|
+
* zcap sharing a renewed zcap's scope is removed from the envelopes already
|
|
851
|
+
* pending, an envelope left with none is dropped, and `message` is appended
|
|
852
|
+
* with `createdAt: now`.
|
|
853
|
+
*
|
|
854
|
+
* A renewed zcap must copy its source's `invocationTarget` as stored, since
|
|
855
|
+
* the scope compares it verbatim (a trailing `*` marker included).
|
|
856
|
+
*
|
|
857
|
+
* Refuses with an `Error`, writing nothing: a retired entry (whose `retired`
|
|
858
|
+
* stays), an entry with no live inbox channel (`receivedGrants`, then
|
|
859
|
+
* `liveInboxChannel`), a renewal whose source record is gone, a renewal
|
|
860
|
+
* whose source is no longer the latest record of its scope (a concurrent
|
|
861
|
+
* renewal ran), and a renewal whose `expires` is not strictly later than its
|
|
862
|
+
* source's. The last is what keeps a renewal minted under a short-lived parent from
|
|
863
|
+
* being appended at every login. A renewed zcap not delegated to the party,
|
|
864
|
+
* not targeting this Space, or of another scope than its source, and a
|
|
865
|
+
* `message` whose zcap ids are not exactly the renewed zcap ids, are refused
|
|
866
|
+
* with a `TypeError`. An absent entry writes nothing.
|
|
867
|
+
*
|
|
868
|
+
* @param options {object}
|
|
869
|
+
* @param options.store {ConnectionsStore}
|
|
870
|
+
* @param options.hmacKey {ConnectionIdKey}
|
|
871
|
+
* @param options.did {string} the party's DID, every renewed grant's
|
|
872
|
+
* controller
|
|
873
|
+
* @param options.pairwiseDid {string} the wallet's pairwise did:key toward
|
|
874
|
+
* the party, the inbox channel's controller
|
|
875
|
+
* @param options.spaceUrl {string} this Space's container URL
|
|
876
|
+
* @param options.renewals {Array<object>} `{ zcap, sourceZcapId }` per
|
|
877
|
+
* renewed grant: the new capability and the id of the record it renews
|
|
878
|
+
* @param options.message {object} the envelope to queue, verbatim
|
|
879
|
+
* @param options.channelMaxLifetimeMs {number} how long after `receivedAt`
|
|
880
|
+
* an inbox record lapses at the latest
|
|
881
|
+
* @param [options.now] {Date}
|
|
882
|
+
* @returns {Promise<ConnectionWriteResult>}
|
|
883
|
+
*/
|
|
884
|
+
export async function recordRenewedGrants({ store, hmacKey, did, pairwiseDid, spaceUrl, renewals, message, channelMaxLifetimeMs, now = new Date() }) {
|
|
885
|
+
if (renewals.length === 0) {
|
|
886
|
+
throw new TypeError('A renewal write needs at least one renewed grant.');
|
|
887
|
+
}
|
|
888
|
+
const renewed = renewals.map(renewal => ({
|
|
889
|
+
zcap: checkedDelegatedZcap({ did, spaceUrl, zcap: renewal.zcap }),
|
|
890
|
+
sourceZcapId: renewal.sourceZcapId
|
|
891
|
+
}));
|
|
892
|
+
if (new Set(renewed.map(renewal => renewal.sourceZcapId)).size !==
|
|
893
|
+
renewed.length) {
|
|
894
|
+
throw new TypeError('Each renewed grant must renew a different record.');
|
|
895
|
+
}
|
|
896
|
+
assertEnvelopeCarries({
|
|
897
|
+
message,
|
|
898
|
+
zcapIds: renewed.map(renewal => renewal.zcap.id),
|
|
899
|
+
what: 'renewal'
|
|
900
|
+
});
|
|
901
|
+
const renewedScopes = new Set(renewed.map(renewal => grantScopeKey({ zcap: renewal.zcap })));
|
|
902
|
+
const stamp = now.toISOString();
|
|
903
|
+
return writePartyConnection({
|
|
904
|
+
store,
|
|
905
|
+
hmacKey,
|
|
906
|
+
did,
|
|
907
|
+
change(current) {
|
|
908
|
+
if (current === undefined) {
|
|
909
|
+
return { stop: 'absent' };
|
|
910
|
+
}
|
|
911
|
+
const { entry } = current;
|
|
912
|
+
if (entry.retired !== undefined) {
|
|
913
|
+
throw new Error('The connections entry is retired; its grants are not renewed.');
|
|
914
|
+
}
|
|
915
|
+
const channel = liveInboxChannel({
|
|
916
|
+
grants: receivedGrants({ entry, pairwiseDid, spaceUrl }),
|
|
917
|
+
now,
|
|
918
|
+
maxLifetimeMs: channelMaxLifetimeMs
|
|
919
|
+
});
|
|
920
|
+
if (channel === undefined) {
|
|
921
|
+
throw new Error('The connections entry holds no live inbox channel to deliver a ' +
|
|
922
|
+
'renewal through.');
|
|
923
|
+
}
|
|
924
|
+
const latest = latestPerScope({ grants: entry.grants });
|
|
925
|
+
const appended = [];
|
|
926
|
+
for (const { zcap, sourceZcapId } of renewed) {
|
|
927
|
+
const sourceIndex = entry.grants.findIndex(grant => grant.zcap.id === sourceZcapId);
|
|
928
|
+
const source = entry.grants[sourceIndex];
|
|
929
|
+
if (source === undefined) {
|
|
930
|
+
throw new Error(`The grant "${sourceZcapId}" a renewal renews is no longer on ` +
|
|
931
|
+
'the connections entry.');
|
|
932
|
+
}
|
|
933
|
+
const scope = grantScopeKey({ zcap: source.zcap });
|
|
934
|
+
if (grantScopeKey({ zcap }) !== scope) {
|
|
935
|
+
throw new TypeError(`A renewal of "${sourceZcapId}" must keep its controller, ` +
|
|
936
|
+
'invocation target, and allowed actions.');
|
|
937
|
+
}
|
|
938
|
+
if (latest.get(scope) !== sourceIndex) {
|
|
939
|
+
throw new Error(`The grant "${sourceZcapId}" is no longer the latest of its ` +
|
|
940
|
+
'scope; a concurrent renewal superseded it.');
|
|
941
|
+
}
|
|
942
|
+
if (expiresAtOf(zcap) <= expiresAtOf(source.zcap)) {
|
|
943
|
+
throw new Error(`A renewal of "${sourceZcapId}" must expire strictly later than ` +
|
|
944
|
+
'the record it renews.');
|
|
945
|
+
}
|
|
946
|
+
appended.push({
|
|
947
|
+
zcap,
|
|
948
|
+
grantKind: source.grantKind,
|
|
949
|
+
grantedAt: source.grantedAt,
|
|
950
|
+
renewedAt: stamp
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
body: {
|
|
955
|
+
...current.body,
|
|
956
|
+
grants: [...entry.grants, ...appended],
|
|
957
|
+
outbox: supersedeAndQueueEnvelope({
|
|
958
|
+
outbox: entry.outbox,
|
|
959
|
+
message,
|
|
960
|
+
stamp,
|
|
961
|
+
supersedes: zcap => renewedScopes.has(grantScopeKey({ zcap }))
|
|
962
|
+
})
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Removes the outbox item(s) queued at `createdAt`, once a push delivered
|
|
970
|
+
* them. None queued then writes nothing, and an absent entry writes nothing.
|
|
971
|
+
*
|
|
972
|
+
* @param options {object}
|
|
973
|
+
* @param options.store {ConnectionsStore}
|
|
974
|
+
* @param options.hmacKey {ConnectionIdKey}
|
|
975
|
+
* @param options.did {string} the party's DID
|
|
976
|
+
* @param options.createdAt {string} the delivered item's `createdAt`
|
|
977
|
+
* @returns {Promise<ConnectionWriteResult>}
|
|
978
|
+
*/
|
|
979
|
+
export async function settleOutboxItem({ store, hmacKey, did, createdAt }) {
|
|
980
|
+
return writePartyConnection({
|
|
981
|
+
store,
|
|
982
|
+
hmacKey,
|
|
983
|
+
did,
|
|
984
|
+
change(current) {
|
|
985
|
+
if (current === undefined) {
|
|
986
|
+
return { stop: 'absent' };
|
|
987
|
+
}
|
|
988
|
+
const kept = current.entry.outbox.filter(item => item.createdAt !== createdAt);
|
|
989
|
+
if (kept.length === current.entry.outbox.length) {
|
|
990
|
+
return { stop: 'unchanged' };
|
|
991
|
+
}
|
|
992
|
+
return { body: { ...current.body, outbox: kept } };
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Drops the grant records renewal superseded, and the outbox items no push
|
|
998
|
+
* can still deliver. In each scope holding more than one record, every
|
|
999
|
+
* record but the latest is dropped when its `expires` is past by more than
|
|
1000
|
+
* the revocation clock skew, or when its capability id is among
|
|
1001
|
+
* `deadChainZcapIds`. The latest record of a scope always stays. An outbox
|
|
1002
|
+
* item is discarded when every zcap its envelope carries is past `expires` by
|
|
1003
|
+
* more than the skew; an item carrying a zcap this build cannot read stays.
|
|
1004
|
+
* Nothing to drop writes nothing, and an absent
|
|
1005
|
+
* entry writes nothing.
|
|
1006
|
+
*
|
|
1007
|
+
* @param options {object}
|
|
1008
|
+
* @param options.store {ConnectionsStore}
|
|
1009
|
+
* @param options.hmacKey {ConnectionIdKey}
|
|
1010
|
+
* @param options.did {string} the party's DID
|
|
1011
|
+
* @param [options.deadChainZcapIds] {Iterable<string>} ids of superseded
|
|
1012
|
+
* records whose chain revocation reads as dead (the signer gone from the
|
|
1013
|
+
* account document, or the parent naming another annex generation) and
|
|
1014
|
+
* whose successor's outbox item was settled. A grant `agentGrantDue` reads
|
|
1015
|
+
* as due by replacement does not qualify on that alone: a grant under an
|
|
1016
|
+
* in-place-renewed delegation stays live until that delegation expires.
|
|
1017
|
+
* @param [options.now] {Date}
|
|
1018
|
+
* @returns {Promise<ConnectionWriteResult>}
|
|
1019
|
+
*/
|
|
1020
|
+
export async function pruneSupersededGrants({ store, hmacKey, did, deadChainZcapIds = [], now = new Date() }) {
|
|
1021
|
+
const deadChains = new Set(deadChainZcapIds);
|
|
1022
|
+
return writePartyConnection({
|
|
1023
|
+
store,
|
|
1024
|
+
hmacKey,
|
|
1025
|
+
did,
|
|
1026
|
+
change(current) {
|
|
1027
|
+
if (current === undefined) {
|
|
1028
|
+
return { stop: 'absent' };
|
|
1029
|
+
}
|
|
1030
|
+
const { entry } = current;
|
|
1031
|
+
const kept = new Set(latestPerScope({ grants: entry.grants }).values());
|
|
1032
|
+
const grants = entry.grants.filter((grant, index) => kept.has(index) ||
|
|
1033
|
+
!(expiredBeyondSkew({ zcap: grant.zcap, now }) ||
|
|
1034
|
+
deadChains.has(grant.zcap.id)));
|
|
1035
|
+
const outbox = entry.outbox.filter(item => {
|
|
1036
|
+
const zcaps = envelopeZcaps(item.message)?.map(parseConnectionZcap);
|
|
1037
|
+
return (zcaps === undefined ||
|
|
1038
|
+
zcaps.length === 0 ||
|
|
1039
|
+
!zcaps.every(zcap => zcap !== undefined && expiredBeyondSkew({ zcap, now })));
|
|
1040
|
+
});
|
|
1041
|
+
if (grants.length === entry.grants.length &&
|
|
1042
|
+
outbox.length === entry.outbox.length) {
|
|
1043
|
+
return { stop: 'unchanged' };
|
|
1044
|
+
}
|
|
1045
|
+
return { body: { ...current.body, grants, outbox } };
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Empties the party's `grantsReceived` and `outbox`: the agent revoke's
|
|
1051
|
+
* first write, so no client pushes to the party once it starts. Both already
|
|
1052
|
+
* empty writes nothing, and an absent entry writes nothing.
|
|
1053
|
+
*
|
|
1054
|
+
* @param options {object}
|
|
1055
|
+
* @param options.store {ConnectionsStore}
|
|
1056
|
+
* @param options.hmacKey {ConnectionIdKey}
|
|
1057
|
+
* @param options.did {string} the party's DID
|
|
1058
|
+
* @returns {Promise<ConnectionWriteResult>}
|
|
1059
|
+
*/
|
|
1060
|
+
export async function clearReceivedGrants({ store, hmacKey, did }) {
|
|
1061
|
+
return writePartyConnection({
|
|
1062
|
+
store,
|
|
1063
|
+
hmacKey,
|
|
1064
|
+
did,
|
|
1065
|
+
change(current) {
|
|
1066
|
+
if (current === undefined) {
|
|
1067
|
+
return { stop: 'absent' };
|
|
1068
|
+
}
|
|
1069
|
+
if (current.entry.grantsReceived.length === 0 &&
|
|
1070
|
+
current.entry.outbox.length === 0) {
|
|
1071
|
+
return { stop: 'unchanged' };
|
|
1072
|
+
}
|
|
1073
|
+
return { body: { ...current.body, grantsReceived: [], outbox: [] } };
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Records the owner's decline of the party's offer: sets `declined` to
|
|
1079
|
+
* `now`. An entry already declined keeps its time and writes nothing, and an
|
|
1080
|
+
* absent entry writes nothing. A later consent (`recordGrants`) clears it.
|
|
1081
|
+
*
|
|
1082
|
+
* @param options {object}
|
|
1083
|
+
* @param options.store {ConnectionsStore}
|
|
1084
|
+
* @param options.hmacKey {ConnectionIdKey}
|
|
1085
|
+
* @param options.did {string} the party's DID
|
|
1086
|
+
* @param [options.now] {Date}
|
|
1087
|
+
* @returns {Promise<ConnectionWriteResult>}
|
|
1088
|
+
*/
|
|
1089
|
+
export async function markDeclined({ store, hmacKey, did, now = new Date() }) {
|
|
1090
|
+
return writePartyConnection({
|
|
1091
|
+
store,
|
|
1092
|
+
hmacKey,
|
|
1093
|
+
did,
|
|
1094
|
+
change(current) {
|
|
1095
|
+
if (current === undefined) {
|
|
1096
|
+
return { stop: 'absent' };
|
|
1097
|
+
}
|
|
1098
|
+
if (current.entry.declined !== undefined) {
|
|
1099
|
+
return { stop: 'unchanged' };
|
|
1100
|
+
}
|
|
1101
|
+
return { body: { ...current.body, declined: now.toISOString() } };
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
518
1105
|
//# sourceMappingURL=upsert.js.map
|