@agentvault/grok-bridge 0.4.13 → 0.4.14
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/index.js +534 -270
- package/package.json +1 -1
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.41" : FALLBACK;
|
|
64661
64661
|
return `${PACKAGE}@${v2}`;
|
|
64662
64662
|
}
|
|
64663
64663
|
function buildSha() {
|
|
64664
|
-
const raw = true ? "
|
|
64664
|
+
const raw = true ? "916d6c5-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.41" : "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.41" : "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.41" : "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
|
}
|
|
@@ -70955,9 +70990,34 @@ ${baseText}`;
|
|
|
70955
70990
|
*
|
|
70956
70991
|
* Returns the number of requests actually fulfilled.
|
|
70957
70992
|
*/
|
|
70993
|
+
/**
|
|
70994
|
+
* #1226: the roster to submit on a commit frame — the caller's known members,
|
|
70995
|
+
* the devices being admitted, and ALWAYS ourselves — deduplicated, UUIDs
|
|
70996
|
+
* only. Self is non-negotiable: once a roster is non-empty the server
|
|
70997
|
+
* requires the committer in it (#469), and our own head read
|
|
70998
|
+
* (`assert_group_member`) requires it too. So with no device id of our own
|
|
70999
|
+
* this returns `[]` and the frame carries NO roster (the pre-#1226 shape)
|
|
71000
|
+
* rather than a roster that locks us out. Non-UUID entries are dropped:
|
|
71001
|
+
* `known` can come off an inbound Welcome frame, and a malformed id in the
|
|
71002
|
+
* outbound frame is refused by the server (`MEMBER_DEVICE_IDS_MALFORMED`).
|
|
71003
|
+
* @internal exposed for tests
|
|
71004
|
+
*/
|
|
71005
|
+
_commitRoster(known, targets) {
|
|
71006
|
+
if (!this._deviceId) return [];
|
|
71007
|
+
const out = [];
|
|
71008
|
+
const seen = /* @__PURE__ */ new Set();
|
|
71009
|
+
for (const id of [...known ?? [], ...targets, this._deviceId]) {
|
|
71010
|
+
if (typeof id !== "string" || !UUID_RE.test(id) || seen.has(id)) continue;
|
|
71011
|
+
seen.add(id);
|
|
71012
|
+
out.push(id);
|
|
71013
|
+
}
|
|
71014
|
+
return out;
|
|
71015
|
+
}
|
|
70958
71016
|
async _fulfilWelcomeRequests(opts) {
|
|
70959
71017
|
const { mlsGroup, mlsGroupId, label, welcomeFields, targets } = opts;
|
|
70960
71018
|
if (targets.length === 0) return 0;
|
|
71019
|
+
const rosterBefore = this._commitRoster(opts.memberDeviceIds, []);
|
|
71020
|
+
const rosterAfter = this._commitRoster(opts.memberDeviceIds, targets.map((t2) => t2.deviceId));
|
|
70961
71021
|
if (!this._wsIsOpen()) {
|
|
70962
71022
|
console.warn(
|
|
70963
71023
|
`[SecureChannel] ${label}: socket is not OPEN \u2014 NOT advancing the group; ${targets.length} request(s) stay pending for the next pull`
|
|
@@ -70970,9 +71030,16 @@ ${baseText}`;
|
|
|
70970
71030
|
console.warn(
|
|
70971
71031
|
`[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
71032
|
);
|
|
71033
|
+
await this._catchUpAfterPreflightSkip(mlsGroupId, preHead.epoch, Number(mlsGroup.epoch), label);
|
|
70973
71034
|
return 0;
|
|
70974
71035
|
}
|
|
70975
71036
|
let parent2 = preHead.ok ? preHead.digest : void 0;
|
|
71037
|
+
if (this._commitInFlight.has(mlsGroupId)) {
|
|
71038
|
+
console.warn(
|
|
71039
|
+
`[SecureChannel] ${label}: another commit for this group is in flight \u2014 NOT advancing; ${targets.length} request(s) stay pending for the next pull`
|
|
71040
|
+
);
|
|
71041
|
+
return 0;
|
|
71042
|
+
}
|
|
70976
71043
|
const inFlight = { frames: [], sent: [] };
|
|
70977
71044
|
this._commitInFlight.set(mlsGroupId, inFlight);
|
|
70978
71045
|
try {
|
|
@@ -70997,7 +71064,8 @@ ${baseText}`;
|
|
|
70997
71064
|
// only knows the last one, which is why the result carries them.
|
|
70998
71065
|
epoch: Number(c2.epoch),
|
|
70999
71066
|
payload: Buffer.from(c2.commit).toString("hex"),
|
|
71000
|
-
...parent2 ? { parent_digest: parent2 } : {}
|
|
71067
|
+
...parent2 ? { parent_digest: parent2 } : {},
|
|
71068
|
+
...(sentCommits === commits.length - 1 ? rosterAfter : rosterBefore).length > 0 ? { member_device_ids: sentCommits === commits.length - 1 ? rosterAfter : rosterBefore } : {}
|
|
71001
71069
|
}
|
|
71002
71070
|
},
|
|
71003
71071
|
`${label} commit ${sentCommits + 1}/${commits.length}`
|
|
@@ -71145,6 +71213,126 @@ ${baseText}`;
|
|
|
71145
71213
|
return this._ws?.readyState === 1;
|
|
71146
71214
|
}
|
|
71147
71215
|
/** SHA-256 hex of a commit's bytes — its identity on the server (#1175/#1177). */
|
|
71216
|
+
/**
|
|
71217
|
+
* #1225: send ONE commit over the socket and wait for the server's verdict,
|
|
71218
|
+
* with the #1184 machinery — an in-flight record (so a `… commit rejected`
|
|
71219
|
+
* frame is correlated to it and a concurrent commit for the group is held
|
|
71220
|
+
* and replayed), the head poll, and rollback. The three A2A commit sites
|
|
71221
|
+
* (`a2a_participant_joined`, the `/distribute` WS fallback, `_foundA2AGroup`)
|
|
71222
|
+
* used a raw `ws.send` and treated "the socket took it" as "the server
|
|
71223
|
+
* stored it" — two of them then PERSISTED a state the server never saw.
|
|
71224
|
+
*
|
|
71225
|
+
* Returns the verdict. On `refused` / `unsent` the manager has been restored
|
|
71226
|
+
* to `stateBefore` and nothing must be persisted or minted from it.
|
|
71227
|
+
* `unverifiable` (head unreadable on every try AND no refusal) proceeds
|
|
71228
|
+
* UNCHECKED, the same deliberate choice as the fulfil site.
|
|
71229
|
+
*/
|
|
71230
|
+
/**
|
|
71231
|
+
* #1225 (review F1): reserve the single in-flight slot for a group. The
|
|
71232
|
+
* record is what `_noteCommitRefusal` correlates a refusal to and what
|
|
71233
|
+
* `_handleMlsCommit` holds concurrent socket commits behind; a second run on
|
|
71234
|
+
* the same group would clobber both (measured: two overlapping joins → two
|
|
71235
|
+
* commits claiming one epoch, one Welcome instead of two, the manager AHEAD
|
|
71236
|
+
* of the server). Taken BEFORE the commit is generated; released by
|
|
71237
|
+
* `_releaseCommit` after the last step that depends on it.
|
|
71238
|
+
*/
|
|
71239
|
+
/**
|
|
71240
|
+
* A pre-flight skip must not be a dead end. The skip happens because the
|
|
71241
|
+
* server's head is somewhere else; before this the group "caught up through
|
|
71242
|
+
* the normal divergence path", which only runs when a message for the group
|
|
71243
|
+
* arrives — so a group nobody writes to stayed diverged and its pending
|
|
71244
|
+
* Welcome requests were never fulfilled (named in PR #1227). When the server
|
|
71245
|
+
* is AHEAD, replay the missing commits now so the next pull can commit; when
|
|
71246
|
+
* WE are ahead there is nothing to replay (the fork-ahead direction, #972).
|
|
71247
|
+
*/
|
|
71248
|
+
async _catchUpAfterPreflightSkip(mlsGroupId, serverEpoch, localEpoch, label) {
|
|
71249
|
+
if (serverEpoch < localEpoch) {
|
|
71250
|
+
console.warn(`[SecureChannel] ${label}: we are AHEAD of the server (${localEpoch} > ${serverEpoch}) \u2014 nothing to replay`);
|
|
71251
|
+
return;
|
|
71252
|
+
}
|
|
71253
|
+
const fam = this._resolveGroupFamily(mlsGroupId);
|
|
71254
|
+
if (!fam) return;
|
|
71255
|
+
try {
|
|
71256
|
+
const caughtUp = await this._resyncOnDivergence(fam.managerKey, mlsGroupId);
|
|
71257
|
+
console.log(`[SecureChannel] ${label}: catch-up after pre-flight skip ${caughtUp ? "replayed the missing commits" : "could not replay"}`);
|
|
71258
|
+
} catch (err) {
|
|
71259
|
+
console.warn(`[SecureChannel] ${label}: catch-up after pre-flight skip failed:`, err);
|
|
71260
|
+
}
|
|
71261
|
+
}
|
|
71262
|
+
_reserveCommit(groupId, label) {
|
|
71263
|
+
if (this._commitInFlight.has(groupId)) {
|
|
71264
|
+
console.warn(
|
|
71265
|
+
`[SecureChannel] ${label}: another commit for group ${groupId.slice(0, 8)} is in flight \u2014 NOT advancing the group (a Welcome can be requested again)`
|
|
71266
|
+
);
|
|
71267
|
+
return null;
|
|
71268
|
+
}
|
|
71269
|
+
const rec = { frames: [], sent: [] };
|
|
71270
|
+
this._commitInFlight.set(groupId, rec);
|
|
71271
|
+
return rec;
|
|
71272
|
+
}
|
|
71273
|
+
/** Release the slot taken by `_reserveCommit` and replay the socket commits held meanwhile. */
|
|
71274
|
+
async _releaseCommit(groupId, rec, label) {
|
|
71275
|
+
if (this._commitInFlight.get(groupId) === rec) this._commitInFlight.delete(groupId);
|
|
71276
|
+
for (const frame of rec.frames) {
|
|
71277
|
+
try {
|
|
71278
|
+
await this._handleMlsCommit(frame);
|
|
71279
|
+
} catch (err) {
|
|
71280
|
+
console.error(`[SecureChannel] ${label}: replaying a held MLS commit failed:`, err);
|
|
71281
|
+
}
|
|
71282
|
+
}
|
|
71283
|
+
}
|
|
71284
|
+
/**
|
|
71285
|
+
* #1225: send ONE commit over the socket and wait for the server's verdict
|
|
71286
|
+
* with the #1184 machinery (refusal frame correlated through the reserved
|
|
71287
|
+
* in-flight record, head poll). The three A2A commit sites
|
|
71288
|
+
* (`a2a_participant_joined`, the `/distribute` WS fallback, `_foundA2AGroup`)
|
|
71289
|
+
* used a raw `ws.send` and treated "the socket took it" as "the server
|
|
71290
|
+
* stored it" — two of them then PERSISTED a state the server never saw.
|
|
71291
|
+
*
|
|
71292
|
+
* Only `confirmed` proceeds. `unverifiable` (head unreadable on every try,
|
|
71293
|
+
* no refusal) is a ROLLBACK here, unlike the fulfil site: that site proceeds
|
|
71294
|
+
* UNCHECKED because 79 of 80 production rooms 403 the head read on an empty
|
|
71295
|
+
* roster; an A2A head read admits any active participant (observer
|
|
71296
|
+
* fallback), so five failed reads there mean an outage, not a routine 403.
|
|
71297
|
+
* On anything but `confirmed` the manager is back at `stateBefore`.
|
|
71298
|
+
*/
|
|
71299
|
+
async _sendCommitConfirmed(opts) {
|
|
71300
|
+
const { mlsGroup, mlsGroupId, label, commit, epoch, parent: parent2, memberDeviceIds, stateBefore, inFlight } = opts;
|
|
71301
|
+
const ours = this._commitDigest(commit);
|
|
71302
|
+
inFlight.sent.push({ epoch, digest: ours, verified: false, stateBefore });
|
|
71303
|
+
const ok = await this._sendFrameConfirmed(
|
|
71304
|
+
{
|
|
71305
|
+
event: "mls_commit",
|
|
71306
|
+
data: {
|
|
71307
|
+
group_id: mlsGroupId,
|
|
71308
|
+
epoch,
|
|
71309
|
+
payload: Buffer.from(commit).toString("hex"),
|
|
71310
|
+
...parent2 ? { parent_digest: parent2 } : {},
|
|
71311
|
+
...memberDeviceIds && memberDeviceIds.length > 0 ? { member_device_ids: memberDeviceIds } : {}
|
|
71312
|
+
}
|
|
71313
|
+
},
|
|
71314
|
+
`${label} commit`
|
|
71315
|
+
);
|
|
71316
|
+
if (!ok) {
|
|
71317
|
+
inFlight.sent.pop();
|
|
71318
|
+
mlsGroup.importState(stateBefore);
|
|
71319
|
+
console.warn(
|
|
71320
|
+
`[SecureChannel] ${label}: commit at epoch ${epoch} was NOT written \u2014 rolled back to epoch ${Number(mlsGroup.epoch)}`
|
|
71321
|
+
);
|
|
71322
|
+
return "unsent";
|
|
71323
|
+
}
|
|
71324
|
+
const verdict = await this._confirmCommitAtHead(mlsGroupId, epoch, ours, inFlight, label);
|
|
71325
|
+
if (inFlight.refusal === void 0 && verdict === "confirmed") {
|
|
71326
|
+
inFlight.sent[inFlight.sent.length - 1].verified = true;
|
|
71327
|
+
return "confirmed";
|
|
71328
|
+
}
|
|
71329
|
+
const why = inFlight.refusal?.detail ?? verdict.toUpperCase();
|
|
71330
|
+
mlsGroup.importState(stateBefore);
|
|
71331
|
+
console.warn(
|
|
71332
|
+
`[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`
|
|
71333
|
+
);
|
|
71334
|
+
return "refused";
|
|
71335
|
+
}
|
|
71148
71336
|
_commitDigest(bytes) {
|
|
71149
71337
|
return createHash("sha256").update(bytes).digest("hex");
|
|
71150
71338
|
}
|
|
@@ -72098,7 +72286,9 @@ ${baseText}`;
|
|
|
72098
72286
|
mlsGroupId: roomState.mlsGroupId,
|
|
72099
72287
|
label: `room ${roomId.slice(0, 8)}`,
|
|
72100
72288
|
welcomeFields: { room_id: roomId },
|
|
72101
|
-
targets: roomTargets
|
|
72289
|
+
targets: roomTargets,
|
|
72290
|
+
memberDeviceIds: (roomState.members ?? []).map((m2) => m2.deviceId)
|
|
72291
|
+
// #1226
|
|
72102
72292
|
});
|
|
72103
72293
|
if (done > 0) {
|
|
72104
72294
|
console.log(
|
|
@@ -72153,7 +72343,9 @@ ${baseText}`;
|
|
|
72153
72343
|
mlsGroupId: entry.mlsGroupId,
|
|
72154
72344
|
label: `shared group ${gid.slice(0, 8)}`,
|
|
72155
72345
|
welcomeFields: { conversation_group_id: gid },
|
|
72156
|
-
targets: sharedTargets
|
|
72346
|
+
targets: sharedTargets,
|
|
72347
|
+
memberDeviceIds: entry.memberDeviceIds ?? []
|
|
72348
|
+
// #1226
|
|
72157
72349
|
});
|
|
72158
72350
|
if (done > 0) {
|
|
72159
72351
|
console.log(`[SecureChannel] Batched Welcome for ${done}/${validReqs.length} devices in shared group ${gid.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
|
|
@@ -72167,7 +72359,9 @@ ${baseText}`;
|
|
|
72167
72359
|
mlsGroupId: entry.mlsGroupId,
|
|
72168
72360
|
label: `shared group ${gid.slice(0, 8)}`,
|
|
72169
72361
|
welcomeFields: { conversation_group_id: gid },
|
|
72170
|
-
targets: [target]
|
|
72362
|
+
targets: [target],
|
|
72363
|
+
memberDeviceIds: entry.memberDeviceIds ?? []
|
|
72364
|
+
// #1226
|
|
72171
72365
|
});
|
|
72172
72366
|
if (done > 0) {
|
|
72173
72367
|
console.log(`[SecureChannel] Individual Welcome for ${target.deviceId.slice(0, 8)} in shared group ${gid.slice(0, 8)}`);
|
|
@@ -72214,7 +72408,11 @@ ${baseText}`;
|
|
|
72214
72408
|
requestId: req.id,
|
|
72215
72409
|
deviceId: req.requesting_device_id,
|
|
72216
72410
|
keyPackage: memberKp
|
|
72217
|
-
}]
|
|
72411
|
+
}],
|
|
72412
|
+
// #1226: a 1:1 roster is [self, owner] — the web's shape too.
|
|
72413
|
+
memberDeviceIds: [this._persisted?.sessions?.[convId]?.ownerDeviceId].filter(
|
|
72414
|
+
(d2) => typeof d2 === "string"
|
|
72415
|
+
)
|
|
72218
72416
|
});
|
|
72219
72417
|
if (done > 0) {
|
|
72220
72418
|
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in conv ${convId.slice(0, 8)}`);
|
|
@@ -72272,192 +72470,8 @@ ${baseText}`;
|
|
|
72272
72470
|
console.log(`[SecureChannel] ${pending.length} pending A2A Welcome requests for channel ${chId.slice(0, 8)}`);
|
|
72273
72471
|
for (const req of pending) {
|
|
72274
72472
|
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
|
-
}
|
|
72473
|
+
const next = await this._fulfilA2AWelcome(chId, chState, mlsGroup, req);
|
|
72474
|
+
if (next === "stop") break;
|
|
72461
72475
|
} catch (fulfillErr) {
|
|
72462
72476
|
console.log(`[SecureChannel] A2A Welcome fulfill failed: ${fulfillErr instanceof Error ? fulfillErr.message : String(fulfillErr)}`);
|
|
72463
72477
|
}
|
|
@@ -72728,6 +72742,227 @@ ${baseText}`;
|
|
|
72728
72742
|
}
|
|
72729
72743
|
}
|
|
72730
72744
|
}
|
|
72745
|
+
/**
|
|
72746
|
+
* #1225: fulfil ONE pending A2A Welcome request — the `/distribute` path.
|
|
72747
|
+
* Extracted from the delivery pull so the in-flight slot (review F1) is held
|
|
72748
|
+
* for exactly this request and released on every exit. Returns "stop" when
|
|
72749
|
+
* nothing else on this channel can succeed this pull (review F2).
|
|
72750
|
+
*/
|
|
72751
|
+
async _fulfilA2AWelcome(chId, chState, mlsGroup, req) {
|
|
72752
|
+
const a2aLabel = `A2A ${chId.slice(0, 8)}`;
|
|
72753
|
+
const preHead = await this._readGroupHead(chState.mlsGroupId);
|
|
72754
|
+
if (preHead.ok && preHead.epoch !== Number(mlsGroup.epoch)) {
|
|
72755
|
+
console.warn(
|
|
72756
|
+
`[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`
|
|
72757
|
+
);
|
|
72758
|
+
await this._catchUpAfterPreflightSkip(chState.mlsGroupId, preHead.epoch, Number(mlsGroup.epoch), a2aLabel);
|
|
72759
|
+
return "stop";
|
|
72760
|
+
}
|
|
72761
|
+
const inFlight = this._reserveCommit(chState.mlsGroupId, a2aLabel);
|
|
72762
|
+
if (!inFlight) return "stop";
|
|
72763
|
+
try {
|
|
72764
|
+
const kpRes = await fetch(
|
|
72765
|
+
`${this.config.apiUrl}/api/v1/mls/key-packages/batch?device_ids=${req.requesting_device_id}`,
|
|
72766
|
+
{ headers: { Authorization: `Bearer ${this._deviceJwt}` } }
|
|
72767
|
+
);
|
|
72768
|
+
if (!kpRes.ok) return "next";
|
|
72769
|
+
const kpData = await kpRes.json();
|
|
72770
|
+
const kpHex = kpData[req.requesting_device_id];
|
|
72771
|
+
if (!kpHex) return "next";
|
|
72772
|
+
const kpBytes = new Uint8Array(Buffer.from(kpHex, "hex"));
|
|
72773
|
+
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
72774
|
+
console.log(`[SecureChannel] A2A addMembers for ${req.requesting_device_id.slice(0, 8)} group=${chState.mlsGroupId.slice(0, 8)} epoch=${mlsGroup.epoch}`);
|
|
72775
|
+
let parentDigest = preHead.ok ? preHead.digest : void 0;
|
|
72776
|
+
let commit;
|
|
72777
|
+
let welcome;
|
|
72778
|
+
let preAddState = mlsGroup.exportState();
|
|
72779
|
+
try {
|
|
72780
|
+
const result = await mlsGroup.addMembers([memberKp]);
|
|
72781
|
+
commit = result.commit;
|
|
72782
|
+
welcome = result.welcome;
|
|
72783
|
+
} catch (addErr) {
|
|
72784
|
+
const errMsg = addErr instanceof Error ? addErr.message : String(addErr);
|
|
72785
|
+
if (errMsg.includes("already in the group")) {
|
|
72786
|
+
console.log(`[SecureChannel] A2A member already in group \u2014 removing and re-adding ${req.requesting_device_id.slice(0, 8)}`);
|
|
72787
|
+
const preRemoveState = JSON.stringify(mlsGroup.exportState());
|
|
72788
|
+
let removeCommitPublished = false;
|
|
72789
|
+
try {
|
|
72790
|
+
const identityBytes = new TextEncoder().encode(req.requesting_device_id);
|
|
72791
|
+
const leafIndex = mlsGroup.memberLeafIndex(identityBytes);
|
|
72792
|
+
if (leafIndex === null) {
|
|
72793
|
+
console.log(`[SecureChannel] A2A re-add: no leaf index for ${req.requesting_device_id.slice(0, 8)} \u2014 cannot remove`);
|
|
72794
|
+
return "next";
|
|
72795
|
+
}
|
|
72796
|
+
const removeResult = await mlsGroup.removeMember(leafIndex);
|
|
72797
|
+
try {
|
|
72798
|
+
const removeResp = await fetch(
|
|
72799
|
+
`${this.config.apiUrl}/api/v1/mls/groups/${chState.mlsGroupId}/distribute`,
|
|
72800
|
+
{
|
|
72801
|
+
method: "POST",
|
|
72802
|
+
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
72803
|
+
body: JSON.stringify({
|
|
72804
|
+
group_id: chState.mlsGroupId,
|
|
72805
|
+
device_id: this._deviceId,
|
|
72806
|
+
commit: Buffer.from(removeResult.commit).toString("hex"),
|
|
72807
|
+
commit_epoch: Number(mlsGroup.epoch),
|
|
72808
|
+
expected_epoch: Number(mlsGroup.epoch) - 1,
|
|
72809
|
+
// #1225: exact-step, like the WS path
|
|
72810
|
+
...parentDigest ? { parent_digest: parentDigest } : {}
|
|
72811
|
+
})
|
|
72812
|
+
}
|
|
72813
|
+
);
|
|
72814
|
+
if (removeResp.ok) {
|
|
72815
|
+
removeCommitPublished = true;
|
|
72816
|
+
await saveMlsState(this.config.dataDir, chState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
72817
|
+
parentDigest = this._commitDigest(removeResult.commit);
|
|
72818
|
+
} else {
|
|
72819
|
+
const detail = await removeResp.text().catch(() => "");
|
|
72820
|
+
console.warn(`[SecureChannel] A2A remove-commit distribute failed (${removeResp.status}): ${detail}`);
|
|
72821
|
+
}
|
|
72822
|
+
} catch (distErr) {
|
|
72823
|
+
console.warn(`[SecureChannel] A2A remove-commit distribute network error:`, distErr);
|
|
72824
|
+
}
|
|
72825
|
+
if (!removeCommitPublished) {
|
|
72826
|
+
mlsGroup.importState(JSON.parse(preRemoveState));
|
|
72827
|
+
return "next";
|
|
72828
|
+
}
|
|
72829
|
+
const kpRes2 = await fetch(
|
|
72830
|
+
`${this.config.apiUrl}/api/v1/mls/key-packages/batch?device_ids=${req.requesting_device_id}`,
|
|
72831
|
+
{ headers: { Authorization: `Bearer ${this._deviceJwt}` } }
|
|
72832
|
+
);
|
|
72833
|
+
if (kpRes2.ok) {
|
|
72834
|
+
const kpData2 = await kpRes2.json();
|
|
72835
|
+
const kpHex2 = kpData2[req.requesting_device_id];
|
|
72836
|
+
if (kpHex2) {
|
|
72837
|
+
const kpBytes2 = new Uint8Array(Buffer.from(kpHex2, "hex"));
|
|
72838
|
+
const memberKp2 = MLSGroupManager.deserializeKeyPackage(kpBytes2);
|
|
72839
|
+
preAddState = mlsGroup.exportState();
|
|
72840
|
+
const result2 = await mlsGroup.addMembers([memberKp2]);
|
|
72841
|
+
commit = result2.commit;
|
|
72842
|
+
welcome = result2.welcome;
|
|
72843
|
+
} else {
|
|
72844
|
+
console.log(`[SecureChannel] A2A re-add: no fresh KeyPackage for ${req.requesting_device_id.slice(0, 8)}`);
|
|
72845
|
+
return "next";
|
|
72846
|
+
}
|
|
72847
|
+
} else {
|
|
72848
|
+
console.log(`[SecureChannel] A2A re-add: KeyPackage fetch failed (${kpRes2.status})`);
|
|
72849
|
+
return "next";
|
|
72850
|
+
}
|
|
72851
|
+
} catch (readdErr) {
|
|
72852
|
+
console.log(`[SecureChannel] A2A remove+re-add FAILED: ${readdErr instanceof Error ? readdErr.message : String(readdErr)}`);
|
|
72853
|
+
if (!removeCommitPublished) {
|
|
72854
|
+
try {
|
|
72855
|
+
mlsGroup.importState(JSON.parse(preRemoveState));
|
|
72856
|
+
} catch {
|
|
72857
|
+
}
|
|
72858
|
+
}
|
|
72859
|
+
return "next";
|
|
72860
|
+
}
|
|
72861
|
+
} else {
|
|
72862
|
+
console.log(`[SecureChannel] A2A addMembers FAILED: ${errMsg}`);
|
|
72863
|
+
return "next";
|
|
72864
|
+
}
|
|
72865
|
+
}
|
|
72866
|
+
if (!welcome) {
|
|
72867
|
+
console.warn(`[SecureChannel] A2A addMembers produced no Welcome for ${req.requesting_device_id.slice(0, 8)}`);
|
|
72868
|
+
return "next";
|
|
72869
|
+
}
|
|
72870
|
+
const commitHex = Buffer.from(commit).toString("hex");
|
|
72871
|
+
const welcomeHex = Buffer.from(welcome).toString("hex");
|
|
72872
|
+
let distributed = false;
|
|
72873
|
+
let transportFailed = false;
|
|
72874
|
+
const updatedMembers = this._commitRoster([this._deviceId], [req.requesting_device_id]);
|
|
72875
|
+
try {
|
|
72876
|
+
const distResp = await fetch(
|
|
72877
|
+
`${this.config.apiUrl}/api/v1/mls/groups/${chState.mlsGroupId}/distribute`,
|
|
72878
|
+
{
|
|
72879
|
+
method: "POST",
|
|
72880
|
+
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
72881
|
+
body: JSON.stringify({
|
|
72882
|
+
group_id: chState.mlsGroupId,
|
|
72883
|
+
device_id: this._deviceId,
|
|
72884
|
+
commit: commitHex,
|
|
72885
|
+
commit_epoch: Number(mlsGroup.epoch),
|
|
72886
|
+
welcomes: [{ target_device_id: req.requesting_device_id, payload: welcomeHex }],
|
|
72887
|
+
member_device_ids: updatedMembers,
|
|
72888
|
+
expected_epoch: Number(mlsGroup.epoch) - 1,
|
|
72889
|
+
// #1225: exact-step, like the WS path
|
|
72890
|
+
...parentDigest ? { parent_digest: parentDigest } : {}
|
|
72891
|
+
})
|
|
72892
|
+
}
|
|
72893
|
+
);
|
|
72894
|
+
if (distResp.ok) {
|
|
72895
|
+
distributed = true;
|
|
72896
|
+
} else {
|
|
72897
|
+
const detail = await distResp.text().catch(() => "");
|
|
72898
|
+
console.warn(`[SecureChannel] A2A distribute failed (${distResp.status}): ${detail}`);
|
|
72899
|
+
if (distResp.status >= 500) {
|
|
72900
|
+
transportFailed = true;
|
|
72901
|
+
} else if (distResp.status === 409) {
|
|
72902
|
+
mlsGroup.importState(preAddState);
|
|
72903
|
+
console.warn(
|
|
72904
|
+
`[SecureChannel] ${a2aLabel}: add commit lost the race (409) \u2014 rolled back to epoch ${Number(mlsGroup.epoch)}, request stays pending`
|
|
72905
|
+
);
|
|
72906
|
+
return "next";
|
|
72907
|
+
} else {
|
|
72908
|
+
mlsGroup.importState(preAddState);
|
|
72909
|
+
console.error(
|
|
72910
|
+
`[SecureChannel] ${a2aLabel}: add commit refused with ${distResp.status} \u2014 rolled back to epoch ${Number(mlsGroup.epoch)}; not retrying this channel this pull`
|
|
72911
|
+
);
|
|
72912
|
+
return "stop";
|
|
72913
|
+
}
|
|
72914
|
+
}
|
|
72915
|
+
} catch (distErr) {
|
|
72916
|
+
console.warn(`[SecureChannel] A2A distribute network error:`, distErr);
|
|
72917
|
+
transportFailed = true;
|
|
72918
|
+
}
|
|
72919
|
+
if (!distributed && transportFailed && this._wsIsOpen()) {
|
|
72920
|
+
const verdict = await this._sendCommitConfirmed({
|
|
72921
|
+
mlsGroup,
|
|
72922
|
+
mlsGroupId: chState.mlsGroupId,
|
|
72923
|
+
label: a2aLabel,
|
|
72924
|
+
commit,
|
|
72925
|
+
epoch: Number(mlsGroup.epoch),
|
|
72926
|
+
parent: parentDigest,
|
|
72927
|
+
memberDeviceIds: updatedMembers,
|
|
72928
|
+
stateBefore: preAddState,
|
|
72929
|
+
inFlight
|
|
72930
|
+
});
|
|
72931
|
+
if (verdict !== "confirmed") return "next";
|
|
72932
|
+
const welcomeSent = await this._sendFrameConfirmed({
|
|
72933
|
+
event: "mls_welcome",
|
|
72934
|
+
data: {
|
|
72935
|
+
target_device_id: req.requesting_device_id,
|
|
72936
|
+
group_id: chState.mlsGroupId,
|
|
72937
|
+
a2a_channel_id: chId,
|
|
72938
|
+
payload: welcomeHex
|
|
72939
|
+
}
|
|
72940
|
+
}, `${a2aLabel} welcome`);
|
|
72941
|
+
if (!welcomeSent) {
|
|
72942
|
+
await saveMlsState(this.config.dataDir, chState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
72943
|
+
console.warn(`[SecureChannel] ${a2aLabel}: Welcome frame was NOT written \u2014 leaving the request pending`);
|
|
72944
|
+
return "next";
|
|
72945
|
+
}
|
|
72946
|
+
distributed = true;
|
|
72947
|
+
}
|
|
72948
|
+
if (!distributed) {
|
|
72949
|
+
console.warn(`[SecureChannel] Cannot deliver Welcome \u2014 HTTP distribute and WS both unavailable`);
|
|
72950
|
+
return "next";
|
|
72951
|
+
}
|
|
72952
|
+
const a2aFulfilled = await this._postFulfilWelcome(
|
|
72953
|
+
chState.mlsGroupId,
|
|
72954
|
+
req.id,
|
|
72955
|
+
`A2A ${chId.slice(0, 8)}`
|
|
72956
|
+
);
|
|
72957
|
+
await saveMlsState(this.config.dataDir, chState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
72958
|
+
if (a2aFulfilled) {
|
|
72959
|
+
console.log(`[SecureChannel] Fulfilled A2A Welcome for ${req.requesting_device_id.slice(0, 8)} in channel ${chId.slice(0, 8)}`);
|
|
72960
|
+
}
|
|
72961
|
+
return "next";
|
|
72962
|
+
} finally {
|
|
72963
|
+
await this._releaseCommit(chState.mlsGroupId, inFlight, a2aLabel);
|
|
72964
|
+
}
|
|
72965
|
+
}
|
|
72731
72966
|
/**
|
|
72732
72967
|
* Found the MLS group for an A2A channel as the creator agent.
|
|
72733
72968
|
* Calls /mls/a2a/{channelId}/init, creates the local MLS group, and sends
|
|
@@ -72770,48 +73005,77 @@ ${baseText}`;
|
|
|
72770
73005
|
await saveMlsState(this.config.dataDir, mlsData.group_id, JSON.stringify(mgr.exportState()));
|
|
72771
73006
|
for (const p2 of participants) {
|
|
72772
73007
|
if (p2.status !== "active" && p2.status !== "invited") continue;
|
|
73008
|
+
const a2aLabel = `A2A ${channelId.slice(0, 8)} found\u2192${p2.hubAddress}`;
|
|
72773
73009
|
try {
|
|
72774
|
-
|
|
72775
|
-
|
|
72776
|
-
|
|
72777
|
-
|
|
72778
|
-
|
|
72779
|
-
|
|
72780
|
-
|
|
72781
|
-
|
|
72782
|
-
|
|
72783
|
-
|
|
72784
|
-
|
|
72785
|
-
|
|
72786
|
-
|
|
73010
|
+
if (!p2.deviceId) {
|
|
73011
|
+
console.warn(
|
|
73012
|
+
`[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.`
|
|
73013
|
+
);
|
|
73014
|
+
continue;
|
|
73015
|
+
}
|
|
73016
|
+
if (!this._wsIsOpen()) {
|
|
73017
|
+
console.warn(`[SecureChannel] ${a2aLabel}: socket is not OPEN \u2014 NOT advancing the group`);
|
|
73018
|
+
continue;
|
|
73019
|
+
}
|
|
73020
|
+
const head = await this._readGroupHead(mlsData.group_id);
|
|
73021
|
+
if (head.ok && head.epoch !== Number(mgr.epoch)) {
|
|
73022
|
+
console.warn(`[SecureChannel] ${a2aLabel}: server head is at epoch ${head.epoch}, we are at ${Number(mgr.epoch)} \u2014 NOT committing`);
|
|
73023
|
+
await this._catchUpAfterPreflightSkip(mlsData.group_id, head.epoch, Number(mgr.epoch), a2aLabel);
|
|
73024
|
+
continue;
|
|
73025
|
+
}
|
|
73026
|
+
const inFlight = this._reserveCommit(mlsData.group_id, a2aLabel);
|
|
73027
|
+
if (!inFlight) continue;
|
|
73028
|
+
try {
|
|
73029
|
+
const kpRes = await fetch(
|
|
73030
|
+
`${this.config.apiUrl}/api/v1/mls/key-packages?hub_id=${encodeURIComponent(p2.hubId)}`,
|
|
73031
|
+
{ headers: { Authorization: `Bearer ${this._deviceJwt}` } }
|
|
73032
|
+
);
|
|
73033
|
+
if (kpRes.ok) {
|
|
73034
|
+
const kpData = await kpRes.json();
|
|
73035
|
+
if (kpData.key_package) {
|
|
73036
|
+
const kpBytes = new Uint8Array(Buffer.from(kpData.key_package, "hex"));
|
|
73037
|
+
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
73038
|
+
const stateBefore = mgr.exportState();
|
|
73039
|
+
const { commit, welcome } = await mgr.addMembers([memberKp]);
|
|
73040
|
+
const committedEpoch = Number(mgr.epoch);
|
|
73041
|
+
if (!welcome) {
|
|
73042
|
+
mgr.importState(stateBefore);
|
|
73043
|
+
console.warn(`[SecureChannel] ${a2aLabel}: addMembers produced no Welcome \u2014 rolled back`);
|
|
73044
|
+
continue;
|
|
73045
|
+
}
|
|
73046
|
+
const verdict = await this._sendCommitConfirmed({
|
|
73047
|
+
mlsGroup: mgr,
|
|
73048
|
+
mlsGroupId: mlsData.group_id,
|
|
73049
|
+
label: a2aLabel,
|
|
73050
|
+
commit,
|
|
73051
|
+
epoch: committedEpoch,
|
|
73052
|
+
parent: head.ok ? head.digest : void 0,
|
|
73053
|
+
memberDeviceIds: this._commitRoster(participants.map((q22) => q22.deviceId), [p2.deviceId]),
|
|
73054
|
+
stateBefore,
|
|
73055
|
+
inFlight
|
|
73056
|
+
});
|
|
73057
|
+
if (verdict !== "confirmed") continue;
|
|
73058
|
+
const sent = await this._sendFrameConfirmed({
|
|
73059
|
+
event: "mls_welcome",
|
|
72787
73060
|
data: {
|
|
72788
73061
|
group_id: mlsData.group_id,
|
|
72789
|
-
|
|
72790
|
-
|
|
73062
|
+
payload: Buffer.from(welcome).toString("hex"),
|
|
73063
|
+
target_device_id: p2.deviceId,
|
|
73064
|
+
a2a_channel_id: channelId,
|
|
73065
|
+
target_hub_id: p2.hubId
|
|
73066
|
+
// observability only — the backend ignores it
|
|
72791
73067
|
}
|
|
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
|
-
}));
|
|
73068
|
+
}, `${a2aLabel} welcome`);
|
|
73069
|
+
await saveMlsState(this.config.dataDir, mlsData.group_id, JSON.stringify(mgr.exportState()));
|
|
73070
|
+
if (sent) {
|
|
72810
73071
|
console.log(`[SecureChannel] Sent MLS Welcome to ${p2.hubAddress} (device ${p2.deviceId.slice(0, 8)}) for A2A ${channelId.slice(0, 8)}`);
|
|
73072
|
+
} else {
|
|
73073
|
+
console.warn(`[SecureChannel] ${a2aLabel}: Welcome was NOT written \u2014 the participant will request one`);
|
|
72811
73074
|
}
|
|
72812
73075
|
}
|
|
72813
|
-
await saveMlsState(this.config.dataDir, mlsData.group_id, JSON.stringify(mgr.exportState()));
|
|
72814
73076
|
}
|
|
73077
|
+
} finally {
|
|
73078
|
+
await this._releaseCommit(mlsData.group_id, inFlight, a2aLabel);
|
|
72815
73079
|
}
|
|
72816
73080
|
} catch (kpErr) {
|
|
72817
73081
|
console.warn(`[SecureChannel] Failed to Welcome participant ${p2.hubAddress}:`, kpErr);
|
|
@@ -99883,7 +100147,7 @@ var init_index = __esm2({
|
|
|
99883
100147
|
await init_skill_telemetry();
|
|
99884
100148
|
await init_policy_enforcer();
|
|
99885
100149
|
init_room_protocol();
|
|
99886
|
-
VERSION = true ? "0.23.
|
|
100150
|
+
VERSION = true ? "0.23.41" : "0.0.0-dev";
|
|
99887
100151
|
}
|
|
99888
100152
|
});
|
|
99889
100153
|
await init_index();
|
|
@@ -100820,7 +101084,7 @@ async function main() {
|
|
|
100820
101084
|
const info = parseInfoFlags(process.argv.slice(2));
|
|
100821
101085
|
if (info.showVersion || info.showHelp) {
|
|
100822
101086
|
const version2 = buildClientVersion2(
|
|
100823
|
-
true ? "0.4.
|
|
101087
|
+
true ? "0.4.14" : void 0
|
|
100824
101088
|
);
|
|
100825
101089
|
if (info.showVersion) printLine(version2);
|
|
100826
101090
|
if (info.showHelp) {
|
|
@@ -100873,7 +101137,7 @@ async function main() {
|
|
|
100873
101137
|
agentName: cfg.agentName,
|
|
100874
101138
|
platform: "node",
|
|
100875
101139
|
clientVersion: buildClientVersion2(
|
|
100876
|
-
true ? "0.4.
|
|
101140
|
+
true ? "0.4.14" : void 0
|
|
100877
101141
|
)
|
|
100878
101142
|
});
|
|
100879
101143
|
channelRef = channel;
|
package/package.json
CHANGED