keri-ts 2.2.1 → 2.2.2
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/esm/keri/npm/src/_dnt.polyfills.js +1 -127
- package/esm/keri/npm/src/acdc/dhking.js +6 -1
- package/esm/keri/npm/src/acdc/salting.js +18 -0
- package/esm/keri/npm/src/acdc/scheming.js +31 -18
- package/esm/keri/npm/src/acdc/unblinding.js +112 -6
- package/esm/keri/npm/src/app/delivery/forwarding.js +9 -0
- package/esm/keri/npm/src/app/delivery/respondant.js +21 -0
- package/esm/keri/npm/src/app/identity/habbing.js +4 -1
- package/esm/keri/npm/src/app/identity/keeping.js +38 -1
- package/esm/keri/npm/src/app/ipex/group-issuance.js +9 -6
- package/esm/keri/npm/src/app/ipex/ipex-building.js +2 -1
- package/esm/keri/npm/src/app/ipex/ipex-discloser.js +32 -8
- package/esm/keri/npm/src/app/ipex/issuance.js +131 -43
- package/esm/keri/npm/src/app/ipex/pending-presentations.js +50 -0
- package/esm/keri/npm/src/app/ipex/presenting.js +36 -1
- package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +140 -36
- package/esm/keri/npm/src/app/mailbox/mailbox-poller.js +37 -9
- package/esm/keri/npm/src/app/mailbox/mailboxing.js +9 -3
- package/esm/keri/npm/src/app/multisig/counseling.js +76 -3
- package/esm/keri/npm/src/app/multisig/event-forwarding.js +27 -0
- package/esm/keri/npm/src/app/multisig/grouping.js +120 -1
- package/esm/keri/npm/src/app/multisig/proposals.js +1 -1
- package/esm/keri/npm/src/app/runtime/agent-runtime.js +43 -23
- package/esm/keri/npm/src/app/runtime/ipex-auxiliaries.js +59 -25
- package/esm/keri/npm/src/app/runtime/operation-owners.js +1 -0
- package/esm/keri/npm/src/app/runtime/signer-conditions.js +51 -0
- package/esm/keri/npm/src/app/transport/cesr-egress.js +6 -0
- package/esm/keri/npm/src/core/errors.js +91 -3
- package/esm/keri/npm/src/core/json-schema-meta.js +688 -0
- package/esm/keri/npm/src/core/json-schema.js +147 -0
- package/esm/keri/npm/src/core/kel/eventing.js +20 -2
- package/esm/keri/npm/src/core/scheming.js +27 -26
- package/esm/keri/npm/src/db/replay/tel-replay.js +6 -0
- package/esm/keri/npm/src/db/store-lock.js +12 -0
- package/esm/keri/npm/src/runtime/index.js +8 -7
- package/esm/keri/npm/src/runtime/web/output-barrier.js +9 -0
- package/esm/keri/npm/src/version.js +2 -2
- package/package.json +2 -2
- package/types/_dnt.polyfills.d.ts +0 -99
- package/types/_dnt.polyfills.d.ts.map +1 -1
- package/types/acdc/dhking.d.ts.map +1 -1
- package/types/acdc/salting.d.ts +2 -0
- package/types/acdc/salting.d.ts.map +1 -1
- package/types/acdc/scheming.d.ts +2 -0
- package/types/acdc/scheming.d.ts.map +1 -1
- package/types/acdc/unblinding.d.ts +16 -5
- package/types/acdc/unblinding.d.ts.map +1 -1
- package/types/app/delivery/forwarding.d.ts +1 -0
- package/types/app/delivery/forwarding.d.ts.map +1 -1
- package/types/app/delivery/respondant.d.ts +7 -0
- package/types/app/delivery/respondant.d.ts.map +1 -1
- package/types/app/identity/habbing.d.ts +10 -1
- package/types/app/identity/habbing.d.ts.map +1 -1
- package/types/app/identity/keeping.d.ts +18 -0
- package/types/app/identity/keeping.d.ts.map +1 -1
- package/types/app/ipex/group-issuance.d.ts.map +1 -1
- package/types/app/ipex/ipex-building.d.ts.map +1 -1
- package/types/app/ipex/ipex-discloser.d.ts +31 -19
- package/types/app/ipex/ipex-discloser.d.ts.map +1 -1
- package/types/app/ipex/issuance.d.ts +41 -10
- package/types/app/ipex/issuance.d.ts.map +1 -1
- package/types/app/ipex/pending-presentations.d.ts +62 -1
- package/types/app/ipex/pending-presentations.d.ts.map +1 -1
- package/types/app/ipex/presenting.d.ts +39 -4
- package/types/app/ipex/presenting.d.ts.map +1 -1
- package/types/app/ipex/unblinding-workflow.d.ts +46 -12
- package/types/app/ipex/unblinding-workflow.d.ts.map +1 -1
- package/types/app/mailbox/mailbox-poller.d.ts +11 -2
- package/types/app/mailbox/mailbox-poller.d.ts.map +1 -1
- package/types/app/mailbox/mailboxing.d.ts +4 -1
- package/types/app/mailbox/mailboxing.d.ts.map +1 -1
- package/types/app/multisig/counseling.d.ts +22 -1
- package/types/app/multisig/counseling.d.ts.map +1 -1
- package/types/app/multisig/event-forwarding.d.ts +6 -0
- package/types/app/multisig/event-forwarding.d.ts.map +1 -0
- package/types/app/multisig/grouping.d.ts +26 -0
- package/types/app/multisig/grouping.d.ts.map +1 -1
- package/types/app/multisig/proposals.d.ts +3 -2
- package/types/app/multisig/proposals.d.ts.map +1 -1
- package/types/app/multisig/proposing.d.ts +8 -1
- package/types/app/multisig/proposing.d.ts.map +1 -1
- package/types/app/runtime/agent-runtime.d.ts +3 -0
- package/types/app/runtime/agent-runtime.d.ts.map +1 -1
- package/types/app/runtime/ipex-auxiliaries.d.ts +8 -3
- package/types/app/runtime/ipex-auxiliaries.d.ts.map +1 -1
- package/types/app/runtime/operation-owners.d.ts.map +1 -1
- package/types/app/runtime/signer-conditions.d.ts +32 -0
- package/types/app/runtime/signer-conditions.d.ts.map +1 -0
- package/types/app/transport/cesr-egress.d.ts +1 -0
- package/types/app/transport/cesr-egress.d.ts.map +1 -1
- package/types/core/errors.d.ts +20 -1
- package/types/core/errors.d.ts.map +1 -1
- package/types/core/json-schema-meta.d.ts +19 -0
- package/types/core/json-schema-meta.d.ts.map +1 -0
- package/types/core/json-schema.d.ts +29 -0
- package/types/core/json-schema.d.ts.map +1 -0
- package/types/core/scheming.d.ts +1 -0
- package/types/core/scheming.d.ts.map +1 -1
- package/types/db/replay/tel-replay.d.ts +1 -0
- package/types/db/replay/tel-replay.d.ts.map +1 -1
- package/types/db/store-lock.d.ts +2 -0
- package/types/db/store-lock.d.ts.map +1 -1
- package/types/runtime/index.d.ts +10 -9
- package/types/runtime/index.d.ts.map +1 -1
- package/types/runtime/web/output-barrier.d.ts +1 -0
- package/types/runtime/web/output-barrier.d.ts.map +1 -1
- package/types/version.d.ts +2 -2
|
@@ -184,7 +184,7 @@ export function* preparePresentation(deps, input) {
|
|
|
184
184
|
break;
|
|
185
185
|
case "agree":
|
|
186
186
|
case "spurn":
|
|
187
|
-
prepared = yield* discloser.prepareReply(sender, input.verb, input.
|
|
187
|
+
prepared = yield* discloser.prepareReply(sender, input.verb, input.priorSaid, input.message ?? "", input.recipient, input.auths);
|
|
188
188
|
break;
|
|
189
189
|
case "offer":
|
|
190
190
|
prepared = yield* discloser.prepareOffer(sender, input.selection, input.options);
|
|
@@ -259,6 +259,41 @@ export function* proposeGroupPresentation(deps, groupIpex, { hab, selection, opt
|
|
|
259
259
|
},
|
|
260
260
|
});
|
|
261
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* A group's consent to send an Apply, Offer, Agree or Spurn: freezes the unsigned body (the same body a single
|
|
264
|
+
* identifier would sign) and proposes it as a group-authored message with {@link proposeGroupIpex}. The other members
|
|
265
|
+
* list and approve it with `approveGroupIpex` and the elected lead delivers the completed message. A Grant has its own
|
|
266
|
+
* call, {@link proposeGroupPresentation}; an Admit has `proposeGroupAdmit`.
|
|
267
|
+
*
|
|
268
|
+
* @throws ValidationError for an identifier that is not a local group, a selection or predecessor the discloser
|
|
269
|
+
* refuses, an exchange that requires anchoring (a group message carries member contributions, not one source seal),
|
|
270
|
+
* or a message awaiting authenticating dependencies.
|
|
271
|
+
*/
|
|
272
|
+
export function* proposeGroupNegotiation(deps, groupIpex, input) {
|
|
273
|
+
const { hab, deliver } = input;
|
|
274
|
+
const sender = hab.pre;
|
|
275
|
+
let exn, nests, gvrsn;
|
|
276
|
+
switch (input.verb) {
|
|
277
|
+
case "apply":
|
|
278
|
+
gvrsn = input.options.gvrsn;
|
|
279
|
+
exn = discloserFor(deps, hab).applyBody(sender, input.options);
|
|
280
|
+
break;
|
|
281
|
+
case "offer": {
|
|
282
|
+
gvrsn = input.options.gvrsn;
|
|
283
|
+
const offer = yield* discloserFor(deps, hab, input.selection.derive).prepareOfferBody(sender, input.selection, input.options);
|
|
284
|
+
({ exn, nests } = offer);
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case "agree":
|
|
288
|
+
case "spurn":
|
|
289
|
+
exn = yield* discloserFor(deps, hab).replyBody(sender, input.verb, input.priorSaid, input.message ?? "", input.recipient);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
if (exn.sad.a.ax?.[0] === true) {
|
|
293
|
+
throw new ValidationError("A group cannot send an exchange that requires anchoring.");
|
|
294
|
+
}
|
|
295
|
+
return yield* proposeGroupIpex(groupIpex, { hab, exn, nests, gvrsn: gvrsn ?? undefined, deliver });
|
|
296
|
+
}
|
|
262
297
|
/**
|
|
263
298
|
* Deliver a group's completed prepared Grant ({@link resumePresentationFile}): queues the delivery bytes for
|
|
264
299
|
* `processOnce` when the local member is the elected lead, with the retry and deadline of any presentation. Signs
|
|
@@ -34,11 +34,16 @@ import { singleKeyQueryWindow } from "../../core/mailbox/kram-policy.js";
|
|
|
34
34
|
import { timestampMicros } from "../../time/mod.js";
|
|
35
35
|
import { groupSigningMembers, isLocalGroupHab, localGroupMember } from "../multisig/group-members.js";
|
|
36
36
|
import { MULTISIG_QRY_ROUTE, MULTISIG_RPY_ROUTE, multisigExnBody } from "../multisig/grouping.js";
|
|
37
|
+
import { remoteGroupMembers } from "../multisig/multisig-messaging.js";
|
|
37
38
|
import { envelopeFromFrame } from "../protocol/parsering.js";
|
|
38
39
|
import { DeliveryOutbox } from "../runtime/delivery-outbox.js";
|
|
39
40
|
import { observeUntil, waitOnClock } from "../runtime/workflow-contract.js";
|
|
40
41
|
/** Milliseconds between resends of the unchanged disclosee query, as `tufa ipex unblind` does. */
|
|
41
42
|
export const UNBLIND_QUERY_RESEND_MS = 1000;
|
|
43
|
+
/** The key of one party's operation on a Grant. */
|
|
44
|
+
export function unblindingKey(grant, role) {
|
|
45
|
+
return `${role}:${grant}`;
|
|
46
|
+
}
|
|
42
47
|
/** Per-operation state of a group party; memory only. */
|
|
43
48
|
class GroupUnblind {
|
|
44
49
|
constructor(member, proposer) {
|
|
@@ -94,6 +99,8 @@ class GroupUnblind {
|
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
const EXPIRED = "The presentation reached its original exchange deadline";
|
|
102
|
+
const NOT_ANSWERED = "No answer is begun for this presentation here; a group discloser's members begin it with answerUnblind";
|
|
103
|
+
const UNNEEDED = "The presentation was already answered with its opening delivered, so this answer was not needed";
|
|
97
104
|
const CHANGED = "Original presentation deadline changed";
|
|
98
105
|
function message(error) {
|
|
99
106
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -113,14 +120,14 @@ export class UnblindingOperations {
|
|
|
113
120
|
writable: true,
|
|
114
121
|
value: deps
|
|
115
122
|
});
|
|
116
|
-
/** Operations whose approval is live in this process.
|
|
123
|
+
/** Operations whose approval is live in this process, by {@link unblindingKey}. Read them through {@link liveOf}. */
|
|
117
124
|
Object.defineProperty(this, "live", {
|
|
118
125
|
enumerable: true,
|
|
119
126
|
configurable: true,
|
|
120
127
|
writable: true,
|
|
121
128
|
value: new Map()
|
|
122
129
|
});
|
|
123
|
-
/** Operations that finished or ended in this process; not durable, see the module doc. */
|
|
130
|
+
/** Operations that finished or ended in this process, by {@link unblindingKey}; not durable, see the module doc. */
|
|
124
131
|
Object.defineProperty(this, "settled", {
|
|
125
132
|
enumerable: true,
|
|
126
133
|
configurable: true,
|
|
@@ -134,10 +141,48 @@ export class UnblindingOperations {
|
|
|
134
141
|
writable: true,
|
|
135
142
|
value: null
|
|
136
143
|
});
|
|
144
|
+
/**
|
|
145
|
+
* The delivery the outboxes use. A recipient that is an identifier of this Habery takes the bytes through the
|
|
146
|
+
* ordinary reactor path in process, so the checks a remote delivery meets run too; a local group's members that
|
|
147
|
+
* are not held here are still delivered to.
|
|
148
|
+
*/
|
|
149
|
+
Object.defineProperty(this, "transport", {
|
|
150
|
+
enumerable: true,
|
|
151
|
+
configurable: true,
|
|
152
|
+
writable: true,
|
|
153
|
+
value: { deliver: (message) => this.deliverToParty(message) }
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/** The live operation of one party of `grant`; without `role`, the requester's when this Habery holds both. */
|
|
157
|
+
liveOf(grant, role) {
|
|
158
|
+
return role
|
|
159
|
+
? this.live.get(unblindingKey(grant, role))
|
|
160
|
+
: this.live.get(unblindingKey(grant, "requester")) ?? this.live.get(unblindingKey(grant, "responder"));
|
|
137
161
|
}
|
|
138
|
-
/**
|
|
139
|
-
|
|
140
|
-
return
|
|
162
|
+
/** The operation of one party of `grant` that finished or ended in this process; without `role`, as {@link liveOf}. */
|
|
163
|
+
settledOf(grant, role) {
|
|
164
|
+
return role
|
|
165
|
+
? this.settled.get(unblindingKey(grant, role))
|
|
166
|
+
: this.settled.get(unblindingKey(grant, "requester")) ?? this.settled.get(unblindingKey(grant, "responder"));
|
|
167
|
+
}
|
|
168
|
+
/** Every live operation, for the module functions that look across parties. */
|
|
169
|
+
liveOps() {
|
|
170
|
+
return [...this.live.values()];
|
|
171
|
+
}
|
|
172
|
+
/** Record a consented operation as live, replacing what an earlier one of the same party left behind. */
|
|
173
|
+
begin(op) {
|
|
174
|
+
this.settled.delete(unblindingKey(op.grant, op.role));
|
|
175
|
+
this.live.set(unblindingKey(op.grant, op.role), op);
|
|
176
|
+
}
|
|
177
|
+
/** Remember how a party's operation on `grant` finished or ended without a live operation to end. */
|
|
178
|
+
remember(grant, settled) {
|
|
179
|
+
this.settled.set(unblindingKey(grant, settled.role), settled);
|
|
180
|
+
}
|
|
181
|
+
/** Whether a live operation for `grant` (or only its party `role`) still holds a message the transport has not taken. */
|
|
182
|
+
owes(grant, role) {
|
|
183
|
+
return role
|
|
184
|
+
? (this.liveOf(grant, role)?.outbox.length ?? 0) > 0
|
|
185
|
+
: this.owes(grant, "requester") || this.owes(grant, "responder");
|
|
141
186
|
}
|
|
142
187
|
/**
|
|
143
188
|
* Whether a pass has work: a live operation, or an owner holds an authenticated peer query whose reply is
|
|
@@ -156,19 +201,23 @@ export class UnblindingOperations {
|
|
|
156
201
|
const { deps } = this;
|
|
157
202
|
this.adopt();
|
|
158
203
|
for (const op of [...this.live.values()]) {
|
|
159
|
-
if (op.busy ||
|
|
204
|
+
if (op.busy || this.live.get(unblindingKey(op.grant, op.role)) !== op)
|
|
160
205
|
continue;
|
|
161
206
|
op.busy = true;
|
|
162
207
|
try {
|
|
163
208
|
const left = op.expires - BigInt(deps.clock.now()) * 1000n;
|
|
164
209
|
if (left <= 0n)
|
|
165
|
-
this.end(op, "expired",
|
|
210
|
+
this.end(op, "expired", this.expiry(op));
|
|
166
211
|
else if ((yield* race([this.advance(op), this.timer(Number(left) / 1000)])) === "expired") {
|
|
167
|
-
this.end(op, "expired",
|
|
212
|
+
this.end(op, "expired", this.expiry(op));
|
|
168
213
|
}
|
|
169
214
|
}
|
|
170
215
|
catch (error) {
|
|
171
|
-
|
|
216
|
+
// A discloser whose presentation closed before it was asked had nothing left to answer; that is not a failure.
|
|
217
|
+
if (op.role === "responder" && deps.hby.db.erpy.get([op.grant]))
|
|
218
|
+
this.end(op, "unneeded", UNNEEDED);
|
|
219
|
+
else
|
|
220
|
+
this.end(op, "failed", message(error));
|
|
172
221
|
}
|
|
173
222
|
finally {
|
|
174
223
|
op.busy = false;
|
|
@@ -185,7 +234,7 @@ export class UnblindingOperations {
|
|
|
185
234
|
const { deps } = this;
|
|
186
235
|
for (const owner of deps.unblinds.unblindOwners()) {
|
|
187
236
|
for (const { xid, grant } of owner.awaitingReply()) {
|
|
188
|
-
if (this.live.has(grant))
|
|
237
|
+
if (this.live.has(unblindingKey(grant, "responder")))
|
|
189
238
|
continue;
|
|
190
239
|
let input, deadline, expires;
|
|
191
240
|
try {
|
|
@@ -196,14 +245,18 @@ export class UnblindingOperations {
|
|
|
196
245
|
catch (error) {
|
|
197
246
|
// Unreadable stored state fails this reply alone; release the owner's approval so it is not retried.
|
|
198
247
|
owner.abandon(xid);
|
|
199
|
-
this.settled.set(grant,
|
|
248
|
+
this.settled.set(unblindingKey(grant, "responder"), {
|
|
249
|
+
role: "responder",
|
|
250
|
+
state: "failed",
|
|
251
|
+
reason: message(error),
|
|
252
|
+
});
|
|
200
253
|
deps.changes?.push({ kind: "unblinding", id: grant });
|
|
201
254
|
continue;
|
|
202
255
|
}
|
|
203
256
|
if (!input || !deadline || expires === null || BigInt(deps.clock.now()) * 1000n >= expires)
|
|
204
257
|
continue;
|
|
205
|
-
this.settled.delete(grant);
|
|
206
|
-
this.live.set(grant, {
|
|
258
|
+
this.settled.delete(unblindingKey(grant, "responder"));
|
|
259
|
+
this.live.set(unblindingKey(grant, "responder"), {
|
|
207
260
|
grant,
|
|
208
261
|
xid,
|
|
209
262
|
role: "responder",
|
|
@@ -223,15 +276,20 @@ export class UnblindingOperations {
|
|
|
223
276
|
}
|
|
224
277
|
}
|
|
225
278
|
}
|
|
279
|
+
/** The reason an operation reached its deadline; a query still waiting for the issuer's anchoring event says so. */
|
|
280
|
+
expiry(op) {
|
|
281
|
+
const missing = op.role === "responder" ? op.owner.missingSource(op.xid) : null;
|
|
282
|
+
return missing ? `${EXPIRED}; ${missing}` : EXPIRED;
|
|
283
|
+
}
|
|
226
284
|
/** End a live operation: release the approval, remember why until the process ends, tell the sink. */
|
|
227
285
|
end(op, state, reason) {
|
|
228
286
|
this.release(op);
|
|
229
|
-
this.settled.set(op.grant, { role: op.role, state, reason });
|
|
287
|
+
this.settled.set(unblindingKey(op.grant, op.role), { role: op.role, state, reason });
|
|
230
288
|
this.deps.changes?.push({ kind: "unblinding", id: op.grant });
|
|
231
289
|
}
|
|
232
290
|
/** Drop the live entry and the owner's approval and frozen messages; delivered bytes stay the peer's. */
|
|
233
291
|
release(op) {
|
|
234
|
-
this.live.delete(op.grant);
|
|
292
|
+
this.live.delete(unblindingKey(op.grant, op.role));
|
|
235
293
|
try {
|
|
236
294
|
op.owner.abandon(op.xid);
|
|
237
295
|
}
|
|
@@ -242,7 +300,7 @@ export class UnblindingOperations {
|
|
|
242
300
|
}
|
|
243
301
|
finished(op) {
|
|
244
302
|
this.release(op);
|
|
245
|
-
this.settled.set(op.grant, { role: op.role, state: "done" });
|
|
303
|
+
this.settled.set(unblindingKey(op.grant, op.role), { role: op.role, state: "done" });
|
|
246
304
|
this.deps.changes?.push({ kind: "unblinding", id: op.grant });
|
|
247
305
|
}
|
|
248
306
|
*timer(milliseconds) {
|
|
@@ -413,9 +471,23 @@ export class UnblindingOperations {
|
|
|
413
471
|
op.finished = true;
|
|
414
472
|
return "stepped";
|
|
415
473
|
}
|
|
474
|
+
*deliverToParty(message) {
|
|
475
|
+
const { hby, reactor, delivery } = this.deps, recipient = hby.habs.get(message.dest);
|
|
476
|
+
if (!recipient || !reactor)
|
|
477
|
+
return yield* delivery.deliver(message);
|
|
478
|
+
yield* reactor.processBytes(message.msg);
|
|
479
|
+
const reached = [`local:${recipient.pre}`];
|
|
480
|
+
if (!isLocalGroupHab(hby, recipient))
|
|
481
|
+
return reached;
|
|
482
|
+
const member = localGroupMember(hby, recipient.pre);
|
|
483
|
+
for (const dest of remoteGroupMembers(hby, member, groupSigningMembers(hby, recipient.pre))) {
|
|
484
|
+
reached.push(...yield* delivery.deliver({ ...message, dest }));
|
|
485
|
+
}
|
|
486
|
+
return reached;
|
|
487
|
+
}
|
|
416
488
|
/** One pass: send what is queued, step only once the queue is empty, send what the step queued. */
|
|
417
489
|
*advance(op) {
|
|
418
|
-
const drain = () => op.outbox.drain(this.
|
|
490
|
+
const drain = () => op.outbox.drain(this.transport, this.deps.clock);
|
|
419
491
|
if (!(yield* drain()))
|
|
420
492
|
return "stepped";
|
|
421
493
|
if (!op.finished)
|
|
@@ -428,7 +500,7 @@ export class UnblindingOperations {
|
|
|
428
500
|
/** The exact live Grant the local party may consent to, with its deadline and role. */
|
|
429
501
|
function selectGrant(ops, grant, role, proposer) {
|
|
430
502
|
const { deps } = ops;
|
|
431
|
-
if (ops.
|
|
503
|
+
if (ops.liveOf(grant, role))
|
|
432
504
|
throw new ValidationError("Unblind was already begun for this Grant");
|
|
433
505
|
const input = deps.exc.retainedInput(grant);
|
|
434
506
|
if (!input)
|
|
@@ -460,6 +532,14 @@ function consent(ops, grant, role, options) {
|
|
|
460
532
|
if (role === "responder" && !isLocalGroupHab(deps.hby, hab)) {
|
|
461
533
|
throw new ValidationError("An individual discloser answers an authenticated query without consent");
|
|
462
534
|
}
|
|
535
|
+
if (role === "responder" && deps.hby.db.erpy.get([grant])) {
|
|
536
|
+
// The verifier already answered the presentation, so no query will come: nothing to approve or answer.
|
|
537
|
+
const closed = selected.input.serder;
|
|
538
|
+
const xid = closed.ilk === "xip" ? closed.said : String(closed.sad.x);
|
|
539
|
+
ops.remember(grant, { role: "responder", state: "unneeded", reason: UNNEEDED });
|
|
540
|
+
deps.changes?.push({ kind: "unblinding", id: grant });
|
|
541
|
+
return { op: null, begun: { grant, xid, role } };
|
|
542
|
+
}
|
|
463
543
|
const owner = deps.unblinds.unblind(hab), xid = owner.approve(grant, { states: options.states });
|
|
464
544
|
const begun = { grant, xid, role };
|
|
465
545
|
// A current authenticated view needs no empty auxiliary query or new proof writes.
|
|
@@ -467,7 +547,7 @@ function consent(ops, grant, role, options) {
|
|
|
467
547
|
const current = deps.exc.verifyGrantProofs(grant, selected.input.nests ?? []);
|
|
468
548
|
if (current === true || typeof current === "object" && current.kind === "accept") {
|
|
469
549
|
owner.abandon(xid);
|
|
470
|
-
ops.
|
|
550
|
+
ops.remember(grant, { role: "requester", state: "done" });
|
|
471
551
|
deps.changes?.push({ kind: "unblinding", id: grant });
|
|
472
552
|
return { op: null, begun };
|
|
473
553
|
}
|
|
@@ -512,8 +592,7 @@ export function requestUnblind(ops, grant, options = {}) {
|
|
|
512
592
|
}
|
|
513
593
|
else
|
|
514
594
|
op.query = op.owner.start(grant);
|
|
515
|
-
ops.
|
|
516
|
-
ops.live.set(grant, op);
|
|
595
|
+
ops.begin(op);
|
|
517
596
|
ops.deps.changes?.push({ kind: "unblinding", id: grant });
|
|
518
597
|
}
|
|
519
598
|
return begun;
|
|
@@ -530,9 +609,10 @@ export function requestUnblind(ops, grant, options = {}) {
|
|
|
530
609
|
export function answerUnblind(ops, grant, options = {}) {
|
|
531
610
|
const { deps } = ops;
|
|
532
611
|
const { op, begun } = consent(ops, grant, "responder", options);
|
|
612
|
+
if (!op)
|
|
613
|
+
return begun;
|
|
533
614
|
registerUnblindRoutes(ops);
|
|
534
|
-
ops.
|
|
535
|
-
ops.live.set(grant, op);
|
|
615
|
+
ops.begin(op);
|
|
536
616
|
deps.changes?.push({ kind: "unblinding", id: grant });
|
|
537
617
|
return begun;
|
|
538
618
|
}
|
|
@@ -582,7 +662,7 @@ function registerUnblindRoutes(ops) {
|
|
|
582
662
|
acceptsNests: true,
|
|
583
663
|
verify: (args) => structural(deps.hby, args.serder, args.nests ?? []) && !args.attachments.length && !args.essrs?.length,
|
|
584
664
|
handle: (args) => {
|
|
585
|
-
for (const op of ops.
|
|
665
|
+
for (const op of ops.liveOps()) {
|
|
586
666
|
if (op.group && groupAccepts(deps.hby, op, args.serder, args.nests)) {
|
|
587
667
|
op.group.pending.set(args.serder.said, { parent: args.serder, child: args.nests[0] });
|
|
588
668
|
}
|
|
@@ -601,8 +681,9 @@ function registerUnblindRoutes(ops) {
|
|
|
601
681
|
* @throws ValidationError when `grant` is not a Grant a local identifier is the disclosee of, and no
|
|
602
682
|
* operation for it is live or remembered.
|
|
603
683
|
*/
|
|
604
|
-
export function unblindingStatus(ops, grant) {
|
|
605
|
-
const { deps } = ops,
|
|
684
|
+
export function unblindingStatus(ops, grant, party = {}) {
|
|
685
|
+
const { deps } = ops, role = chooseParty(deps, grant, party);
|
|
686
|
+
const settled = ops.settledOf(grant, role), live = ops.liveOf(grant, role);
|
|
606
687
|
if (settled) {
|
|
607
688
|
return settled.state === "done"
|
|
608
689
|
? { state: "done", role: settled.role }
|
|
@@ -618,6 +699,13 @@ export function unblindingStatus(ops, grant) {
|
|
|
618
699
|
};
|
|
619
700
|
}
|
|
620
701
|
const input = deps.exc.retainedInput(grant), recipient = input?.serder.sad?.ri;
|
|
702
|
+
if (role === "responder" && input && input.serder.pre && deps.hby.habs.has(input.serder.pre)) {
|
|
703
|
+
// A discloser that has not begun (a group member before its answer) reads as not begun, not as an error.
|
|
704
|
+
const lifetime = deps.exc.exchangeDeadline(input.serder);
|
|
705
|
+
return lifetime && lapsed(deps.clock, lifetime)
|
|
706
|
+
? { state: "expired", role: "responder", reason: EXPIRED }
|
|
707
|
+
: { state: "interrupted", role: "responder", reason: NOT_ANSWERED };
|
|
708
|
+
}
|
|
621
709
|
if (!input || typeof recipient !== "string" || !deps.hby.habs.has(recipient)) {
|
|
622
710
|
throw new ValidationError("No Unblind was begun for this Grant");
|
|
623
711
|
}
|
|
@@ -634,16 +722,32 @@ export function unblindingStatus(ops, grant) {
|
|
|
634
722
|
return { state: "expired", role: "requester", reason: EXPIRED };
|
|
635
723
|
return { state: "interrupted", role: "requester", reason: INTERRUPTED };
|
|
636
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* The party a read is for: the one asked for, else the Habery's only party, else (it holds both) the requester.
|
|
727
|
+
* Undefined when the Grant is not stored here, so only what is live or remembered answers.
|
|
728
|
+
*
|
|
729
|
+
* @throws ValidationError when `choice` names a party this Habery does not hold.
|
|
730
|
+
*/
|
|
731
|
+
function chooseParty(deps, grant, choice) {
|
|
732
|
+
const input = deps.exc.retainedInput(grant);
|
|
733
|
+
if (!input)
|
|
734
|
+
return choice.role;
|
|
735
|
+
const held = [
|
|
736
|
+
...(deps.hby.habs.has(String(input.serder.sad?.ri)) ? ["requester"] : []),
|
|
737
|
+
...(input.serder.pre && deps.hby.habs.has(input.serder.pre) ? ["responder"] : []),
|
|
738
|
+
];
|
|
739
|
+
if (choice.role && !held.includes(choice.role)) {
|
|
740
|
+
throw new ValidationError(`This Habery holds no ${choice.role} of this presentation`);
|
|
741
|
+
}
|
|
742
|
+
return choice.role ?? held[0];
|
|
743
|
+
}
|
|
637
744
|
/**
|
|
638
745
|
* Observe until the Unblind is done or ended, or the budget expires. Starts no worker and drives nothing:
|
|
639
746
|
* someone else must call `processOnce`.
|
|
640
747
|
*/
|
|
641
|
-
export function* waitForUnblinding(ops, grant, { timeoutMs, clock = ops.deps.clock }) {
|
|
642
|
-
const settled = yield* observeUntil(() => unblindingStatus(ops, grant).state !== "active" && !ops.owes(grant), {
|
|
643
|
-
|
|
644
|
-
clock,
|
|
645
|
-
});
|
|
646
|
-
const status = unblindingStatus(ops, grant);
|
|
748
|
+
export function* waitForUnblinding(ops, grant, { timeoutMs, clock = ops.deps.clock, role }) {
|
|
749
|
+
const settled = yield* observeUntil(() => unblindingStatus(ops, grant, { role }).state !== "active" && !ops.owes(grant, role), { timeoutMs, clock });
|
|
750
|
+
const status = unblindingStatus(ops, grant, { role });
|
|
647
751
|
return settled ? { outcome: "settled", status } : { outcome: "timed-out", status, timeoutMs };
|
|
648
752
|
}
|
|
649
753
|
/**
|
|
@@ -653,15 +757,15 @@ export function* waitForUnblinding(ops, grant, { timeoutMs, clock = ops.deps.clo
|
|
|
653
757
|
*
|
|
654
758
|
* @throws ValidationError when no Unblind was begun for `grant`.
|
|
655
759
|
*/
|
|
656
|
-
export function abandonUnblinding(ops, grant, reason = "Abandoned by the user") {
|
|
657
|
-
const status = unblindingStatus(ops, grant);
|
|
760
|
+
export function abandonUnblinding(ops, grant, reason = "Abandoned by the user", party = {}) {
|
|
761
|
+
const status = unblindingStatus(ops, grant, party);
|
|
658
762
|
if (status.state !== "active" && status.state !== "interrupted")
|
|
659
763
|
return;
|
|
660
|
-
const live = ops.
|
|
764
|
+
const live = ops.liveOf(grant, status.role);
|
|
661
765
|
if (live)
|
|
662
766
|
ops.end(live, "abandoned", reason);
|
|
663
767
|
else {
|
|
664
|
-
ops.
|
|
768
|
+
ops.remember(grant, { role: status.role, state: "abandoned", reason });
|
|
665
769
|
ops.deps.changes?.push({ kind: "unblinding", id: grant });
|
|
666
770
|
}
|
|
667
771
|
}
|
|
@@ -6,6 +6,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
6
6
|
var _MailboxPoller_dirty;
|
|
7
7
|
/** Recipient mailbox polling: signed queries, awaited SSE intake and persisted cursor progression. */
|
|
8
8
|
import { call, scoped, spawn } from "effection";
|
|
9
|
+
import { SignerNotHeldError } from "../identity/keeping.js";
|
|
9
10
|
import { abortable } from "../runtime/abortable.js";
|
|
10
11
|
import { doEvery, PROTOCOL_TOCK_MS } from "../runtime/doing.js";
|
|
11
12
|
import { runtimeTurn } from "../runtime/runtime-turn.js";
|
|
@@ -89,6 +90,13 @@ export class MailboxPoller {
|
|
|
89
90
|
writable: true,
|
|
90
91
|
value: void 0
|
|
91
92
|
});
|
|
93
|
+
/** Where unsignable polls are reported, when the host keeps such a record. */
|
|
94
|
+
Object.defineProperty(this, "signerConditions", {
|
|
95
|
+
enumerable: true,
|
|
96
|
+
configurable: true,
|
|
97
|
+
writable: true,
|
|
98
|
+
value: void 0
|
|
99
|
+
});
|
|
92
100
|
/** Told when a mailbox answers 403. */
|
|
93
101
|
Object.defineProperty(this, "onUnauthorized", {
|
|
94
102
|
enumerable: true,
|
|
@@ -98,7 +106,8 @@ export class MailboxPoller {
|
|
|
98
106
|
});
|
|
99
107
|
/** Identifiers whose mailbox endpoints changed; the poller re-selects their workers on the next tock. */
|
|
100
108
|
_MailboxPoller_dirty.set(this, new Set());
|
|
101
|
-
const { timeouts, onPolled, onUnauthorized } = options;
|
|
109
|
+
const { timeouts, onPolled, onUnauthorized, signerConditions } = options;
|
|
110
|
+
this.signerConditions = signerConditions;
|
|
102
111
|
this.onPolled = onPolled;
|
|
103
112
|
this.onUnauthorized = onUnauthorized;
|
|
104
113
|
this.hby = hby;
|
|
@@ -167,17 +176,26 @@ export class MailboxPoller {
|
|
|
167
176
|
* @param target Mailbox subject habitat plus the actual local signer (a member for groups).
|
|
168
177
|
* @param endpoint Selected remote endpoint AID and HTTP(S) URL.
|
|
169
178
|
* @param onBatch Awaited consumer; successful completion precedes durable cursor advancement.
|
|
170
|
-
* @returns How the poll ended; see {@link PollOutcome}.
|
|
179
|
+
* @returns How the poll ended; see {@link PollOutcome}. An `unsignable` poll carries the missing public key.
|
|
171
180
|
*/
|
|
172
181
|
*pollRemoteEndpointOnce(target, endpoint, onBatch) {
|
|
173
182
|
const cursor = this.mailboxDirector.remoteQueryCursor(target.targetPre, endpoint.eid);
|
|
174
183
|
// deno-lint-ignore no-this-alias -- Generator callbacks have no lexical this.
|
|
175
184
|
const self = this;
|
|
176
|
-
// The concrete HTTP/SSE operation owns and joins this whole poll attempt.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
185
|
+
// The concrete HTTP/SSE operation owns and joins this whole poll attempt. A signer the host released
|
|
186
|
+
// (while signing the query, or in the consumer's protocol work) ends the poll as `unsignable`.
|
|
187
|
+
try {
|
|
188
|
+
const outcome = yield* call(() => fetchMailboxMessages(target.signerHab, target.targetPre, endpoint.eid, endpoint.url, cursor, mailboxFetchTimeoutPolicy(this.timeoutPolicy), this.http, this.clock, function* deliverThenAdvanceCursor(message) {
|
|
189
|
+
yield* call(() => onBatch({ pre: target.targetPre, eid: endpoint.eid, messages: [message.msg] }));
|
|
190
|
+
self.mailboxDirector.updateRemoteCursor(target.targetPre, endpoint.eid, message.topic, message.idx);
|
|
191
|
+
}));
|
|
192
|
+
return { outcome };
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (error instanceof SignerNotHeldError)
|
|
196
|
+
return { outcome: "unsignable", missing: error.pub };
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
181
199
|
}
|
|
182
200
|
/**
|
|
183
201
|
* Keep one long-lived worker running for each currently configured remote endpoint.
|
|
@@ -225,7 +243,12 @@ export class MailboxPoller {
|
|
|
225
243
|
*remoteEndpointWorker(target, endpoint, onBatch) {
|
|
226
244
|
let state = { backoffMs: 0 };
|
|
227
245
|
while (true) {
|
|
228
|
-
const outcome = yield* this.pollRemoteEndpointOnce(target, endpoint, onBatch);
|
|
246
|
+
const { outcome, missing } = yield* this.pollRemoteEndpointOnce(target, endpoint, onBatch);
|
|
247
|
+
const source = `mailbox-poller:${target.targetPre}/${endpoint.eid}`;
|
|
248
|
+
if (missing)
|
|
249
|
+
this.signerConditions?.report(source, missing, this.clock.now());
|
|
250
|
+
else
|
|
251
|
+
this.signerConditions?.clear(source);
|
|
229
252
|
const step = nextPoll(state, outcome, {
|
|
230
253
|
initialMs: this.timeoutPolicy.refusalInitialMs,
|
|
231
254
|
maxMs: this.timeoutPolicy.refusalMaxMs,
|
|
@@ -241,7 +264,11 @@ export class MailboxPoller {
|
|
|
241
264
|
yield* waitOnClock(this.clock, step.action.ms);
|
|
242
265
|
break;
|
|
243
266
|
case "wait":
|
|
244
|
-
|
|
267
|
+
// A released signer is waited out until one is held again, not for the full backoff.
|
|
268
|
+
if (missing && this.signerConditions)
|
|
269
|
+
yield* this.signerConditions.waitForHold(this.clock, step.action.ms);
|
|
270
|
+
else
|
|
271
|
+
yield* waitOnClock(this.clock, step.action.ms);
|
|
245
272
|
break;
|
|
246
273
|
}
|
|
247
274
|
}
|
|
@@ -347,6 +374,7 @@ export function nextPoll(state, outcome, backoff = MailboxPoller.RefusalBackoff)
|
|
|
347
374
|
}
|
|
348
375
|
case "no-response":
|
|
349
376
|
case "unreachable":
|
|
377
|
+
case "unsignable":
|
|
350
378
|
case "refused":
|
|
351
379
|
return backOff(state, backoff);
|
|
352
380
|
}
|
|
@@ -264,9 +264,12 @@ export function hostedEndpointPathMatches(hby, pathname, eids) {
|
|
|
264
264
|
* @param pathname Incoming URL pathname to compare against accepted endpoint base paths.
|
|
265
265
|
* @param resourceSuffix Protocol resource suffix sought beneath the hosted base path.
|
|
266
266
|
* @param eids Optional explicit hosted-AID filter; do not infer host eligibility from all local identities.
|
|
267
|
+
* @param destination The request's `CESR-DESTINATION`: when several local identifiers share the longest base
|
|
268
|
+
* path (one ingress URL for many identifiers), the one it names is the match. Without it, or when it names
|
|
269
|
+
* none of them, the tie stays ambiguous.
|
|
267
270
|
* @returns Matching hosted endpoint projection; ambiguity follows this helper's documented selection policy.
|
|
268
271
|
*/
|
|
269
|
-
export function resolveHostedEndpointPath(hby, pathname, resourceSuffix = "", eids) {
|
|
272
|
+
export function resolveHostedEndpointPath(hby, pathname, resourceSuffix = "", eids, destination) {
|
|
270
273
|
const matches = hostedEndpointPathMatches(hby, pathname, eids)
|
|
271
274
|
.filter((match) => resourceSuffix.length === 0
|
|
272
275
|
? true
|
|
@@ -275,8 +278,11 @@ export function resolveHostedEndpointPath(hby, pathname, resourceSuffix = "", ei
|
|
|
275
278
|
return { kind: "none", endpoint: null, relativePath: null };
|
|
276
279
|
}
|
|
277
280
|
const longest = matches[0].basePath.length;
|
|
278
|
-
|
|
279
|
-
if (narrowed.length > 1) {
|
|
281
|
+
let narrowed = matches.filter((match) => match.basePath.length === longest);
|
|
282
|
+
if (narrowed.length > 1 && destination) {
|
|
283
|
+
narrowed = narrowed.filter((match) => match.eid === destination);
|
|
284
|
+
}
|
|
285
|
+
if (narrowed.length !== 1) {
|
|
280
286
|
return { kind: "ambiguous", endpoint: null, relativePath: null };
|
|
281
287
|
}
|
|
282
288
|
const endpoint = narrowed[0];
|
|
@@ -41,7 +41,10 @@ import { Kever } from "../../core/kel/kever.js";
|
|
|
41
41
|
import { dgKey } from "../../db/core/keys.js";
|
|
42
42
|
import { pinAuthorizingSeal, retryDelegatorWitnessQuery, } from "../identity/delegating.js";
|
|
43
43
|
import { observeUntil } from "../runtime/workflow-contract.js";
|
|
44
|
-
import { groupWitnesser, isLocalGroupHab, localGroupMember } from "./group-members.js";
|
|
44
|
+
import { groupSigningMembers, groupWitnesser, isLocalGroupHab, localGroupMember } from "./group-members.js";
|
|
45
|
+
import { remoteGroupMembers } from "./multisig-messaging.js";
|
|
46
|
+
/** Forwarding attempts of one completed event before the Counselor stops retrying its failed members. */
|
|
47
|
+
export const FORWARD_ATTEMPTS = 8;
|
|
45
48
|
/** Grace before a non-elected member submits receipts itself. */
|
|
46
49
|
export const DEFAULT_FALLBACK_AFTER_MS = 60_000;
|
|
47
50
|
/** First and longest wait between receipt attempts for one event. */
|
|
@@ -80,7 +83,7 @@ export class Counselor {
|
|
|
80
83
|
* @param mailboxes Told when one of the group's rotations completes; the member runtime's publisher then tells
|
|
81
84
|
* the group's mailboxes the new key state, and several members doing so is harmless.
|
|
82
85
|
*/
|
|
83
|
-
constructor(hby, receipting, clock, changes, delegation, mailboxes, receiptRetry = DEFAULT_RECEIPT_RETRY) {
|
|
86
|
+
constructor(hby, receipting, clock, changes, delegation, mailboxes, receiptRetry = DEFAULT_RECEIPT_RETRY, forwarding) {
|
|
84
87
|
Object.defineProperty(this, "hby", {
|
|
85
88
|
enumerable: true,
|
|
86
89
|
configurable: true,
|
|
@@ -123,12 +126,31 @@ export class Counselor {
|
|
|
123
126
|
writable: true,
|
|
124
127
|
value: receiptRetry
|
|
125
128
|
});
|
|
129
|
+
Object.defineProperty(this, "forwarding", {
|
|
130
|
+
enumerable: true,
|
|
131
|
+
configurable: true,
|
|
132
|
+
writable: true,
|
|
133
|
+
value: forwarding
|
|
134
|
+
});
|
|
126
135
|
Object.defineProperty(this, "runs", {
|
|
127
136
|
enumerable: true,
|
|
128
137
|
configurable: true,
|
|
129
138
|
writable: true,
|
|
130
139
|
value: new Map()
|
|
131
140
|
});
|
|
141
|
+
/** Completed events this member, as the elected one, still owes to other members; memory only. */
|
|
142
|
+
Object.defineProperty(this, "forwards", {
|
|
143
|
+
enumerable: true,
|
|
144
|
+
configurable: true,
|
|
145
|
+
writable: true,
|
|
146
|
+
value: new Map()
|
|
147
|
+
});
|
|
148
|
+
Object.defineProperty(this, "forwarded", {
|
|
149
|
+
enumerable: true,
|
|
150
|
+
configurable: true,
|
|
151
|
+
writable: true,
|
|
152
|
+
value: new Map()
|
|
153
|
+
});
|
|
132
154
|
Object.defineProperty(this, "anchorQuery", {
|
|
133
155
|
enumerable: true,
|
|
134
156
|
configurable: true,
|
|
@@ -264,11 +286,53 @@ export class Counselor {
|
|
|
264
286
|
*processOnce() {
|
|
265
287
|
// A runtime calls this every turn; with nothing counseled, three row counts are the whole cost.
|
|
266
288
|
const db = this.hby.db;
|
|
267
|
-
if (db.gpse.cnt() === 0 && db.gdee.cnt() === 0 && db.gpwe.cnt() === 0)
|
|
289
|
+
if (db.gpse.cnt() === 0 && db.gdee.cnt() === 0 && db.gpwe.cnt() === 0 && this.forwards.size === 0)
|
|
268
290
|
return;
|
|
269
291
|
this.processSignatures();
|
|
270
292
|
yield* this.processDelegation();
|
|
271
293
|
yield* this.processWitnesses();
|
|
294
|
+
yield* this.forwardCompleted();
|
|
295
|
+
}
|
|
296
|
+
/** What became of the completed `event` this member forwarded to the others, per member; empty before it did. */
|
|
297
|
+
forwardOutcomes(event) {
|
|
298
|
+
return this.forwarded.get(this.runKey(event)) ?? [];
|
|
299
|
+
}
|
|
300
|
+
/** Send each completed event to the members that may not have it; failed members are retried with a doubling delay. */
|
|
301
|
+
*forwardCompleted() {
|
|
302
|
+
if (!this.forwarding)
|
|
303
|
+
return;
|
|
304
|
+
for (const [key, owed] of [...this.forwards]) {
|
|
305
|
+
const now = this.clock.now();
|
|
306
|
+
if (now < owed.nextAt)
|
|
307
|
+
continue;
|
|
308
|
+
let outcomes;
|
|
309
|
+
try {
|
|
310
|
+
outcomes = yield* this.forwarding.forward(owed.event, owed.members);
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
314
|
+
outcomes = owed.members.map((recipient) => ({
|
|
315
|
+
recipient,
|
|
316
|
+
exnSaid: owed.event.said,
|
|
317
|
+
payloadSaid: owed.event.said,
|
|
318
|
+
outcome: "failed",
|
|
319
|
+
deliveries: [],
|
|
320
|
+
reason,
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
const failed = outcomes.filter((outcome) => outcome.outcome === "failed");
|
|
324
|
+
this.forwarded.set(key, [
|
|
325
|
+
...(this.forwarded.get(key)?.filter((prior) => !outcomes.some((o) => o.recipient === prior.recipient)) ?? []),
|
|
326
|
+
...outcomes,
|
|
327
|
+
]);
|
|
328
|
+
owed.attempts++;
|
|
329
|
+
if (!failed.length || owed.attempts >= FORWARD_ATTEMPTS) {
|
|
330
|
+
this.forwards.delete(key);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
owed.members = failed.map((outcome) => outcome.recipient);
|
|
334
|
+
owed.nextAt = now + Math.min(this.receiptRetry.baseMs * 2 ** owed.attempts, this.receiptRetry.maxMs);
|
|
335
|
+
}
|
|
272
336
|
}
|
|
273
337
|
/** Move events accepted with their escrowed SAID on to delegation or witnesses. */
|
|
274
338
|
processSignatures() {
|
|
@@ -342,6 +406,14 @@ export class Counselor {
|
|
|
342
406
|
});
|
|
343
407
|
this.changes?.push({ kind: "group", id: `${event.pre}:${event.sn}` });
|
|
344
408
|
}
|
|
409
|
+
/** The elected member owes the completed event to the members that did not sign it into existence. */
|
|
410
|
+
owe(event) {
|
|
411
|
+
if (!this.forwarding || !groupWitnesser(this.hby, event.pre, event))
|
|
412
|
+
return;
|
|
413
|
+
const members = remoteGroupMembers(this.hby, localGroupMember(this.hby, event.pre), groupSigningMembers(this.hby, event.pre));
|
|
414
|
+
if (members.length)
|
|
415
|
+
this.forwards.set(this.runKey(event), { event, members, attempts: 0, nextAt: 0 });
|
|
416
|
+
}
|
|
345
417
|
/** Record completion of a counseled event and drop its row from `table`. */
|
|
346
418
|
finish(table, event, row) {
|
|
347
419
|
const db = this.hby.db;
|
|
@@ -351,6 +423,7 @@ export class Counselor {
|
|
|
351
423
|
});
|
|
352
424
|
this.runs.delete(this.runKey(event));
|
|
353
425
|
this.changes?.push({ kind: "group", id: `${event.pre}:${event.sn}` });
|
|
426
|
+
this.owe(event);
|
|
354
427
|
const ilk = db.getEvtSerder(event.pre, event.said)?.ilk;
|
|
355
428
|
if (ilk === Ilks.rot || ilk === Ilks.drt) {
|
|
356
429
|
this.mailboxes?.rotated({ kin: "rotated", pre: event.pre, sn: event.sn, said: event.said });
|