keri-ts 2.2.1 → 2.2.3
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/delegating.js +2 -5
- package/esm/keri/npm/src/app/identity/habbing.js +52 -6
- 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 +142 -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 +150 -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/group-members.js +22 -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/multisig/replace-member.js +34 -0
- 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 +9 -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/delegating.d.ts.map +1 -1
- package/types/app/identity/habbing.d.ts +21 -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 +46 -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 +51 -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/group-members.d.ts +6 -0
- package/types/app/multisig/group-members.d.ts.map +1 -1
- 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/multisig/replace-member.d.ts +30 -0
- package/types/app/multisig/replace-member.d.ts.map +1 -0
- 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 +11 -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
|
@@ -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"));
|
|
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"));
|
|
137
167
|
}
|
|
138
|
-
/**
|
|
139
|
-
|
|
140
|
-
return
|
|
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,30 @@ 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
|
}
|
|
290
|
+
/**
|
|
291
|
+
* End, as abandoned with `reason`, every live Unblind of local group `group` whose party member is `member`.
|
|
292
|
+
* Releasing the approval drops the owner's held query. Returns the Grants ended.
|
|
293
|
+
*/
|
|
294
|
+
endBoundToMember(group, member, reason) {
|
|
295
|
+
const bound = [...this.live.values()].filter(op => op.hab.pre === group && op.group?.member.pre === member);
|
|
296
|
+
for (const op of bound)
|
|
297
|
+
this.end(op, "abandoned", reason);
|
|
298
|
+
return bound.map(op => op.grant);
|
|
299
|
+
}
|
|
232
300
|
/** Drop the live entry and the owner's approval and frozen messages; delivered bytes stay the peer's. */
|
|
233
301
|
release(op) {
|
|
234
|
-
this.live.delete(op.grant);
|
|
302
|
+
this.live.delete(unblindingKey(op.grant, op.role));
|
|
235
303
|
try {
|
|
236
304
|
op.owner.abandon(op.xid);
|
|
237
305
|
}
|
|
@@ -242,7 +310,7 @@ export class UnblindingOperations {
|
|
|
242
310
|
}
|
|
243
311
|
finished(op) {
|
|
244
312
|
this.release(op);
|
|
245
|
-
this.settled.set(op.grant, { role: op.role, state: "done" });
|
|
313
|
+
this.settled.set(unblindingKey(op.grant, op.role), { role: op.role, state: "done" });
|
|
246
314
|
this.deps.changes?.push({ kind: "unblinding", id: op.grant });
|
|
247
315
|
}
|
|
248
316
|
*timer(milliseconds) {
|
|
@@ -413,9 +481,23 @@ export class UnblindingOperations {
|
|
|
413
481
|
op.finished = true;
|
|
414
482
|
return "stepped";
|
|
415
483
|
}
|
|
484
|
+
*deliverToParty(message) {
|
|
485
|
+
const { hby, reactor, delivery } = this.deps, recipient = hby.habs.get(message.dest);
|
|
486
|
+
if (!recipient || !reactor)
|
|
487
|
+
return yield* delivery.deliver(message);
|
|
488
|
+
yield* reactor.processBytes(message.msg);
|
|
489
|
+
const reached = [`local:${recipient.pre}`];
|
|
490
|
+
if (!isLocalGroupHab(hby, recipient))
|
|
491
|
+
return reached;
|
|
492
|
+
const member = localGroupMember(hby, recipient.pre);
|
|
493
|
+
for (const dest of remoteGroupMembers(hby, member, groupSigningMembers(hby, recipient.pre))) {
|
|
494
|
+
reached.push(...yield* delivery.deliver({ ...message, dest }));
|
|
495
|
+
}
|
|
496
|
+
return reached;
|
|
497
|
+
}
|
|
416
498
|
/** One pass: send what is queued, step only once the queue is empty, send what the step queued. */
|
|
417
499
|
*advance(op) {
|
|
418
|
-
const drain = () => op.outbox.drain(this.
|
|
500
|
+
const drain = () => op.outbox.drain(this.transport, this.deps.clock);
|
|
419
501
|
if (!(yield* drain()))
|
|
420
502
|
return "stepped";
|
|
421
503
|
if (!op.finished)
|
|
@@ -428,7 +510,7 @@ export class UnblindingOperations {
|
|
|
428
510
|
/** The exact live Grant the local party may consent to, with its deadline and role. */
|
|
429
511
|
function selectGrant(ops, grant, role, proposer) {
|
|
430
512
|
const { deps } = ops;
|
|
431
|
-
if (ops.
|
|
513
|
+
if (ops.liveOf(grant, role))
|
|
432
514
|
throw new ValidationError("Unblind was already begun for this Grant");
|
|
433
515
|
const input = deps.exc.retainedInput(grant);
|
|
434
516
|
if (!input)
|
|
@@ -460,6 +542,14 @@ function consent(ops, grant, role, options) {
|
|
|
460
542
|
if (role === "responder" && !isLocalGroupHab(deps.hby, hab)) {
|
|
461
543
|
throw new ValidationError("An individual discloser answers an authenticated query without consent");
|
|
462
544
|
}
|
|
545
|
+
if (role === "responder" && deps.hby.db.erpy.get([grant])) {
|
|
546
|
+
// The verifier already answered the presentation, so no query will come: nothing to approve or answer.
|
|
547
|
+
const closed = selected.input.serder;
|
|
548
|
+
const xid = closed.ilk === "xip" ? closed.said : String(closed.sad.x);
|
|
549
|
+
ops.remember(grant, { role: "responder", state: "unneeded", reason: UNNEEDED });
|
|
550
|
+
deps.changes?.push({ kind: "unblinding", id: grant });
|
|
551
|
+
return { op: null, begun: { grant, xid, role } };
|
|
552
|
+
}
|
|
463
553
|
const owner = deps.unblinds.unblind(hab), xid = owner.approve(grant, { states: options.states });
|
|
464
554
|
const begun = { grant, xid, role };
|
|
465
555
|
// A current authenticated view needs no empty auxiliary query or new proof writes.
|
|
@@ -467,7 +557,7 @@ function consent(ops, grant, role, options) {
|
|
|
467
557
|
const current = deps.exc.verifyGrantProofs(grant, selected.input.nests ?? []);
|
|
468
558
|
if (current === true || typeof current === "object" && current.kind === "accept") {
|
|
469
559
|
owner.abandon(xid);
|
|
470
|
-
ops.
|
|
560
|
+
ops.remember(grant, { role: "requester", state: "done" });
|
|
471
561
|
deps.changes?.push({ kind: "unblinding", id: grant });
|
|
472
562
|
return { op: null, begun };
|
|
473
563
|
}
|
|
@@ -512,8 +602,7 @@ export function requestUnblind(ops, grant, options = {}) {
|
|
|
512
602
|
}
|
|
513
603
|
else
|
|
514
604
|
op.query = op.owner.start(grant);
|
|
515
|
-
ops.
|
|
516
|
-
ops.live.set(grant, op);
|
|
605
|
+
ops.begin(op);
|
|
517
606
|
ops.deps.changes?.push({ kind: "unblinding", id: grant });
|
|
518
607
|
}
|
|
519
608
|
return begun;
|
|
@@ -530,9 +619,10 @@ export function requestUnblind(ops, grant, options = {}) {
|
|
|
530
619
|
export function answerUnblind(ops, grant, options = {}) {
|
|
531
620
|
const { deps } = ops;
|
|
532
621
|
const { op, begun } = consent(ops, grant, "responder", options);
|
|
622
|
+
if (!op)
|
|
623
|
+
return begun;
|
|
533
624
|
registerUnblindRoutes(ops);
|
|
534
|
-
ops.
|
|
535
|
-
ops.live.set(grant, op);
|
|
625
|
+
ops.begin(op);
|
|
536
626
|
deps.changes?.push({ kind: "unblinding", id: grant });
|
|
537
627
|
return begun;
|
|
538
628
|
}
|
|
@@ -582,7 +672,7 @@ function registerUnblindRoutes(ops) {
|
|
|
582
672
|
acceptsNests: true,
|
|
583
673
|
verify: (args) => structural(deps.hby, args.serder, args.nests ?? []) && !args.attachments.length && !args.essrs?.length,
|
|
584
674
|
handle: (args) => {
|
|
585
|
-
for (const op of ops.
|
|
675
|
+
for (const op of ops.liveOps()) {
|
|
586
676
|
if (op.group && groupAccepts(deps.hby, op, args.serder, args.nests)) {
|
|
587
677
|
op.group.pending.set(args.serder.said, { parent: args.serder, child: args.nests[0] });
|
|
588
678
|
}
|
|
@@ -601,8 +691,9 @@ function registerUnblindRoutes(ops) {
|
|
|
601
691
|
* @throws ValidationError when `grant` is not a Grant a local identifier is the disclosee of, and no
|
|
602
692
|
* operation for it is live or remembered.
|
|
603
693
|
*/
|
|
604
|
-
export function unblindingStatus(ops, grant) {
|
|
605
|
-
const { deps } = ops,
|
|
694
|
+
export function unblindingStatus(ops, grant, party = {}) {
|
|
695
|
+
const { deps } = ops, role = chooseParty(deps, grant, party);
|
|
696
|
+
const settled = ops.settledOf(grant, role), live = ops.liveOf(grant, role);
|
|
606
697
|
if (settled) {
|
|
607
698
|
return settled.state === "done"
|
|
608
699
|
? { state: "done", role: settled.role }
|
|
@@ -618,6 +709,13 @@ export function unblindingStatus(ops, grant) {
|
|
|
618
709
|
};
|
|
619
710
|
}
|
|
620
711
|
const input = deps.exc.retainedInput(grant), recipient = input?.serder.sad?.ri;
|
|
712
|
+
if (role === "responder" && input && input.serder.pre && deps.hby.habs.has(input.serder.pre)) {
|
|
713
|
+
// A discloser that has not begun (a group member before its answer) reads as not begun, not as an error.
|
|
714
|
+
const lifetime = deps.exc.exchangeDeadline(input.serder);
|
|
715
|
+
return lifetime && lapsed(deps.clock, lifetime)
|
|
716
|
+
? { state: "expired", role: "responder", reason: EXPIRED }
|
|
717
|
+
: { state: "interrupted", role: "responder", reason: NOT_ANSWERED };
|
|
718
|
+
}
|
|
621
719
|
if (!input || typeof recipient !== "string" || !deps.hby.habs.has(recipient)) {
|
|
622
720
|
throw new ValidationError("No Unblind was begun for this Grant");
|
|
623
721
|
}
|
|
@@ -634,16 +732,32 @@ export function unblindingStatus(ops, grant) {
|
|
|
634
732
|
return { state: "expired", role: "requester", reason: EXPIRED };
|
|
635
733
|
return { state: "interrupted", role: "requester", reason: INTERRUPTED };
|
|
636
734
|
}
|
|
735
|
+
/**
|
|
736
|
+
* The party a read is for: the one asked for, else the Habery's only party, else (it holds both) the requester.
|
|
737
|
+
* Undefined when the Grant is not stored here, so only what is live or remembered answers.
|
|
738
|
+
*
|
|
739
|
+
* @throws ValidationError when `choice` names a party this Habery does not hold.
|
|
740
|
+
*/
|
|
741
|
+
function chooseParty(deps, grant, choice) {
|
|
742
|
+
const input = deps.exc.retainedInput(grant);
|
|
743
|
+
if (!input)
|
|
744
|
+
return choice.role;
|
|
745
|
+
const held = [
|
|
746
|
+
...(deps.hby.habs.has(String(input.serder.sad?.ri)) ? ["requester"] : []),
|
|
747
|
+
...(input.serder.pre && deps.hby.habs.has(input.serder.pre) ? ["responder"] : []),
|
|
748
|
+
];
|
|
749
|
+
if (choice.role && !held.includes(choice.role)) {
|
|
750
|
+
throw new ValidationError(`This Habery holds no ${choice.role} of this presentation`);
|
|
751
|
+
}
|
|
752
|
+
return choice.role ?? held[0];
|
|
753
|
+
}
|
|
637
754
|
/**
|
|
638
755
|
* Observe until the Unblind is done or ended, or the budget expires. Starts no worker and drives nothing:
|
|
639
756
|
* someone else must call `processOnce`.
|
|
640
757
|
*/
|
|
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);
|
|
758
|
+
export function* waitForUnblinding(ops, grant, { timeoutMs, clock = ops.deps.clock, role }) {
|
|
759
|
+
const settled = yield* observeUntil(() => unblindingStatus(ops, grant, { role }).state !== "active" && !ops.owes(grant, role), { timeoutMs, clock });
|
|
760
|
+
const status = unblindingStatus(ops, grant, { role });
|
|
647
761
|
return settled ? { outcome: "settled", status } : { outcome: "timed-out", status, timeoutMs };
|
|
648
762
|
}
|
|
649
763
|
/**
|
|
@@ -653,15 +767,15 @@ export function* waitForUnblinding(ops, grant, { timeoutMs, clock = ops.deps.clo
|
|
|
653
767
|
*
|
|
654
768
|
* @throws ValidationError when no Unblind was begun for `grant`.
|
|
655
769
|
*/
|
|
656
|
-
export function abandonUnblinding(ops, grant, reason = "Abandoned by the user") {
|
|
657
|
-
const status = unblindingStatus(ops, grant);
|
|
770
|
+
export function abandonUnblinding(ops, grant, reason = "Abandoned by the user", party = {}) {
|
|
771
|
+
const status = unblindingStatus(ops, grant, party);
|
|
658
772
|
if (status.state !== "active" && status.state !== "interrupted")
|
|
659
773
|
return;
|
|
660
|
-
const live = ops.
|
|
774
|
+
const live = ops.liveOf(grant, status.role);
|
|
661
775
|
if (live)
|
|
662
776
|
ops.end(live, "abandoned", reason);
|
|
663
777
|
else {
|
|
664
|
-
ops.
|
|
778
|
+
ops.remember(grant, { role: status.role, state: "abandoned", reason });
|
|
665
779
|
ops.deps.changes?.push({ kind: "unblinding", id: grant });
|
|
666
780
|
}
|
|
667
781
|
}
|
|
@@ -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 });
|
|
@@ -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
|
+
}
|