keri-ts 2.2.0 → 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/custody.js +23 -0
- 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-ipex.js +14 -0
- package/esm/keri/npm/src/app/ipex/group-issuance.js +15 -9
- 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/ipex-grouping.js +16 -8
- package/esm/keri/npm/src/app/ipex/ipex-node-authentication.js +42 -18
- package/esm/keri/npm/src/app/ipex/issuance.js +136 -40
- package/esm/keri/npm/src/app/ipex/pending-presentations.js +50 -0
- package/esm/keri/npm/src/app/ipex/presenting.js +44 -2
- package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +141 -30
- 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 +83 -4
- 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/multisig-messaging.js +33 -6
- package/esm/keri/npm/src/app/multisig/multisig-workflows.js +5 -9
- 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/cli/credentials/ipex.js +7 -1
- package/esm/keri/npm/src/cli/messaging/group-join.js +7 -3
- package/esm/keri/npm/src/cli/messaging/multisig.js +37 -11
- 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 +9 -8
- 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/custody.d.ts +7 -0
- package/types/acdc/custody.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-ipex.d.ts +7 -0
- package/types/app/ipex/group-ipex.d.ts.map +1 -1
- package/types/app/ipex/group-issuance.d.ts +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/ipex-grouping.d.ts.map +1 -1
- package/types/app/ipex/ipex-node-authentication.d.ts +9 -1
- package/types/app/ipex/ipex-node-authentication.d.ts.map +1 -1
- package/types/app/ipex/issuance.d.ts +41 -8
- 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 +41 -4
- package/types/app/ipex/presenting.d.ts.map +1 -1
- package/types/app/ipex/unblinding-workflow.d.ts +46 -10
- 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/multisig-messaging.d.ts +23 -6
- package/types/app/multisig/multisig-messaging.d.ts.map +1 -1
- package/types/app/multisig/multisig-workflows.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/cli/credentials/ipex.d.ts.map +1 -1
- package/types/cli/messaging/group-join.d.ts +4 -1
- package/types/cli/messaging/group-join.d.ts.map +1 -1
- package/types/cli/messaging/multisig.d.ts +1 -0
- package/types/cli/messaging/multisig.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 +12 -11
- 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
|
@@ -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,6 +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"));
|
|
161
|
+
}
|
|
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");
|
|
137
186
|
}
|
|
138
187
|
/**
|
|
139
188
|
* Whether a pass has work: a live operation, or an owner holds an authenticated peer query whose reply is
|
|
@@ -152,19 +201,23 @@ export class UnblindingOperations {
|
|
|
152
201
|
const { deps } = this;
|
|
153
202
|
this.adopt();
|
|
154
203
|
for (const op of [...this.live.values()]) {
|
|
155
|
-
if (op.busy ||
|
|
204
|
+
if (op.busy || this.live.get(unblindingKey(op.grant, op.role)) !== op)
|
|
156
205
|
continue;
|
|
157
206
|
op.busy = true;
|
|
158
207
|
try {
|
|
159
208
|
const left = op.expires - BigInt(deps.clock.now()) * 1000n;
|
|
160
209
|
if (left <= 0n)
|
|
161
|
-
this.end(op, "expired",
|
|
210
|
+
this.end(op, "expired", this.expiry(op));
|
|
162
211
|
else if ((yield* race([this.advance(op), this.timer(Number(left) / 1000)])) === "expired") {
|
|
163
|
-
this.end(op, "expired",
|
|
212
|
+
this.end(op, "expired", this.expiry(op));
|
|
164
213
|
}
|
|
165
214
|
}
|
|
166
215
|
catch (error) {
|
|
167
|
-
|
|
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));
|
|
168
221
|
}
|
|
169
222
|
finally {
|
|
170
223
|
op.busy = false;
|
|
@@ -181,7 +234,7 @@ export class UnblindingOperations {
|
|
|
181
234
|
const { deps } = this;
|
|
182
235
|
for (const owner of deps.unblinds.unblindOwners()) {
|
|
183
236
|
for (const { xid, grant } of owner.awaitingReply()) {
|
|
184
|
-
if (this.live.has(grant))
|
|
237
|
+
if (this.live.has(unblindingKey(grant, "responder")))
|
|
185
238
|
continue;
|
|
186
239
|
let input, deadline, expires;
|
|
187
240
|
try {
|
|
@@ -192,14 +245,18 @@ export class UnblindingOperations {
|
|
|
192
245
|
catch (error) {
|
|
193
246
|
// Unreadable stored state fails this reply alone; release the owner's approval so it is not retried.
|
|
194
247
|
owner.abandon(xid);
|
|
195
|
-
this.settled.set(grant,
|
|
248
|
+
this.settled.set(unblindingKey(grant, "responder"), {
|
|
249
|
+
role: "responder",
|
|
250
|
+
state: "failed",
|
|
251
|
+
reason: message(error),
|
|
252
|
+
});
|
|
196
253
|
deps.changes?.push({ kind: "unblinding", id: grant });
|
|
197
254
|
continue;
|
|
198
255
|
}
|
|
199
256
|
if (!input || !deadline || expires === null || BigInt(deps.clock.now()) * 1000n >= expires)
|
|
200
257
|
continue;
|
|
201
|
-
this.settled.delete(grant);
|
|
202
|
-
this.live.set(grant, {
|
|
258
|
+
this.settled.delete(unblindingKey(grant, "responder"));
|
|
259
|
+
this.live.set(unblindingKey(grant, "responder"), {
|
|
203
260
|
grant,
|
|
204
261
|
xid,
|
|
205
262
|
role: "responder",
|
|
@@ -219,15 +276,20 @@ export class UnblindingOperations {
|
|
|
219
276
|
}
|
|
220
277
|
}
|
|
221
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
|
+
}
|
|
222
284
|
/** End a live operation: release the approval, remember why until the process ends, tell the sink. */
|
|
223
285
|
end(op, state, reason) {
|
|
224
286
|
this.release(op);
|
|
225
|
-
this.settled.set(op.grant, { role: op.role, state, reason });
|
|
287
|
+
this.settled.set(unblindingKey(op.grant, op.role), { role: op.role, state, reason });
|
|
226
288
|
this.deps.changes?.push({ kind: "unblinding", id: op.grant });
|
|
227
289
|
}
|
|
228
290
|
/** Drop the live entry and the owner's approval and frozen messages; delivered bytes stay the peer's. */
|
|
229
291
|
release(op) {
|
|
230
|
-
this.live.delete(op.grant);
|
|
292
|
+
this.live.delete(unblindingKey(op.grant, op.role));
|
|
231
293
|
try {
|
|
232
294
|
op.owner.abandon(op.xid);
|
|
233
295
|
}
|
|
@@ -238,7 +300,7 @@ export class UnblindingOperations {
|
|
|
238
300
|
}
|
|
239
301
|
finished(op) {
|
|
240
302
|
this.release(op);
|
|
241
|
-
this.settled.set(op.grant, { role: op.role, state: "done" });
|
|
303
|
+
this.settled.set(unblindingKey(op.grant, op.role), { role: op.role, state: "done" });
|
|
242
304
|
this.deps.changes?.push({ kind: "unblinding", id: op.grant });
|
|
243
305
|
}
|
|
244
306
|
*timer(milliseconds) {
|
|
@@ -409,9 +471,23 @@ export class UnblindingOperations {
|
|
|
409
471
|
op.finished = true;
|
|
410
472
|
return "stepped";
|
|
411
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
|
+
}
|
|
412
488
|
/** One pass: send what is queued, step only once the queue is empty, send what the step queued. */
|
|
413
489
|
*advance(op) {
|
|
414
|
-
const drain = () => op.outbox.drain(this.
|
|
490
|
+
const drain = () => op.outbox.drain(this.transport, this.deps.clock);
|
|
415
491
|
if (!(yield* drain()))
|
|
416
492
|
return "stepped";
|
|
417
493
|
if (!op.finished)
|
|
@@ -424,7 +500,7 @@ export class UnblindingOperations {
|
|
|
424
500
|
/** The exact live Grant the local party may consent to, with its deadline and role. */
|
|
425
501
|
function selectGrant(ops, grant, role, proposer) {
|
|
426
502
|
const { deps } = ops;
|
|
427
|
-
if (ops.
|
|
503
|
+
if (ops.liveOf(grant, role))
|
|
428
504
|
throw new ValidationError("Unblind was already begun for this Grant");
|
|
429
505
|
const input = deps.exc.retainedInput(grant);
|
|
430
506
|
if (!input)
|
|
@@ -456,6 +532,14 @@ function consent(ops, grant, role, options) {
|
|
|
456
532
|
if (role === "responder" && !isLocalGroupHab(deps.hby, hab)) {
|
|
457
533
|
throw new ValidationError("An individual discloser answers an authenticated query without consent");
|
|
458
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
|
+
}
|
|
459
543
|
const owner = deps.unblinds.unblind(hab), xid = owner.approve(grant, { states: options.states });
|
|
460
544
|
const begun = { grant, xid, role };
|
|
461
545
|
// A current authenticated view needs no empty auxiliary query or new proof writes.
|
|
@@ -463,7 +547,7 @@ function consent(ops, grant, role, options) {
|
|
|
463
547
|
const current = deps.exc.verifyGrantProofs(grant, selected.input.nests ?? []);
|
|
464
548
|
if (current === true || typeof current === "object" && current.kind === "accept") {
|
|
465
549
|
owner.abandon(xid);
|
|
466
|
-
ops.
|
|
550
|
+
ops.remember(grant, { role: "requester", state: "done" });
|
|
467
551
|
deps.changes?.push({ kind: "unblinding", id: grant });
|
|
468
552
|
return { op: null, begun };
|
|
469
553
|
}
|
|
@@ -508,8 +592,7 @@ export function requestUnblind(ops, grant, options = {}) {
|
|
|
508
592
|
}
|
|
509
593
|
else
|
|
510
594
|
op.query = op.owner.start(grant);
|
|
511
|
-
ops.
|
|
512
|
-
ops.live.set(grant, op);
|
|
595
|
+
ops.begin(op);
|
|
513
596
|
ops.deps.changes?.push({ kind: "unblinding", id: grant });
|
|
514
597
|
}
|
|
515
598
|
return begun;
|
|
@@ -526,9 +609,10 @@ export function requestUnblind(ops, grant, options = {}) {
|
|
|
526
609
|
export function answerUnblind(ops, grant, options = {}) {
|
|
527
610
|
const { deps } = ops;
|
|
528
611
|
const { op, begun } = consent(ops, grant, "responder", options);
|
|
612
|
+
if (!op)
|
|
613
|
+
return begun;
|
|
529
614
|
registerUnblindRoutes(ops);
|
|
530
|
-
ops.
|
|
531
|
-
ops.live.set(grant, op);
|
|
615
|
+
ops.begin(op);
|
|
532
616
|
deps.changes?.push({ kind: "unblinding", id: grant });
|
|
533
617
|
return begun;
|
|
534
618
|
}
|
|
@@ -578,7 +662,7 @@ function registerUnblindRoutes(ops) {
|
|
|
578
662
|
acceptsNests: true,
|
|
579
663
|
verify: (args) => structural(deps.hby, args.serder, args.nests ?? []) && !args.attachments.length && !args.essrs?.length,
|
|
580
664
|
handle: (args) => {
|
|
581
|
-
for (const op of ops.
|
|
665
|
+
for (const op of ops.liveOps()) {
|
|
582
666
|
if (op.group && groupAccepts(deps.hby, op, args.serder, args.nests)) {
|
|
583
667
|
op.group.pending.set(args.serder.said, { parent: args.serder, child: args.nests[0] });
|
|
584
668
|
}
|
|
@@ -597,8 +681,9 @@ function registerUnblindRoutes(ops) {
|
|
|
597
681
|
* @throws ValidationError when `grant` is not a Grant a local identifier is the disclosee of, and no
|
|
598
682
|
* operation for it is live or remembered.
|
|
599
683
|
*/
|
|
600
|
-
export function unblindingStatus(ops, grant) {
|
|
601
|
-
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);
|
|
602
687
|
if (settled) {
|
|
603
688
|
return settled.state === "done"
|
|
604
689
|
? { state: "done", role: settled.role }
|
|
@@ -614,6 +699,13 @@ export function unblindingStatus(ops, grant) {
|
|
|
614
699
|
};
|
|
615
700
|
}
|
|
616
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
|
+
}
|
|
617
709
|
if (!input || typeof recipient !== "string" || !deps.hby.habs.has(recipient)) {
|
|
618
710
|
throw new ValidationError("No Unblind was begun for this Grant");
|
|
619
711
|
}
|
|
@@ -630,13 +722,32 @@ export function unblindingStatus(ops, grant) {
|
|
|
630
722
|
return { state: "expired", role: "requester", reason: EXPIRED };
|
|
631
723
|
return { state: "interrupted", role: "requester", reason: INTERRUPTED };
|
|
632
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
|
+
}
|
|
633
744
|
/**
|
|
634
745
|
* Observe until the Unblind is done or ended, or the budget expires. Starts no worker and drives nothing:
|
|
635
746
|
* someone else must call `processOnce`.
|
|
636
747
|
*/
|
|
637
|
-
export function* waitForUnblinding(ops, grant, { timeoutMs, clock = ops.deps.clock }) {
|
|
638
|
-
const settled = yield* observeUntil(() => unblindingStatus(ops, grant).state !== "active", { timeoutMs, clock });
|
|
639
|
-
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 });
|
|
640
751
|
return settled ? { outcome: "settled", status } : { outcome: "timed-out", status, timeoutMs };
|
|
641
752
|
}
|
|
642
753
|
/**
|
|
@@ -646,15 +757,15 @@ export function* waitForUnblinding(ops, grant, { timeoutMs, clock = ops.deps.clo
|
|
|
646
757
|
*
|
|
647
758
|
* @throws ValidationError when no Unblind was begun for `grant`.
|
|
648
759
|
*/
|
|
649
|
-
export function abandonUnblinding(ops, grant, reason = "Abandoned by the user") {
|
|
650
|
-
const status = unblindingStatus(ops, grant);
|
|
760
|
+
export function abandonUnblinding(ops, grant, reason = "Abandoned by the user", party = {}) {
|
|
761
|
+
const status = unblindingStatus(ops, grant, party);
|
|
651
762
|
if (status.state !== "active" && status.state !== "interrupted")
|
|
652
763
|
return;
|
|
653
|
-
const live = ops.
|
|
764
|
+
const live = ops.liveOf(grant, status.role);
|
|
654
765
|
if (live)
|
|
655
766
|
ops.end(live, "abandoned", reason);
|
|
656
767
|
else {
|
|
657
|
-
ops.
|
|
768
|
+
ops.remember(grant, { role: status.role, state: "abandoned", reason });
|
|
658
769
|
ops.deps.changes?.push({ kind: "unblinding", id: grant });
|
|
659
770
|
}
|
|
660
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];
|
|
@@ -35,13 +35,16 @@
|
|
|
35
35
|
* restart never leaves a delegated event waiting for one. Collaborators are
|
|
36
36
|
* narrow (ADR-0013); nothing here reaches a runtime.
|
|
37
37
|
*/
|
|
38
|
-
import { Diger, Ilks, NumberPrimitive, NumDex, Verfer } from "cesr-ts";
|
|
38
|
+
import { Diger, Ilks, NumberPrimitive, NumDex, ParserError, Verfer } from "cesr-ts";
|
|
39
39
|
import { ValidationError } from "../../core/errors.js";
|
|
40
40
|
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 });
|
|
@@ -394,6 +467,12 @@ export class Counselor {
|
|
|
394
467
|
// A witness refusing or being unreachable is a remote outcome: report it in status and retry later.
|
|
395
468
|
run.failure = error instanceof Error ? error.message : String(error);
|
|
396
469
|
run.delay = Math.min(run.delay * 2, this.receiptRetry.maxMs);
|
|
470
|
+
// A witness that answered with a reply this member cannot read (a receipt the elected member already
|
|
471
|
+
// obtained) will not answer better later: submit on the next attempt instead of waiting out the grace.
|
|
472
|
+
if (!submit && error instanceof ParserError) {
|
|
473
|
+
run.fallbackAfterMs = 0;
|
|
474
|
+
run.delay = this.receiptRetry.baseMs;
|
|
475
|
+
}
|
|
397
476
|
}
|
|
398
477
|
run.nextAt = now + run.delay;
|
|
399
478
|
if (!this.hby.db.fullyWitnessed(serder))
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { acceptedEventReplayMessage } from "../identity/habbing.js";
|
|
2
|
+
import { localGroupMember } from "./group-members.js";
|
|
3
|
+
import { failedGroupDelivery } from "./multisig-messaging.js";
|
|
4
|
+
/** The Counselor's forwarding port over a Habery and its Poster; the Poster's delivery worker must run. */
|
|
5
|
+
export function groupEventForwarding(hby, postman) {
|
|
6
|
+
return {
|
|
7
|
+
*forward(event, members) {
|
|
8
|
+
const { serder } = acceptedEventReplayMessage(hby, event.pre, event.sn);
|
|
9
|
+
const { message } = acceptedEventReplayMessage(hby, event.pre, event.sn, serder.pvrsn);
|
|
10
|
+
const sender = localGroupMember(hby, event.pre);
|
|
11
|
+
const outcomes = [];
|
|
12
|
+
for (const recipient of members) {
|
|
13
|
+
const ref = { recipient, exnSaid: event.said, payloadSaid: event.said };
|
|
14
|
+
try {
|
|
15
|
+
const outcome = yield* postman.wait(postman.send({ src: sender.pre, dest: recipient, topic: "/replay", msg: message }, { notify: true }));
|
|
16
|
+
outcomes.push(outcome.kin === "failed"
|
|
17
|
+
? failedGroupDelivery(ref, outcome.error)
|
|
18
|
+
: { ...ref, outcome: "delivered", deliveries: [...outcome.deliveries] });
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
outcomes.push(failedGroupDelivery(ref, error));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return outcomes;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|