@agentvault/grok-bridge 0.4.13 → 0.4.15
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/dist/bridge.d.ts +3 -0
- package/dist/index.js +548 -273
- package/package.json +1 -1
package/dist/bridge.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface RoomMessage {
|
|
|
14
14
|
roomId: string;
|
|
15
15
|
senderName: string;
|
|
16
16
|
plaintext: string;
|
|
17
|
+
/** #1223 — the text the sender typed, before any attachment was folded into
|
|
18
|
+
* `plaintext`. The @ gate reads this; absent on emitters < 0.23.42. */
|
|
19
|
+
caption?: string;
|
|
17
20
|
/** True when the sender is another agent (SecureChannel derives this from the
|
|
18
21
|
* room roster). Used to decide reply expectation: a human/owner speaking in a
|
|
19
22
|
* room always expects a reply; agent-authored traffic does not (the agent may
|
package/dist/index.js
CHANGED
|
@@ -64657,11 +64657,11 @@ var init_mls_kp_pool = __esm2({
|
|
|
64657
64657
|
}
|
|
64658
64658
|
});
|
|
64659
64659
|
function ownIdentity() {
|
|
64660
|
-
const v2 = true ? "0.23.
|
|
64660
|
+
const v2 = true ? "0.23.42" : FALLBACK;
|
|
64661
64661
|
return `${PACKAGE}@${v2}`;
|
|
64662
64662
|
}
|
|
64663
64663
|
function buildSha() {
|
|
64664
|
-
const raw = true ? "
|
|
64664
|
+
const raw = true ? "d9d53a0-dirty" : "";
|
|
64665
64665
|
const cleaned = (raw ?? "").trim();
|
|
64666
64666
|
return !cleaned || cleaned === "unknown" ? null : cleaned;
|
|
64667
64667
|
}
|
|
@@ -65568,6 +65568,7 @@ var CREDENTIAL_MESSAGE_TYPES;
|
|
|
65568
65568
|
var CATCHUP_MESSAGE_TYPES;
|
|
65569
65569
|
var POLL_INTERVAL_MS;
|
|
65570
65570
|
var RECONNECT_BASE_MS;
|
|
65571
|
+
var UUID_RE;
|
|
65571
65572
|
var RECONNECT_MAX_MS;
|
|
65572
65573
|
var PENDING_POLL_INTERVAL_MS;
|
|
65573
65574
|
var OUTBOUND_DEDUPE_WINDOW_MS;
|
|
@@ -65615,6 +65616,7 @@ var init_channel = __esm2({
|
|
|
65615
65616
|
]);
|
|
65616
65617
|
POLL_INTERVAL_MS = 6e3;
|
|
65617
65618
|
RECONNECT_BASE_MS = 1e3;
|
|
65619
|
+
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
65618
65620
|
RECONNECT_MAX_MS = 3e4;
|
|
65619
65621
|
PENDING_POLL_INTERVAL_MS = 15e3;
|
|
65620
65622
|
OUTBOUND_DEDUPE_WINDOW_MS = 3e3;
|
|
@@ -66910,7 +66912,7 @@ ${text}`;
|
|
|
66910
66912
|
*/
|
|
66911
66913
|
sendActivitySpan(spanData) {
|
|
66912
66914
|
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
|
|
66913
|
-
const pluginVersion = true ? "0.23.
|
|
66915
|
+
const pluginVersion = true ? "0.23.42" : "0.0.0-dev";
|
|
66914
66916
|
const agentName = this.config.agentName ?? "Agent";
|
|
66915
66917
|
const resource = {
|
|
66916
66918
|
"service.name": "agentvault-agent",
|
|
@@ -68848,7 +68850,7 @@ ${text}`;
|
|
|
68848
68850
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
68849
68851
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
68850
68852
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
68851
|
-
pluginVersion: true ? "0.23.
|
|
68853
|
+
pluginVersion: true ? "0.23.42" : "0.0.0-dev"
|
|
68852
68854
|
});
|
|
68853
68855
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
68854
68856
|
}
|
|
@@ -69172,7 +69174,7 @@ ${text}`;
|
|
|
69172
69174
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
69173
69175
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
69174
69176
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
69175
|
-
pluginVersion: true ? "0.23.
|
|
69177
|
+
pluginVersion: true ? "0.23.42" : "0.0.0-dev"
|
|
69176
69178
|
});
|
|
69177
69179
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
69178
69180
|
}
|
|
@@ -69387,45 +69389,78 @@ ${text}`;
|
|
|
69387
69389
|
const mlsGroup = this._mlsGroups.get(`a2a:${channelId}`);
|
|
69388
69390
|
if (mlsGroup?.isInitialized) {
|
|
69389
69391
|
try {
|
|
69390
|
-
const
|
|
69391
|
-
|
|
69392
|
-
|
|
69393
|
-
|
|
69394
|
-
|
|
69395
|
-
|
|
69396
|
-
|
|
69397
|
-
|
|
69398
|
-
|
|
69399
|
-
|
|
69400
|
-
|
|
69401
|
-
|
|
69402
|
-
|
|
69403
|
-
|
|
69404
|
-
|
|
69405
|
-
|
|
69406
|
-
|
|
69407
|
-
|
|
69408
|
-
|
|
69409
|
-
|
|
69410
|
-
|
|
69411
|
-
|
|
69412
|
-
|
|
69413
|
-
);
|
|
69414
|
-
|
|
69415
|
-
|
|
69416
|
-
|
|
69417
|
-
|
|
69418
|
-
|
|
69419
|
-
|
|
69420
|
-
|
|
69421
|
-
|
|
69422
|
-
|
|
69423
|
-
|
|
69392
|
+
const newDeviceId = participant.device_id || null;
|
|
69393
|
+
const a2aLabel = `A2A ${channelId.slice(0, 8)} participant_joined`;
|
|
69394
|
+
if (!newDeviceId) {
|
|
69395
|
+
console.warn(
|
|
69396
|
+
`[SecureChannel] ${a2aLabel}: no device_id on the payload for ${participant.hub_address} \u2014 NOT adding (a Welcome could not be addressed)`
|
|
69397
|
+
);
|
|
69398
|
+
} else if (!this._wsIsOpen()) {
|
|
69399
|
+
console.warn(`[SecureChannel] ${a2aLabel}: socket is not OPEN \u2014 NOT advancing the group`);
|
|
69400
|
+
} else {
|
|
69401
|
+
const preHead = await this._readGroupHead(entry.mlsGroupId);
|
|
69402
|
+
const inFlight = preHead.ok && preHead.epoch !== Number(mlsGroup.epoch) ? null : this._reserveCommit(entry.mlsGroupId, a2aLabel);
|
|
69403
|
+
if (preHead.ok && preHead.epoch !== Number(mlsGroup.epoch)) {
|
|
69404
|
+
console.warn(
|
|
69405
|
+
`[SecureChannel] ${a2aLabel}: server head is at epoch ${preHead.epoch}, we are at ${Number(mlsGroup.epoch)} \u2014 NOT committing (it would be refused); resync catches up first`
|
|
69406
|
+
);
|
|
69407
|
+
await this._catchUpAfterPreflightSkip(entry.mlsGroupId, preHead.epoch, Number(mlsGroup.epoch), a2aLabel);
|
|
69408
|
+
} else if (inFlight) {
|
|
69409
|
+
try {
|
|
69410
|
+
const kpRes = await fetch(
|
|
69411
|
+
`${this.config.apiUrl}/api/v1/mls/key-packages?hub_id=${encodeURIComponent(participant.hub_id)}`,
|
|
69412
|
+
{ headers: { Authorization: `Bearer ${this._deviceJwt}` } }
|
|
69413
|
+
);
|
|
69414
|
+
if (kpRes.ok) {
|
|
69415
|
+
const kpData = await kpRes.json();
|
|
69416
|
+
if (kpData.key_package) {
|
|
69417
|
+
const kpBytes = new Uint8Array(Buffer.from(kpData.key_package, "hex"));
|
|
69418
|
+
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
69419
|
+
const stateBefore = mlsGroup.exportState();
|
|
69420
|
+
const { commit, welcome } = await mlsGroup.addMembers([memberKp]);
|
|
69421
|
+
const committedEpoch = Number(mlsGroup.epoch);
|
|
69422
|
+
if (!welcome) {
|
|
69423
|
+
mlsGroup.importState(stateBefore);
|
|
69424
|
+
console.warn(`[SecureChannel] ${a2aLabel}: addMembers produced no Welcome \u2014 rolled back`);
|
|
69425
|
+
} else {
|
|
69426
|
+
const verdict = await this._sendCommitConfirmed({
|
|
69427
|
+
mlsGroup,
|
|
69428
|
+
mlsGroupId: entry.mlsGroupId,
|
|
69429
|
+
label: a2aLabel,
|
|
69430
|
+
commit,
|
|
69431
|
+
epoch: committedEpoch,
|
|
69432
|
+
parent: preHead.ok ? preHead.digest : void 0,
|
|
69433
|
+
memberDeviceIds: this._commitRoster(
|
|
69434
|
+
(entry.participants ?? []).map((p2) => p2.deviceId),
|
|
69435
|
+
[newDeviceId]
|
|
69436
|
+
),
|
|
69437
|
+
stateBefore,
|
|
69438
|
+
inFlight
|
|
69439
|
+
});
|
|
69440
|
+
if (verdict === "confirmed") {
|
|
69441
|
+
const sent = await this._sendFrameConfirmed({
|
|
69442
|
+
event: "mls_welcome",
|
|
69443
|
+
data: {
|
|
69444
|
+
group_id: entry.mlsGroupId,
|
|
69445
|
+
payload: Buffer.from(welcome).toString("hex"),
|
|
69446
|
+
target_device_id: newDeviceId,
|
|
69447
|
+
a2a_channel_id: channelId,
|
|
69448
|
+
target_hub_id: participant.hub_id
|
|
69449
|
+
// observability only
|
|
69450
|
+
}
|
|
69451
|
+
}, `${a2aLabel} welcome`);
|
|
69452
|
+
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
69453
|
+
if (sent) {
|
|
69454
|
+
console.log(`[SecureChannel] Sent MLS Welcome to new participant ${participant.hub_address} (device ${String(newDeviceId).slice(0, 8)})`);
|
|
69455
|
+
} else {
|
|
69456
|
+
console.warn(`[SecureChannel] ${a2aLabel}: Welcome to ${String(newDeviceId).slice(0, 8)} was NOT written \u2014 the participant will request one`);
|
|
69457
|
+
}
|
|
69458
|
+
}
|
|
69424
69459
|
}
|
|
69425
|
-
}
|
|
69426
|
-
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
69427
|
-
console.log(`[SecureChannel] Sent MLS Welcome to new participant ${participant.hub_address} (device ${String(newDeviceId).slice(0, 8)})`);
|
|
69460
|
+
}
|
|
69428
69461
|
}
|
|
69462
|
+
} finally {
|
|
69463
|
+
await this._releaseCommit(entry.mlsGroupId, inFlight, a2aLabel);
|
|
69429
69464
|
}
|
|
69430
69465
|
}
|
|
69431
69466
|
}
|
|
@@ -69902,7 +69937,13 @@ ${messageText}`;
|
|
|
69902
69937
|
messageType: msgData.message_type ?? "text",
|
|
69903
69938
|
priority: msgData.priority ?? "normal",
|
|
69904
69939
|
envelopeVersion: msgData.envelope_version ?? "1.0.0",
|
|
69905
|
-
roomId
|
|
69940
|
+
roomId,
|
|
69941
|
+
// #1223 — the pre-fold text. This DR `message` path also sets `roomId`
|
|
69942
|
+
// and folds an attachment into `emitText`, so without this a room
|
|
69943
|
+
// message that reaches the gate here would fall back to the folded
|
|
69944
|
+
// text (the same bug the MLS room path fixes). `messageText` is the
|
|
69945
|
+
// envelope text before the fold; `emitText` is the folded copy.
|
|
69946
|
+
caption: messageText
|
|
69906
69947
|
};
|
|
69907
69948
|
this.emit("message", emitText, metadata);
|
|
69908
69949
|
Promise.resolve(this.config.onMessage?.(emitText, metadata)).catch((err) => {
|
|
@@ -70581,6 +70622,7 @@ ${baseText}`;
|
|
|
70581
70622
|
await this._refreshRoomRoster(resolvedRoomId);
|
|
70582
70623
|
}
|
|
70583
70624
|
}
|
|
70625
|
+
const caption = messageText;
|
|
70584
70626
|
const addressed = parseMentions(messageText).length > 0;
|
|
70585
70627
|
if (roomAttachment && !addressed) {
|
|
70586
70628
|
this._deferRoomAttachment(resolvedRoomId, roomAttachment);
|
|
@@ -70607,7 +70649,8 @@ ${baseText}`;
|
|
|
70607
70649
|
senderDeviceId,
|
|
70608
70650
|
senderIsAgent,
|
|
70609
70651
|
senderName: senderMember?.displayName || "unknown",
|
|
70610
|
-
roomName: this._persisted?.rooms?.[resolvedRoomId]?.name
|
|
70652
|
+
roomName: this._persisted?.rooms?.[resolvedRoomId]?.name,
|
|
70653
|
+
caption
|
|
70611
70654
|
};
|
|
70612
70655
|
const senderLabel = senderMember?.displayName || "someone";
|
|
70613
70656
|
this._appendHistory("owner", messageText, `room:${resolvedRoomId}`);
|
|
@@ -70616,6 +70659,9 @@ ${baseText}`;
|
|
|
70616
70659
|
senderDeviceId,
|
|
70617
70660
|
senderName: senderLabel,
|
|
70618
70661
|
plaintext: messageText,
|
|
70662
|
+
// #1223 — what the sender typed, before the attachment fold. The
|
|
70663
|
+
// bridges gate on this (falling back to `plaintext` when absent).
|
|
70664
|
+
caption,
|
|
70619
70665
|
messageType,
|
|
70620
70666
|
timestamp: data.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
70621
70667
|
// Native bridge (claude-room-bridge) consumes this to decide reply
|
|
@@ -70955,9 +71001,34 @@ ${baseText}`;
|
|
|
70955
71001
|
*
|
|
70956
71002
|
* Returns the number of requests actually fulfilled.
|
|
70957
71003
|
*/
|
|
71004
|
+
/**
|
|
71005
|
+
* #1226: the roster to submit on a commit frame — the caller's known members,
|
|
71006
|
+
* the devices being admitted, and ALWAYS ourselves — deduplicated, UUIDs
|
|
71007
|
+
* only. Self is non-negotiable: once a roster is non-empty the server
|
|
71008
|
+
* requires the committer in it (#469), and our own head read
|
|
71009
|
+
* (`assert_group_member`) requires it too. So with no device id of our own
|
|
71010
|
+
* this returns `[]` and the frame carries NO roster (the pre-#1226 shape)
|
|
71011
|
+
* rather than a roster that locks us out. Non-UUID entries are dropped:
|
|
71012
|
+
* `known` can come off an inbound Welcome frame, and a malformed id in the
|
|
71013
|
+
* outbound frame is refused by the server (`MEMBER_DEVICE_IDS_MALFORMED`).
|
|
71014
|
+
* @internal exposed for tests
|
|
71015
|
+
*/
|
|
71016
|
+
_commitRoster(known, targets) {
|
|
71017
|
+
if (!this._deviceId) return [];
|
|
71018
|
+
const out = [];
|
|
71019
|
+
const seen = /* @__PURE__ */ new Set();
|
|
71020
|
+
for (const id of [...known ?? [], ...targets, this._deviceId]) {
|
|
71021
|
+
if (typeof id !== "string" || !UUID_RE.test(id) || seen.has(id)) continue;
|
|
71022
|
+
seen.add(id);
|
|
71023
|
+
out.push(id);
|
|
71024
|
+
}
|
|
71025
|
+
return out;
|
|
71026
|
+
}
|
|
70958
71027
|
async _fulfilWelcomeRequests(opts) {
|
|
70959
71028
|
const { mlsGroup, mlsGroupId, label, welcomeFields, targets } = opts;
|
|
70960
71029
|
if (targets.length === 0) return 0;
|
|
71030
|
+
const rosterBefore = this._commitRoster(opts.memberDeviceIds, []);
|
|
71031
|
+
const rosterAfter = this._commitRoster(opts.memberDeviceIds, targets.map((t2) => t2.deviceId));
|
|
70961
71032
|
if (!this._wsIsOpen()) {
|
|
70962
71033
|
console.warn(
|
|
70963
71034
|
`[SecureChannel] ${label}: socket is not OPEN \u2014 NOT advancing the group; ${targets.length} request(s) stay pending for the next pull`
|
|
@@ -70970,9 +71041,16 @@ ${baseText}`;
|
|
|
70970
71041
|
console.warn(
|
|
70971
71042
|
`[SecureChannel] ${label}: server head is at epoch ${preHead.epoch}, we are at ${Number(mlsGroup.epoch)} \u2014 NOT committing (it would be refused, #1184); ${targets.length} request(s) stay pending`
|
|
70972
71043
|
);
|
|
71044
|
+
await this._catchUpAfterPreflightSkip(mlsGroupId, preHead.epoch, Number(mlsGroup.epoch), label);
|
|
70973
71045
|
return 0;
|
|
70974
71046
|
}
|
|
70975
71047
|
let parent2 = preHead.ok ? preHead.digest : void 0;
|
|
71048
|
+
if (this._commitInFlight.has(mlsGroupId)) {
|
|
71049
|
+
console.warn(
|
|
71050
|
+
`[SecureChannel] ${label}: another commit for this group is in flight \u2014 NOT advancing; ${targets.length} request(s) stay pending for the next pull`
|
|
71051
|
+
);
|
|
71052
|
+
return 0;
|
|
71053
|
+
}
|
|
70976
71054
|
const inFlight = { frames: [], sent: [] };
|
|
70977
71055
|
this._commitInFlight.set(mlsGroupId, inFlight);
|
|
70978
71056
|
try {
|
|
@@ -70997,7 +71075,8 @@ ${baseText}`;
|
|
|
70997
71075
|
// only knows the last one, which is why the result carries them.
|
|
70998
71076
|
epoch: Number(c2.epoch),
|
|
70999
71077
|
payload: Buffer.from(c2.commit).toString("hex"),
|
|
71000
|
-
...parent2 ? { parent_digest: parent2 } : {}
|
|
71078
|
+
...parent2 ? { parent_digest: parent2 } : {},
|
|
71079
|
+
...(sentCommits === commits.length - 1 ? rosterAfter : rosterBefore).length > 0 ? { member_device_ids: sentCommits === commits.length - 1 ? rosterAfter : rosterBefore } : {}
|
|
71001
71080
|
}
|
|
71002
71081
|
},
|
|
71003
71082
|
`${label} commit ${sentCommits + 1}/${commits.length}`
|
|
@@ -71145,6 +71224,126 @@ ${baseText}`;
|
|
|
71145
71224
|
return this._ws?.readyState === 1;
|
|
71146
71225
|
}
|
|
71147
71226
|
/** SHA-256 hex of a commit's bytes — its identity on the server (#1175/#1177). */
|
|
71227
|
+
/**
|
|
71228
|
+
* #1225: send ONE commit over the socket and wait for the server's verdict,
|
|
71229
|
+
* with the #1184 machinery — an in-flight record (so a `… commit rejected`
|
|
71230
|
+
* frame is correlated to it and a concurrent commit for the group is held
|
|
71231
|
+
* and replayed), the head poll, and rollback. The three A2A commit sites
|
|
71232
|
+
* (`a2a_participant_joined`, the `/distribute` WS fallback, `_foundA2AGroup`)
|
|
71233
|
+
* used a raw `ws.send` and treated "the socket took it" as "the server
|
|
71234
|
+
* stored it" — two of them then PERSISTED a state the server never saw.
|
|
71235
|
+
*
|
|
71236
|
+
* Returns the verdict. On `refused` / `unsent` the manager has been restored
|
|
71237
|
+
* to `stateBefore` and nothing must be persisted or minted from it.
|
|
71238
|
+
* `unverifiable` (head unreadable on every try AND no refusal) proceeds
|
|
71239
|
+
* UNCHECKED, the same deliberate choice as the fulfil site.
|
|
71240
|
+
*/
|
|
71241
|
+
/**
|
|
71242
|
+
* #1225 (review F1): reserve the single in-flight slot for a group. The
|
|
71243
|
+
* record is what `_noteCommitRefusal` correlates a refusal to and what
|
|
71244
|
+
* `_handleMlsCommit` holds concurrent socket commits behind; a second run on
|
|
71245
|
+
* the same group would clobber both (measured: two overlapping joins → two
|
|
71246
|
+
* commits claiming one epoch, one Welcome instead of two, the manager AHEAD
|
|
71247
|
+
* of the server). Taken BEFORE the commit is generated; released by
|
|
71248
|
+
* `_releaseCommit` after the last step that depends on it.
|
|
71249
|
+
*/
|
|
71250
|
+
/**
|
|
71251
|
+
* A pre-flight skip must not be a dead end. The skip happens because the
|
|
71252
|
+
* server's head is somewhere else; before this the group "caught up through
|
|
71253
|
+
* the normal divergence path", which only runs when a message for the group
|
|
71254
|
+
* arrives — so a group nobody writes to stayed diverged and its pending
|
|
71255
|
+
* Welcome requests were never fulfilled (named in PR #1227). When the server
|
|
71256
|
+
* is AHEAD, replay the missing commits now so the next pull can commit; when
|
|
71257
|
+
* WE are ahead there is nothing to replay (the fork-ahead direction, #972).
|
|
71258
|
+
*/
|
|
71259
|
+
async _catchUpAfterPreflightSkip(mlsGroupId, serverEpoch, localEpoch, label) {
|
|
71260
|
+
if (serverEpoch < localEpoch) {
|
|
71261
|
+
console.warn(`[SecureChannel] ${label}: we are AHEAD of the server (${localEpoch} > ${serverEpoch}) \u2014 nothing to replay`);
|
|
71262
|
+
return;
|
|
71263
|
+
}
|
|
71264
|
+
const fam = this._resolveGroupFamily(mlsGroupId);
|
|
71265
|
+
if (!fam) return;
|
|
71266
|
+
try {
|
|
71267
|
+
const caughtUp = await this._resyncOnDivergence(fam.managerKey, mlsGroupId);
|
|
71268
|
+
console.log(`[SecureChannel] ${label}: catch-up after pre-flight skip ${caughtUp ? "replayed the missing commits" : "could not replay"}`);
|
|
71269
|
+
} catch (err) {
|
|
71270
|
+
console.warn(`[SecureChannel] ${label}: catch-up after pre-flight skip failed:`, err);
|
|
71271
|
+
}
|
|
71272
|
+
}
|
|
71273
|
+
_reserveCommit(groupId, label) {
|
|
71274
|
+
if (this._commitInFlight.has(groupId)) {
|
|
71275
|
+
console.warn(
|
|
71276
|
+
`[SecureChannel] ${label}: another commit for group ${groupId.slice(0, 8)} is in flight \u2014 NOT advancing the group (a Welcome can be requested again)`
|
|
71277
|
+
);
|
|
71278
|
+
return null;
|
|
71279
|
+
}
|
|
71280
|
+
const rec = { frames: [], sent: [] };
|
|
71281
|
+
this._commitInFlight.set(groupId, rec);
|
|
71282
|
+
return rec;
|
|
71283
|
+
}
|
|
71284
|
+
/** Release the slot taken by `_reserveCommit` and replay the socket commits held meanwhile. */
|
|
71285
|
+
async _releaseCommit(groupId, rec, label) {
|
|
71286
|
+
if (this._commitInFlight.get(groupId) === rec) this._commitInFlight.delete(groupId);
|
|
71287
|
+
for (const frame of rec.frames) {
|
|
71288
|
+
try {
|
|
71289
|
+
await this._handleMlsCommit(frame);
|
|
71290
|
+
} catch (err) {
|
|
71291
|
+
console.error(`[SecureChannel] ${label}: replaying a held MLS commit failed:`, err);
|
|
71292
|
+
}
|
|
71293
|
+
}
|
|
71294
|
+
}
|
|
71295
|
+
/**
|
|
71296
|
+
* #1225: send ONE commit over the socket and wait for the server's verdict
|
|
71297
|
+
* with the #1184 machinery (refusal frame correlated through the reserved
|
|
71298
|
+
* in-flight record, head poll). The three A2A commit sites
|
|
71299
|
+
* (`a2a_participant_joined`, the `/distribute` WS fallback, `_foundA2AGroup`)
|
|
71300
|
+
* used a raw `ws.send` and treated "the socket took it" as "the server
|
|
71301
|
+
* stored it" — two of them then PERSISTED a state the server never saw.
|
|
71302
|
+
*
|
|
71303
|
+
* Only `confirmed` proceeds. `unverifiable` (head unreadable on every try,
|
|
71304
|
+
* no refusal) is a ROLLBACK here, unlike the fulfil site: that site proceeds
|
|
71305
|
+
* UNCHECKED because 79 of 80 production rooms 403 the head read on an empty
|
|
71306
|
+
* roster; an A2A head read admits any active participant (observer
|
|
71307
|
+
* fallback), so five failed reads there mean an outage, not a routine 403.
|
|
71308
|
+
* On anything but `confirmed` the manager is back at `stateBefore`.
|
|
71309
|
+
*/
|
|
71310
|
+
async _sendCommitConfirmed(opts) {
|
|
71311
|
+
const { mlsGroup, mlsGroupId, label, commit, epoch, parent: parent2, memberDeviceIds, stateBefore, inFlight } = opts;
|
|
71312
|
+
const ours = this._commitDigest(commit);
|
|
71313
|
+
inFlight.sent.push({ epoch, digest: ours, verified: false, stateBefore });
|
|
71314
|
+
const ok = await this._sendFrameConfirmed(
|
|
71315
|
+
{
|
|
71316
|
+
event: "mls_commit",
|
|
71317
|
+
data: {
|
|
71318
|
+
group_id: mlsGroupId,
|
|
71319
|
+
epoch,
|
|
71320
|
+
payload: Buffer.from(commit).toString("hex"),
|
|
71321
|
+
...parent2 ? { parent_digest: parent2 } : {},
|
|
71322
|
+
...memberDeviceIds && memberDeviceIds.length > 0 ? { member_device_ids: memberDeviceIds } : {}
|
|
71323
|
+
}
|
|
71324
|
+
},
|
|
71325
|
+
`${label} commit`
|
|
71326
|
+
);
|
|
71327
|
+
if (!ok) {
|
|
71328
|
+
inFlight.sent.pop();
|
|
71329
|
+
mlsGroup.importState(stateBefore);
|
|
71330
|
+
console.warn(
|
|
71331
|
+
`[SecureChannel] ${label}: commit at epoch ${epoch} was NOT written \u2014 rolled back to epoch ${Number(mlsGroup.epoch)}`
|
|
71332
|
+
);
|
|
71333
|
+
return "unsent";
|
|
71334
|
+
}
|
|
71335
|
+
const verdict = await this._confirmCommitAtHead(mlsGroupId, epoch, ours, inFlight, label);
|
|
71336
|
+
if (inFlight.refusal === void 0 && verdict === "confirmed") {
|
|
71337
|
+
inFlight.sent[inFlight.sent.length - 1].verified = true;
|
|
71338
|
+
return "confirmed";
|
|
71339
|
+
}
|
|
71340
|
+
const why = inFlight.refusal?.detail ?? verdict.toUpperCase();
|
|
71341
|
+
mlsGroup.importState(stateBefore);
|
|
71342
|
+
console.warn(
|
|
71343
|
+
`[SecureChannel] ${label}: commit at epoch ${epoch} was ${verdict === "unverifiable" ? "not verified (head unreadable)" : "refused by the server"} \u2014 "${why}" \u2014 rolled back to epoch ${Number(mlsGroup.epoch)}, nothing persisted`
|
|
71344
|
+
);
|
|
71345
|
+
return "refused";
|
|
71346
|
+
}
|
|
71148
71347
|
_commitDigest(bytes) {
|
|
71149
71348
|
return createHash("sha256").update(bytes).digest("hex");
|
|
71150
71349
|
}
|
|
@@ -72098,7 +72297,9 @@ ${baseText}`;
|
|
|
72098
72297
|
mlsGroupId: roomState.mlsGroupId,
|
|
72099
72298
|
label: `room ${roomId.slice(0, 8)}`,
|
|
72100
72299
|
welcomeFields: { room_id: roomId },
|
|
72101
|
-
targets: roomTargets
|
|
72300
|
+
targets: roomTargets,
|
|
72301
|
+
memberDeviceIds: (roomState.members ?? []).map((m2) => m2.deviceId)
|
|
72302
|
+
// #1226
|
|
72102
72303
|
});
|
|
72103
72304
|
if (done > 0) {
|
|
72104
72305
|
console.log(
|
|
@@ -72153,7 +72354,9 @@ ${baseText}`;
|
|
|
72153
72354
|
mlsGroupId: entry.mlsGroupId,
|
|
72154
72355
|
label: `shared group ${gid.slice(0, 8)}`,
|
|
72155
72356
|
welcomeFields: { conversation_group_id: gid },
|
|
72156
|
-
targets: sharedTargets
|
|
72357
|
+
targets: sharedTargets,
|
|
72358
|
+
memberDeviceIds: entry.memberDeviceIds ?? []
|
|
72359
|
+
// #1226
|
|
72157
72360
|
});
|
|
72158
72361
|
if (done > 0) {
|
|
72159
72362
|
console.log(`[SecureChannel] Batched Welcome for ${done}/${validReqs.length} devices in shared group ${gid.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
|
|
@@ -72167,7 +72370,9 @@ ${baseText}`;
|
|
|
72167
72370
|
mlsGroupId: entry.mlsGroupId,
|
|
72168
72371
|
label: `shared group ${gid.slice(0, 8)}`,
|
|
72169
72372
|
welcomeFields: { conversation_group_id: gid },
|
|
72170
|
-
targets: [target]
|
|
72373
|
+
targets: [target],
|
|
72374
|
+
memberDeviceIds: entry.memberDeviceIds ?? []
|
|
72375
|
+
// #1226
|
|
72171
72376
|
});
|
|
72172
72377
|
if (done > 0) {
|
|
72173
72378
|
console.log(`[SecureChannel] Individual Welcome for ${target.deviceId.slice(0, 8)} in shared group ${gid.slice(0, 8)}`);
|
|
@@ -72214,7 +72419,11 @@ ${baseText}`;
|
|
|
72214
72419
|
requestId: req.id,
|
|
72215
72420
|
deviceId: req.requesting_device_id,
|
|
72216
72421
|
keyPackage: memberKp
|
|
72217
|
-
}]
|
|
72422
|
+
}],
|
|
72423
|
+
// #1226: a 1:1 roster is [self, owner] — the web's shape too.
|
|
72424
|
+
memberDeviceIds: [this._persisted?.sessions?.[convId]?.ownerDeviceId].filter(
|
|
72425
|
+
(d2) => typeof d2 === "string"
|
|
72426
|
+
)
|
|
72218
72427
|
});
|
|
72219
72428
|
if (done > 0) {
|
|
72220
72429
|
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in conv ${convId.slice(0, 8)}`);
|
|
@@ -72272,192 +72481,8 @@ ${baseText}`;
|
|
|
72272
72481
|
console.log(`[SecureChannel] ${pending.length} pending A2A Welcome requests for channel ${chId.slice(0, 8)}`);
|
|
72273
72482
|
for (const req of pending) {
|
|
72274
72483
|
try {
|
|
72275
|
-
const
|
|
72276
|
-
|
|
72277
|
-
{ headers: { Authorization: `Bearer ${this._deviceJwt}` } }
|
|
72278
|
-
);
|
|
72279
|
-
if (!kpRes.ok) continue;
|
|
72280
|
-
const kpData = await kpRes.json();
|
|
72281
|
-
const kpHex = kpData[req.requesting_device_id];
|
|
72282
|
-
if (!kpHex) continue;
|
|
72283
|
-
const kpBytes = new Uint8Array(Buffer.from(kpHex, "hex"));
|
|
72284
|
-
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
72285
|
-
console.log(`[SecureChannel] A2A addMembers for ${req.requesting_device_id.slice(0, 8)} group=${chState.mlsGroupId.slice(0, 8)} epoch=${mlsGroup.epoch}`);
|
|
72286
|
-
let parentDigest = await this._readGroupHeadDigest(chState.mlsGroupId);
|
|
72287
|
-
let commit;
|
|
72288
|
-
let welcome;
|
|
72289
|
-
try {
|
|
72290
|
-
const result = await mlsGroup.addMembers([memberKp]);
|
|
72291
|
-
commit = result.commit;
|
|
72292
|
-
welcome = result.welcome;
|
|
72293
|
-
} catch (addErr) {
|
|
72294
|
-
const errMsg = addErr instanceof Error ? addErr.message : String(addErr);
|
|
72295
|
-
if (errMsg.includes("already in the group")) {
|
|
72296
|
-
console.log(`[SecureChannel] A2A member already in group \u2014 removing and re-adding ${req.requesting_device_id.slice(0, 8)}`);
|
|
72297
|
-
const preRemoveState = JSON.stringify(mlsGroup.exportState());
|
|
72298
|
-
let removeCommitPublished = false;
|
|
72299
|
-
try {
|
|
72300
|
-
const identityBytes = new TextEncoder().encode(req.requesting_device_id);
|
|
72301
|
-
const leafIndex = mlsGroup.memberLeafIndex(identityBytes);
|
|
72302
|
-
if (leafIndex === null) {
|
|
72303
|
-
console.log(`[SecureChannel] A2A re-add: no leaf index for ${req.requesting_device_id.slice(0, 8)} \u2014 cannot remove`);
|
|
72304
|
-
continue;
|
|
72305
|
-
}
|
|
72306
|
-
const removeResult = await mlsGroup.removeMember(leafIndex);
|
|
72307
|
-
try {
|
|
72308
|
-
const removeResp = await fetch(
|
|
72309
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${chState.mlsGroupId}/distribute`,
|
|
72310
|
-
{
|
|
72311
|
-
method: "POST",
|
|
72312
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
72313
|
-
body: JSON.stringify({
|
|
72314
|
-
group_id: chState.mlsGroupId,
|
|
72315
|
-
device_id: this._deviceId,
|
|
72316
|
-
commit: Buffer.from(removeResult.commit).toString("hex"),
|
|
72317
|
-
commit_epoch: Number(mlsGroup.epoch),
|
|
72318
|
-
...parentDigest ? { parent_digest: parentDigest } : {}
|
|
72319
|
-
})
|
|
72320
|
-
}
|
|
72321
|
-
);
|
|
72322
|
-
if (removeResp.ok) {
|
|
72323
|
-
removeCommitPublished = true;
|
|
72324
|
-
parentDigest = this._commitDigest(removeResult.commit);
|
|
72325
|
-
} else {
|
|
72326
|
-
const detail = await removeResp.text().catch(() => "");
|
|
72327
|
-
console.warn(`[SecureChannel] A2A remove-commit distribute failed (${removeResp.status}): ${detail}`);
|
|
72328
|
-
}
|
|
72329
|
-
} catch (distErr) {
|
|
72330
|
-
console.warn(`[SecureChannel] A2A remove-commit distribute network error:`, distErr);
|
|
72331
|
-
}
|
|
72332
|
-
if (!removeCommitPublished) {
|
|
72333
|
-
mlsGroup.importState(JSON.parse(preRemoveState));
|
|
72334
|
-
continue;
|
|
72335
|
-
}
|
|
72336
|
-
const kpRes2 = await fetch(
|
|
72337
|
-
`${this.config.apiUrl}/api/v1/mls/key-packages/batch?device_ids=${req.requesting_device_id}`,
|
|
72338
|
-
{ headers: { Authorization: `Bearer ${this._deviceJwt}` } }
|
|
72339
|
-
);
|
|
72340
|
-
if (kpRes2.ok) {
|
|
72341
|
-
const kpData2 = await kpRes2.json();
|
|
72342
|
-
const kpHex2 = kpData2[req.requesting_device_id];
|
|
72343
|
-
if (kpHex2) {
|
|
72344
|
-
const kpBytes2 = new Uint8Array(Buffer.from(kpHex2, "hex"));
|
|
72345
|
-
const memberKp2 = MLSGroupManager.deserializeKeyPackage(kpBytes2);
|
|
72346
|
-
const result2 = await mlsGroup.addMembers([memberKp2]);
|
|
72347
|
-
commit = result2.commit;
|
|
72348
|
-
welcome = result2.welcome;
|
|
72349
|
-
} else {
|
|
72350
|
-
console.log(`[SecureChannel] A2A re-add: no fresh KeyPackage for ${req.requesting_device_id.slice(0, 8)}`);
|
|
72351
|
-
continue;
|
|
72352
|
-
}
|
|
72353
|
-
} else {
|
|
72354
|
-
console.log(`[SecureChannel] A2A re-add: KeyPackage fetch failed (${kpRes2.status})`);
|
|
72355
|
-
continue;
|
|
72356
|
-
}
|
|
72357
|
-
} catch (readdErr) {
|
|
72358
|
-
console.log(`[SecureChannel] A2A remove+re-add FAILED: ${readdErr instanceof Error ? readdErr.message : String(readdErr)}`);
|
|
72359
|
-
if (!removeCommitPublished) {
|
|
72360
|
-
try {
|
|
72361
|
-
mlsGroup.importState(JSON.parse(preRemoveState));
|
|
72362
|
-
} catch {
|
|
72363
|
-
}
|
|
72364
|
-
}
|
|
72365
|
-
continue;
|
|
72366
|
-
}
|
|
72367
|
-
} else {
|
|
72368
|
-
console.log(`[SecureChannel] A2A addMembers FAILED: ${errMsg}`);
|
|
72369
|
-
continue;
|
|
72370
|
-
}
|
|
72371
|
-
}
|
|
72372
|
-
if (!welcome) {
|
|
72373
|
-
console.warn(`[SecureChannel] A2A addMembers produced no Welcome for ${req.requesting_device_id.slice(0, 8)}`);
|
|
72374
|
-
continue;
|
|
72375
|
-
}
|
|
72376
|
-
const commitHex = Buffer.from(commit).toString("hex");
|
|
72377
|
-
const welcomeHex = Buffer.from(welcome).toString("hex");
|
|
72378
|
-
let distributed = false;
|
|
72379
|
-
const updatedMembers = [this._deviceId, req.requesting_device_id];
|
|
72380
|
-
try {
|
|
72381
|
-
const distResp = await fetch(
|
|
72382
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${chState.mlsGroupId}/distribute`,
|
|
72383
|
-
{
|
|
72384
|
-
method: "POST",
|
|
72385
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
72386
|
-
body: JSON.stringify({
|
|
72387
|
-
group_id: chState.mlsGroupId,
|
|
72388
|
-
device_id: this._deviceId,
|
|
72389
|
-
commit: commitHex,
|
|
72390
|
-
commit_epoch: Number(mlsGroup.epoch),
|
|
72391
|
-
welcomes: [{ target_device_id: req.requesting_device_id, payload: welcomeHex }],
|
|
72392
|
-
member_device_ids: updatedMembers,
|
|
72393
|
-
...parentDigest ? { parent_digest: parentDigest } : {}
|
|
72394
|
-
})
|
|
72395
|
-
}
|
|
72396
|
-
);
|
|
72397
|
-
if (distResp.ok) {
|
|
72398
|
-
distributed = true;
|
|
72399
|
-
} else {
|
|
72400
|
-
const detail = await distResp.text().catch(() => "");
|
|
72401
|
-
console.warn(`[SecureChannel] A2A distribute failed (${distResp.status}): ${detail}`);
|
|
72402
|
-
}
|
|
72403
|
-
} catch (distErr) {
|
|
72404
|
-
console.warn(`[SecureChannel] A2A distribute network error:`, distErr);
|
|
72405
|
-
}
|
|
72406
|
-
if (!distributed && this._wsIsOpen()) {
|
|
72407
|
-
const a2aLabel = `A2A ${chId.slice(0, 8)}`;
|
|
72408
|
-
const commitSent = await this._sendFrameConfirmed({
|
|
72409
|
-
event: "mls_commit",
|
|
72410
|
-
data: {
|
|
72411
|
-
group_id: chState.mlsGroupId,
|
|
72412
|
-
epoch: Number(mlsGroup.epoch),
|
|
72413
|
-
payload: commitHex,
|
|
72414
|
-
...parentDigest ? { parent_digest: parentDigest } : {}
|
|
72415
|
-
}
|
|
72416
|
-
}, `${a2aLabel} commit`);
|
|
72417
|
-
const welcomeSent = commitSent && await this._sendFrameConfirmed({
|
|
72418
|
-
event: "mls_welcome",
|
|
72419
|
-
data: {
|
|
72420
|
-
target_device_id: req.requesting_device_id,
|
|
72421
|
-
group_id: chState.mlsGroupId,
|
|
72422
|
-
a2a_channel_id: chId,
|
|
72423
|
-
payload: welcomeHex
|
|
72424
|
-
}
|
|
72425
|
-
}, `${a2aLabel} welcome`);
|
|
72426
|
-
if (!welcomeSent) {
|
|
72427
|
-
console.warn(`[SecureChannel] ${a2aLabel}: Welcome frames were NOT written \u2014 leaving the request pending`);
|
|
72428
|
-
continue;
|
|
72429
|
-
}
|
|
72430
|
-
try {
|
|
72431
|
-
await fetch(
|
|
72432
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${chState.mlsGroupId}/distribute`,
|
|
72433
|
-
{
|
|
72434
|
-
method: "POST",
|
|
72435
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
72436
|
-
body: JSON.stringify({
|
|
72437
|
-
group_id: chState.mlsGroupId,
|
|
72438
|
-
device_id: this._deviceId,
|
|
72439
|
-
commit_epoch: Number(mlsGroup.epoch),
|
|
72440
|
-
member_device_ids: updatedMembers
|
|
72441
|
-
})
|
|
72442
|
-
}
|
|
72443
|
-
);
|
|
72444
|
-
} catch {
|
|
72445
|
-
}
|
|
72446
|
-
distributed = true;
|
|
72447
|
-
}
|
|
72448
|
-
if (!distributed) {
|
|
72449
|
-
console.warn(`[SecureChannel] Cannot deliver Welcome \u2014 HTTP distribute and WS both unavailable`);
|
|
72450
|
-
continue;
|
|
72451
|
-
}
|
|
72452
|
-
const a2aFulfilled = await this._postFulfilWelcome(
|
|
72453
|
-
chState.mlsGroupId,
|
|
72454
|
-
req.id,
|
|
72455
|
-
`A2A ${chId.slice(0, 8)}`
|
|
72456
|
-
);
|
|
72457
|
-
await saveMlsState(this.config.dataDir, chState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
72458
|
-
if (a2aFulfilled) {
|
|
72459
|
-
console.log(`[SecureChannel] Fulfilled A2A Welcome for ${req.requesting_device_id.slice(0, 8)} in channel ${chId.slice(0, 8)}`);
|
|
72460
|
-
}
|
|
72484
|
+
const next = await this._fulfilA2AWelcome(chId, chState, mlsGroup, req);
|
|
72485
|
+
if (next === "stop") break;
|
|
72461
72486
|
} catch (fulfillErr) {
|
|
72462
72487
|
console.log(`[SecureChannel] A2A Welcome fulfill failed: ${fulfillErr instanceof Error ? fulfillErr.message : String(fulfillErr)}`);
|
|
72463
72488
|
}
|
|
@@ -72728,6 +72753,227 @@ ${baseText}`;
|
|
|
72728
72753
|
}
|
|
72729
72754
|
}
|
|
72730
72755
|
}
|
|
72756
|
+
/**
|
|
72757
|
+
* #1225: fulfil ONE pending A2A Welcome request — the `/distribute` path.
|
|
72758
|
+
* Extracted from the delivery pull so the in-flight slot (review F1) is held
|
|
72759
|
+
* for exactly this request and released on every exit. Returns "stop" when
|
|
72760
|
+
* nothing else on this channel can succeed this pull (review F2).
|
|
72761
|
+
*/
|
|
72762
|
+
async _fulfilA2AWelcome(chId, chState, mlsGroup, req) {
|
|
72763
|
+
const a2aLabel = `A2A ${chId.slice(0, 8)}`;
|
|
72764
|
+
const preHead = await this._readGroupHead(chState.mlsGroupId);
|
|
72765
|
+
if (preHead.ok && preHead.epoch !== Number(mlsGroup.epoch)) {
|
|
72766
|
+
console.warn(
|
|
72767
|
+
`[SecureChannel] ${a2aLabel}: server head is at epoch ${preHead.epoch}, we are at ${Number(mlsGroup.epoch)} \u2014 NOT committing (it would be refused); resync catches up first`
|
|
72768
|
+
);
|
|
72769
|
+
await this._catchUpAfterPreflightSkip(chState.mlsGroupId, preHead.epoch, Number(mlsGroup.epoch), a2aLabel);
|
|
72770
|
+
return "stop";
|
|
72771
|
+
}
|
|
72772
|
+
const inFlight = this._reserveCommit(chState.mlsGroupId, a2aLabel);
|
|
72773
|
+
if (!inFlight) return "stop";
|
|
72774
|
+
try {
|
|
72775
|
+
const kpRes = await fetch(
|
|
72776
|
+
`${this.config.apiUrl}/api/v1/mls/key-packages/batch?device_ids=${req.requesting_device_id}`,
|
|
72777
|
+
{ headers: { Authorization: `Bearer ${this._deviceJwt}` } }
|
|
72778
|
+
);
|
|
72779
|
+
if (!kpRes.ok) return "next";
|
|
72780
|
+
const kpData = await kpRes.json();
|
|
72781
|
+
const kpHex = kpData[req.requesting_device_id];
|
|
72782
|
+
if (!kpHex) return "next";
|
|
72783
|
+
const kpBytes = new Uint8Array(Buffer.from(kpHex, "hex"));
|
|
72784
|
+
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
72785
|
+
console.log(`[SecureChannel] A2A addMembers for ${req.requesting_device_id.slice(0, 8)} group=${chState.mlsGroupId.slice(0, 8)} epoch=${mlsGroup.epoch}`);
|
|
72786
|
+
let parentDigest = preHead.ok ? preHead.digest : void 0;
|
|
72787
|
+
let commit;
|
|
72788
|
+
let welcome;
|
|
72789
|
+
let preAddState = mlsGroup.exportState();
|
|
72790
|
+
try {
|
|
72791
|
+
const result = await mlsGroup.addMembers([memberKp]);
|
|
72792
|
+
commit = result.commit;
|
|
72793
|
+
welcome = result.welcome;
|
|
72794
|
+
} catch (addErr) {
|
|
72795
|
+
const errMsg = addErr instanceof Error ? addErr.message : String(addErr);
|
|
72796
|
+
if (errMsg.includes("already in the group")) {
|
|
72797
|
+
console.log(`[SecureChannel] A2A member already in group \u2014 removing and re-adding ${req.requesting_device_id.slice(0, 8)}`);
|
|
72798
|
+
const preRemoveState = JSON.stringify(mlsGroup.exportState());
|
|
72799
|
+
let removeCommitPublished = false;
|
|
72800
|
+
try {
|
|
72801
|
+
const identityBytes = new TextEncoder().encode(req.requesting_device_id);
|
|
72802
|
+
const leafIndex = mlsGroup.memberLeafIndex(identityBytes);
|
|
72803
|
+
if (leafIndex === null) {
|
|
72804
|
+
console.log(`[SecureChannel] A2A re-add: no leaf index for ${req.requesting_device_id.slice(0, 8)} \u2014 cannot remove`);
|
|
72805
|
+
return "next";
|
|
72806
|
+
}
|
|
72807
|
+
const removeResult = await mlsGroup.removeMember(leafIndex);
|
|
72808
|
+
try {
|
|
72809
|
+
const removeResp = await fetch(
|
|
72810
|
+
`${this.config.apiUrl}/api/v1/mls/groups/${chState.mlsGroupId}/distribute`,
|
|
72811
|
+
{
|
|
72812
|
+
method: "POST",
|
|
72813
|
+
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
72814
|
+
body: JSON.stringify({
|
|
72815
|
+
group_id: chState.mlsGroupId,
|
|
72816
|
+
device_id: this._deviceId,
|
|
72817
|
+
commit: Buffer.from(removeResult.commit).toString("hex"),
|
|
72818
|
+
commit_epoch: Number(mlsGroup.epoch),
|
|
72819
|
+
expected_epoch: Number(mlsGroup.epoch) - 1,
|
|
72820
|
+
// #1225: exact-step, like the WS path
|
|
72821
|
+
...parentDigest ? { parent_digest: parentDigest } : {}
|
|
72822
|
+
})
|
|
72823
|
+
}
|
|
72824
|
+
);
|
|
72825
|
+
if (removeResp.ok) {
|
|
72826
|
+
removeCommitPublished = true;
|
|
72827
|
+
await saveMlsState(this.config.dataDir, chState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
72828
|
+
parentDigest = this._commitDigest(removeResult.commit);
|
|
72829
|
+
} else {
|
|
72830
|
+
const detail = await removeResp.text().catch(() => "");
|
|
72831
|
+
console.warn(`[SecureChannel] A2A remove-commit distribute failed (${removeResp.status}): ${detail}`);
|
|
72832
|
+
}
|
|
72833
|
+
} catch (distErr) {
|
|
72834
|
+
console.warn(`[SecureChannel] A2A remove-commit distribute network error:`, distErr);
|
|
72835
|
+
}
|
|
72836
|
+
if (!removeCommitPublished) {
|
|
72837
|
+
mlsGroup.importState(JSON.parse(preRemoveState));
|
|
72838
|
+
return "next";
|
|
72839
|
+
}
|
|
72840
|
+
const kpRes2 = await fetch(
|
|
72841
|
+
`${this.config.apiUrl}/api/v1/mls/key-packages/batch?device_ids=${req.requesting_device_id}`,
|
|
72842
|
+
{ headers: { Authorization: `Bearer ${this._deviceJwt}` } }
|
|
72843
|
+
);
|
|
72844
|
+
if (kpRes2.ok) {
|
|
72845
|
+
const kpData2 = await kpRes2.json();
|
|
72846
|
+
const kpHex2 = kpData2[req.requesting_device_id];
|
|
72847
|
+
if (kpHex2) {
|
|
72848
|
+
const kpBytes2 = new Uint8Array(Buffer.from(kpHex2, "hex"));
|
|
72849
|
+
const memberKp2 = MLSGroupManager.deserializeKeyPackage(kpBytes2);
|
|
72850
|
+
preAddState = mlsGroup.exportState();
|
|
72851
|
+
const result2 = await mlsGroup.addMembers([memberKp2]);
|
|
72852
|
+
commit = result2.commit;
|
|
72853
|
+
welcome = result2.welcome;
|
|
72854
|
+
} else {
|
|
72855
|
+
console.log(`[SecureChannel] A2A re-add: no fresh KeyPackage for ${req.requesting_device_id.slice(0, 8)}`);
|
|
72856
|
+
return "next";
|
|
72857
|
+
}
|
|
72858
|
+
} else {
|
|
72859
|
+
console.log(`[SecureChannel] A2A re-add: KeyPackage fetch failed (${kpRes2.status})`);
|
|
72860
|
+
return "next";
|
|
72861
|
+
}
|
|
72862
|
+
} catch (readdErr) {
|
|
72863
|
+
console.log(`[SecureChannel] A2A remove+re-add FAILED: ${readdErr instanceof Error ? readdErr.message : String(readdErr)}`);
|
|
72864
|
+
if (!removeCommitPublished) {
|
|
72865
|
+
try {
|
|
72866
|
+
mlsGroup.importState(JSON.parse(preRemoveState));
|
|
72867
|
+
} catch {
|
|
72868
|
+
}
|
|
72869
|
+
}
|
|
72870
|
+
return "next";
|
|
72871
|
+
}
|
|
72872
|
+
} else {
|
|
72873
|
+
console.log(`[SecureChannel] A2A addMembers FAILED: ${errMsg}`);
|
|
72874
|
+
return "next";
|
|
72875
|
+
}
|
|
72876
|
+
}
|
|
72877
|
+
if (!welcome) {
|
|
72878
|
+
console.warn(`[SecureChannel] A2A addMembers produced no Welcome for ${req.requesting_device_id.slice(0, 8)}`);
|
|
72879
|
+
return "next";
|
|
72880
|
+
}
|
|
72881
|
+
const commitHex = Buffer.from(commit).toString("hex");
|
|
72882
|
+
const welcomeHex = Buffer.from(welcome).toString("hex");
|
|
72883
|
+
let distributed = false;
|
|
72884
|
+
let transportFailed = false;
|
|
72885
|
+
const updatedMembers = this._commitRoster([this._deviceId], [req.requesting_device_id]);
|
|
72886
|
+
try {
|
|
72887
|
+
const distResp = await fetch(
|
|
72888
|
+
`${this.config.apiUrl}/api/v1/mls/groups/${chState.mlsGroupId}/distribute`,
|
|
72889
|
+
{
|
|
72890
|
+
method: "POST",
|
|
72891
|
+
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
72892
|
+
body: JSON.stringify({
|
|
72893
|
+
group_id: chState.mlsGroupId,
|
|
72894
|
+
device_id: this._deviceId,
|
|
72895
|
+
commit: commitHex,
|
|
72896
|
+
commit_epoch: Number(mlsGroup.epoch),
|
|
72897
|
+
welcomes: [{ target_device_id: req.requesting_device_id, payload: welcomeHex }],
|
|
72898
|
+
member_device_ids: updatedMembers,
|
|
72899
|
+
expected_epoch: Number(mlsGroup.epoch) - 1,
|
|
72900
|
+
// #1225: exact-step, like the WS path
|
|
72901
|
+
...parentDigest ? { parent_digest: parentDigest } : {}
|
|
72902
|
+
})
|
|
72903
|
+
}
|
|
72904
|
+
);
|
|
72905
|
+
if (distResp.ok) {
|
|
72906
|
+
distributed = true;
|
|
72907
|
+
} else {
|
|
72908
|
+
const detail = await distResp.text().catch(() => "");
|
|
72909
|
+
console.warn(`[SecureChannel] A2A distribute failed (${distResp.status}): ${detail}`);
|
|
72910
|
+
if (distResp.status >= 500) {
|
|
72911
|
+
transportFailed = true;
|
|
72912
|
+
} else if (distResp.status === 409) {
|
|
72913
|
+
mlsGroup.importState(preAddState);
|
|
72914
|
+
console.warn(
|
|
72915
|
+
`[SecureChannel] ${a2aLabel}: add commit lost the race (409) \u2014 rolled back to epoch ${Number(mlsGroup.epoch)}, request stays pending`
|
|
72916
|
+
);
|
|
72917
|
+
return "next";
|
|
72918
|
+
} else {
|
|
72919
|
+
mlsGroup.importState(preAddState);
|
|
72920
|
+
console.error(
|
|
72921
|
+
`[SecureChannel] ${a2aLabel}: add commit refused with ${distResp.status} \u2014 rolled back to epoch ${Number(mlsGroup.epoch)}; not retrying this channel this pull`
|
|
72922
|
+
);
|
|
72923
|
+
return "stop";
|
|
72924
|
+
}
|
|
72925
|
+
}
|
|
72926
|
+
} catch (distErr) {
|
|
72927
|
+
console.warn(`[SecureChannel] A2A distribute network error:`, distErr);
|
|
72928
|
+
transportFailed = true;
|
|
72929
|
+
}
|
|
72930
|
+
if (!distributed && transportFailed && this._wsIsOpen()) {
|
|
72931
|
+
const verdict = await this._sendCommitConfirmed({
|
|
72932
|
+
mlsGroup,
|
|
72933
|
+
mlsGroupId: chState.mlsGroupId,
|
|
72934
|
+
label: a2aLabel,
|
|
72935
|
+
commit,
|
|
72936
|
+
epoch: Number(mlsGroup.epoch),
|
|
72937
|
+
parent: parentDigest,
|
|
72938
|
+
memberDeviceIds: updatedMembers,
|
|
72939
|
+
stateBefore: preAddState,
|
|
72940
|
+
inFlight
|
|
72941
|
+
});
|
|
72942
|
+
if (verdict !== "confirmed") return "next";
|
|
72943
|
+
const welcomeSent = await this._sendFrameConfirmed({
|
|
72944
|
+
event: "mls_welcome",
|
|
72945
|
+
data: {
|
|
72946
|
+
target_device_id: req.requesting_device_id,
|
|
72947
|
+
group_id: chState.mlsGroupId,
|
|
72948
|
+
a2a_channel_id: chId,
|
|
72949
|
+
payload: welcomeHex
|
|
72950
|
+
}
|
|
72951
|
+
}, `${a2aLabel} welcome`);
|
|
72952
|
+
if (!welcomeSent) {
|
|
72953
|
+
await saveMlsState(this.config.dataDir, chState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
72954
|
+
console.warn(`[SecureChannel] ${a2aLabel}: Welcome frame was NOT written \u2014 leaving the request pending`);
|
|
72955
|
+
return "next";
|
|
72956
|
+
}
|
|
72957
|
+
distributed = true;
|
|
72958
|
+
}
|
|
72959
|
+
if (!distributed) {
|
|
72960
|
+
console.warn(`[SecureChannel] Cannot deliver Welcome \u2014 HTTP distribute and WS both unavailable`);
|
|
72961
|
+
return "next";
|
|
72962
|
+
}
|
|
72963
|
+
const a2aFulfilled = await this._postFulfilWelcome(
|
|
72964
|
+
chState.mlsGroupId,
|
|
72965
|
+
req.id,
|
|
72966
|
+
`A2A ${chId.slice(0, 8)}`
|
|
72967
|
+
);
|
|
72968
|
+
await saveMlsState(this.config.dataDir, chState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
72969
|
+
if (a2aFulfilled) {
|
|
72970
|
+
console.log(`[SecureChannel] Fulfilled A2A Welcome for ${req.requesting_device_id.slice(0, 8)} in channel ${chId.slice(0, 8)}`);
|
|
72971
|
+
}
|
|
72972
|
+
return "next";
|
|
72973
|
+
} finally {
|
|
72974
|
+
await this._releaseCommit(chState.mlsGroupId, inFlight, a2aLabel);
|
|
72975
|
+
}
|
|
72976
|
+
}
|
|
72731
72977
|
/**
|
|
72732
72978
|
* Found the MLS group for an A2A channel as the creator agent.
|
|
72733
72979
|
* Calls /mls/a2a/{channelId}/init, creates the local MLS group, and sends
|
|
@@ -72770,48 +73016,77 @@ ${baseText}`;
|
|
|
72770
73016
|
await saveMlsState(this.config.dataDir, mlsData.group_id, JSON.stringify(mgr.exportState()));
|
|
72771
73017
|
for (const p2 of participants) {
|
|
72772
73018
|
if (p2.status !== "active" && p2.status !== "invited") continue;
|
|
73019
|
+
const a2aLabel = `A2A ${channelId.slice(0, 8)} found\u2192${p2.hubAddress}`;
|
|
72773
73020
|
try {
|
|
72774
|
-
|
|
72775
|
-
|
|
72776
|
-
|
|
72777
|
-
|
|
72778
|
-
|
|
72779
|
-
|
|
72780
|
-
|
|
72781
|
-
|
|
72782
|
-
|
|
72783
|
-
|
|
72784
|
-
|
|
72785
|
-
|
|
72786
|
-
|
|
73021
|
+
if (!p2.deviceId) {
|
|
73022
|
+
console.warn(
|
|
73023
|
+
`[SecureChannel] ${a2aLabel}: no deviceId \u2014 NOT adding (a Welcome could not be addressed). Backend a2a_channel_approved payload may be missing device_id for this participant.`
|
|
73024
|
+
);
|
|
73025
|
+
continue;
|
|
73026
|
+
}
|
|
73027
|
+
if (!this._wsIsOpen()) {
|
|
73028
|
+
console.warn(`[SecureChannel] ${a2aLabel}: socket is not OPEN \u2014 NOT advancing the group`);
|
|
73029
|
+
continue;
|
|
73030
|
+
}
|
|
73031
|
+
const head = await this._readGroupHead(mlsData.group_id);
|
|
73032
|
+
if (head.ok && head.epoch !== Number(mgr.epoch)) {
|
|
73033
|
+
console.warn(`[SecureChannel] ${a2aLabel}: server head is at epoch ${head.epoch}, we are at ${Number(mgr.epoch)} \u2014 NOT committing`);
|
|
73034
|
+
await this._catchUpAfterPreflightSkip(mlsData.group_id, head.epoch, Number(mgr.epoch), a2aLabel);
|
|
73035
|
+
continue;
|
|
73036
|
+
}
|
|
73037
|
+
const inFlight = this._reserveCommit(mlsData.group_id, a2aLabel);
|
|
73038
|
+
if (!inFlight) continue;
|
|
73039
|
+
try {
|
|
73040
|
+
const kpRes = await fetch(
|
|
73041
|
+
`${this.config.apiUrl}/api/v1/mls/key-packages?hub_id=${encodeURIComponent(p2.hubId)}`,
|
|
73042
|
+
{ headers: { Authorization: `Bearer ${this._deviceJwt}` } }
|
|
73043
|
+
);
|
|
73044
|
+
if (kpRes.ok) {
|
|
73045
|
+
const kpData = await kpRes.json();
|
|
73046
|
+
if (kpData.key_package) {
|
|
73047
|
+
const kpBytes = new Uint8Array(Buffer.from(kpData.key_package, "hex"));
|
|
73048
|
+
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
73049
|
+
const stateBefore = mgr.exportState();
|
|
73050
|
+
const { commit, welcome } = await mgr.addMembers([memberKp]);
|
|
73051
|
+
const committedEpoch = Number(mgr.epoch);
|
|
73052
|
+
if (!welcome) {
|
|
73053
|
+
mgr.importState(stateBefore);
|
|
73054
|
+
console.warn(`[SecureChannel] ${a2aLabel}: addMembers produced no Welcome \u2014 rolled back`);
|
|
73055
|
+
continue;
|
|
73056
|
+
}
|
|
73057
|
+
const verdict = await this._sendCommitConfirmed({
|
|
73058
|
+
mlsGroup: mgr,
|
|
73059
|
+
mlsGroupId: mlsData.group_id,
|
|
73060
|
+
label: a2aLabel,
|
|
73061
|
+
commit,
|
|
73062
|
+
epoch: committedEpoch,
|
|
73063
|
+
parent: head.ok ? head.digest : void 0,
|
|
73064
|
+
memberDeviceIds: this._commitRoster(participants.map((q22) => q22.deviceId), [p2.deviceId]),
|
|
73065
|
+
stateBefore,
|
|
73066
|
+
inFlight
|
|
73067
|
+
});
|
|
73068
|
+
if (verdict !== "confirmed") continue;
|
|
73069
|
+
const sent = await this._sendFrameConfirmed({
|
|
73070
|
+
event: "mls_welcome",
|
|
72787
73071
|
data: {
|
|
72788
73072
|
group_id: mlsData.group_id,
|
|
72789
|
-
|
|
72790
|
-
|
|
73073
|
+
payload: Buffer.from(welcome).toString("hex"),
|
|
73074
|
+
target_device_id: p2.deviceId,
|
|
73075
|
+
a2a_channel_id: channelId,
|
|
73076
|
+
target_hub_id: p2.hubId
|
|
73077
|
+
// observability only — the backend ignores it
|
|
72791
73078
|
}
|
|
72792
|
-
})
|
|
72793
|
-
|
|
72794
|
-
|
|
72795
|
-
`[SecureChannel] Skipping MLS Welcome to ${p2.hubAddress} for A2A ${channelId.slice(0, 8)}: no deviceId. Backend a2a_channel_approved payload may be missing device_id for this participant.`
|
|
72796
|
-
);
|
|
72797
|
-
} else {
|
|
72798
|
-
this._ws.send(JSON.stringify({
|
|
72799
|
-
event: "mls_welcome",
|
|
72800
|
-
data: {
|
|
72801
|
-
group_id: mlsData.group_id,
|
|
72802
|
-
payload: Buffer.from(welcome).toString("hex"),
|
|
72803
|
-
target_device_id: p2.deviceId,
|
|
72804
|
-
a2a_channel_id: channelId,
|
|
72805
|
-
// Kept for observability — backend ignores but it's
|
|
72806
|
-
// useful in logs/wire captures to correlate to a hub.
|
|
72807
|
-
target_hub_id: p2.hubId
|
|
72808
|
-
}
|
|
72809
|
-
}));
|
|
73079
|
+
}, `${a2aLabel} welcome`);
|
|
73080
|
+
await saveMlsState(this.config.dataDir, mlsData.group_id, JSON.stringify(mgr.exportState()));
|
|
73081
|
+
if (sent) {
|
|
72810
73082
|
console.log(`[SecureChannel] Sent MLS Welcome to ${p2.hubAddress} (device ${p2.deviceId.slice(0, 8)}) for A2A ${channelId.slice(0, 8)}`);
|
|
73083
|
+
} else {
|
|
73084
|
+
console.warn(`[SecureChannel] ${a2aLabel}: Welcome was NOT written \u2014 the participant will request one`);
|
|
72811
73085
|
}
|
|
72812
73086
|
}
|
|
72813
|
-
await saveMlsState(this.config.dataDir, mlsData.group_id, JSON.stringify(mgr.exportState()));
|
|
72814
73087
|
}
|
|
73088
|
+
} finally {
|
|
73089
|
+
await this._releaseCommit(mlsData.group_id, inFlight, a2aLabel);
|
|
72815
73090
|
}
|
|
72816
73091
|
} catch (kpErr) {
|
|
72817
73092
|
console.warn(`[SecureChannel] Failed to Welcome participant ${p2.hubAddress}:`, kpErr);
|
|
@@ -99883,7 +100158,7 @@ var init_index = __esm2({
|
|
|
99883
100158
|
await init_skill_telemetry();
|
|
99884
100159
|
await init_policy_enforcer();
|
|
99885
100160
|
init_room_protocol();
|
|
99886
|
-
VERSION = true ? "0.23.
|
|
100161
|
+
VERSION = true ? "0.23.42" : "0.0.0-dev";
|
|
99887
100162
|
}
|
|
99888
100163
|
});
|
|
99889
100164
|
await init_index();
|
|
@@ -100659,7 +100934,7 @@ function _makeOwesReply(agentName, channel, log) {
|
|
|
100659
100934
|
);
|
|
100660
100935
|
}
|
|
100661
100936
|
return owesRoomReply(
|
|
100662
|
-
e.plaintext,
|
|
100937
|
+
e.caption ?? e.plaintext,
|
|
100663
100938
|
own,
|
|
100664
100939
|
own,
|
|
100665
100940
|
e.senderIsAgent,
|
|
@@ -100820,7 +101095,7 @@ async function main() {
|
|
|
100820
101095
|
const info = parseInfoFlags(process.argv.slice(2));
|
|
100821
101096
|
if (info.showVersion || info.showHelp) {
|
|
100822
101097
|
const version2 = buildClientVersion2(
|
|
100823
|
-
true ? "0.4.
|
|
101098
|
+
true ? "0.4.15" : void 0
|
|
100824
101099
|
);
|
|
100825
101100
|
if (info.showVersion) printLine(version2);
|
|
100826
101101
|
if (info.showHelp) {
|
|
@@ -100873,7 +101148,7 @@ async function main() {
|
|
|
100873
101148
|
agentName: cfg.agentName,
|
|
100874
101149
|
platform: "node",
|
|
100875
101150
|
clientVersion: buildClientVersion2(
|
|
100876
|
-
true ? "0.4.
|
|
101151
|
+
true ? "0.4.15" : void 0
|
|
100877
101152
|
)
|
|
100878
101153
|
});
|
|
100879
101154
|
channelRef = channel;
|
package/package.json
CHANGED