@agentvault/grok-bridge 0.4.0 → 0.4.1
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 +580 -230
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -45899,6 +45899,11 @@ var init_ratchet = __esm2({
|
|
|
45899
45899
|
return { header, headerSignature, ciphertext, nonce };
|
|
45900
45900
|
}
|
|
45901
45901
|
decrypt(message) {
|
|
45902
|
+
if (this.state.peerIdentityPublicKey) {
|
|
45903
|
+
if (!verifyHeaderSignature(message.header, message.headerSignature, this.state.peerIdentityPublicKey)) {
|
|
45904
|
+
throw new Error("Header signature verification failed");
|
|
45905
|
+
}
|
|
45906
|
+
}
|
|
45902
45907
|
const isV2 = message.envelopeVersion === "2.0.0" && message.encryptedHeader != null && message.headerNonce != null;
|
|
45903
45908
|
const skippedResult = this.trySkippedKeys(message, isV2);
|
|
45904
45909
|
if (skippedResult !== null) {
|
|
@@ -45912,14 +45917,14 @@ var init_ratchet = __esm2({
|
|
|
45912
45917
|
if (message.header.messageNumber === 0) {
|
|
45913
45918
|
try {
|
|
45914
45919
|
const { messageKey: testKey, headerKey: testHeaderKey, nextChainKey: nextChainKey2 } = kdfChainKey(this.state.rootKey);
|
|
45915
|
-
let
|
|
45920
|
+
let ad;
|
|
45916
45921
|
if (isV2) {
|
|
45917
45922
|
libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.encryptedHeader, null, message.headerNonce, testHeaderKey);
|
|
45918
|
-
|
|
45923
|
+
ad = message.encryptedHeader;
|
|
45919
45924
|
} else {
|
|
45920
|
-
|
|
45925
|
+
ad = serializeHeader(message.header);
|
|
45921
45926
|
}
|
|
45922
|
-
const ptBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext,
|
|
45927
|
+
const ptBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, ad, message.nonce, testKey);
|
|
45923
45928
|
this.state.dhReceivingPublicKey = message.header.dhPublicKey;
|
|
45924
45929
|
this.state.receivingChain = {
|
|
45925
45930
|
chainKey: nextChainKey2,
|
|
@@ -45939,13 +45944,7 @@ var init_ratchet = __esm2({
|
|
|
45939
45944
|
this.skipMessages(this.state.receivingChain, message.header.messageNumber, message.header.dhPublicKey, isV2);
|
|
45940
45945
|
const chain = this.state.receivingChain;
|
|
45941
45946
|
const { nextChainKey, messageKey, headerKey } = kdfChainKey(chain.chainKey);
|
|
45942
|
-
|
|
45943
|
-
chain.messageNumber++;
|
|
45944
|
-
if (this.state.peerIdentityPublicKey) {
|
|
45945
|
-
if (!verifyHeaderSignature(message.header, message.headerSignature, this.state.peerIdentityPublicKey)) {
|
|
45946
|
-
throw new Error("Header signature verification failed");
|
|
45947
|
-
}
|
|
45948
|
-
}
|
|
45947
|
+
let plaintextBytes;
|
|
45949
45948
|
if (isV2) {
|
|
45950
45949
|
try {
|
|
45951
45950
|
libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.encryptedHeader, null, message.headerNonce, headerKey);
|
|
@@ -45953,19 +45952,21 @@ var init_ratchet = __esm2({
|
|
|
45953
45952
|
throw new Error("V2 header decryption failed");
|
|
45954
45953
|
}
|
|
45955
45954
|
try {
|
|
45956
|
-
|
|
45957
|
-
return libsodium_wrappers_default.to_string(plaintextBytes);
|
|
45955
|
+
plaintextBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, message.encryptedHeader, message.nonce, messageKey);
|
|
45958
45956
|
} catch {
|
|
45959
45957
|
throw new Error("V2 decryption failed: ciphertext tampered or wrong key");
|
|
45960
45958
|
}
|
|
45959
|
+
} else {
|
|
45960
|
+
const ad = serializeHeader(message.header);
|
|
45961
|
+
try {
|
|
45962
|
+
plaintextBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, ad, message.nonce, messageKey);
|
|
45963
|
+
} catch {
|
|
45964
|
+
throw new Error("Decryption failed: ciphertext tampered or wrong key");
|
|
45965
|
+
}
|
|
45961
45966
|
}
|
|
45962
|
-
|
|
45963
|
-
|
|
45964
|
-
|
|
45965
|
-
return libsodium_wrappers_default.to_string(plaintextBytes);
|
|
45966
|
-
} catch {
|
|
45967
|
-
throw new Error("Decryption failed: ciphertext tampered or wrong key");
|
|
45968
|
-
}
|
|
45967
|
+
chain.chainKey = nextChainKey;
|
|
45968
|
+
chain.messageNumber++;
|
|
45969
|
+
return libsodium_wrappers_default.to_string(plaintextBytes);
|
|
45969
45970
|
}
|
|
45970
45971
|
dhRatchetReceive(theirDhPublic) {
|
|
45971
45972
|
this.state.previousSendingChainLength = this.state.sendingChain?.messageNumber ?? 0;
|
|
@@ -64139,6 +64140,23 @@ var init_mls_delivery_order = __esm2({
|
|
|
64139
64140
|
"use strict";
|
|
64140
64141
|
}
|
|
64141
64142
|
});
|
|
64143
|
+
function noteObservedEpoch(current, seen) {
|
|
64144
|
+
const n2 = Number(seen);
|
|
64145
|
+
if (!Number.isInteger(n2) || n2 < 0)
|
|
64146
|
+
return current;
|
|
64147
|
+
return n2 > current ? n2 : current;
|
|
64148
|
+
}
|
|
64149
|
+
function needsPreSendResync(localEpoch, observedEpoch) {
|
|
64150
|
+
if (!Number.isInteger(localEpoch) || !Number.isInteger(observedEpoch)) {
|
|
64151
|
+
return false;
|
|
64152
|
+
}
|
|
64153
|
+
return observedEpoch > localEpoch;
|
|
64154
|
+
}
|
|
64155
|
+
var init_mls_presend_epoch = __esm2({
|
|
64156
|
+
"../crypto/dist/mls-presend-epoch.js"() {
|
|
64157
|
+
"use strict";
|
|
64158
|
+
}
|
|
64159
|
+
});
|
|
64142
64160
|
var dist_exports = {};
|
|
64143
64161
|
__export2(dist_exports, {
|
|
64144
64162
|
AV_CREDENTIAL_CONTEXT: () => AV_CREDENTIAL_CONTEXT,
|
|
@@ -64208,7 +64226,9 @@ __export2(dist_exports, {
|
|
|
64208
64226
|
hexTransportToEncryptedMessage: () => hexTransportToEncryptedMessage,
|
|
64209
64227
|
issueCredential: () => issueCredential,
|
|
64210
64228
|
multibaseToPublicKey: () => multibaseToPublicKey,
|
|
64229
|
+
needsPreSendResync: () => needsPreSendResync,
|
|
64211
64230
|
normalizeBackupCode: () => normalizeBackupCode,
|
|
64231
|
+
noteObservedEpoch: () => noteObservedEpoch,
|
|
64212
64232
|
orderDeliveryBatch: () => orderDeliveryBatch,
|
|
64213
64233
|
parseTraceparent: () => parseTraceparent,
|
|
64214
64234
|
performX3DH: () => performX3DH,
|
|
@@ -64248,6 +64268,7 @@ var init_dist = __esm2({
|
|
|
64248
64268
|
init_mls_group();
|
|
64249
64269
|
await init_owner_sync();
|
|
64250
64270
|
init_mls_delivery_order();
|
|
64271
|
+
init_mls_presend_epoch();
|
|
64251
64272
|
}
|
|
64252
64273
|
});
|
|
64253
64274
|
async function ensureSecureDir(dir) {
|
|
@@ -64440,7 +64461,7 @@ var init_mls_kp_pool = __esm2({
|
|
|
64440
64461
|
}
|
|
64441
64462
|
});
|
|
64442
64463
|
function ownIdentity() {
|
|
64443
|
-
const v2 = true ? "0.23.
|
|
64464
|
+
const v2 = true ? "0.23.27" : FALLBACK;
|
|
64444
64465
|
return `${PACKAGE}@${v2}`;
|
|
64445
64466
|
}
|
|
64446
64467
|
function buildClientVersion(override) {
|
|
@@ -65356,6 +65377,7 @@ var init_channel = __esm2({
|
|
|
65356
65377
|
await init_libsodium_wrappers();
|
|
65357
65378
|
await init_dist();
|
|
65358
65379
|
await init_dist();
|
|
65380
|
+
init_mls_presend_epoch();
|
|
65359
65381
|
init_mls_state();
|
|
65360
65382
|
await init_mls_kp_pool();
|
|
65361
65383
|
init_client_version();
|
|
@@ -65469,6 +65491,17 @@ var init_channel = __esm2({
|
|
|
65469
65491
|
* self-heal rather than be logged and dropped. */
|
|
65470
65492
|
_mlsCommitFailCounts = /* @__PURE__ */ new Map();
|
|
65471
65493
|
static MAX_MLS_DECRYPT_FAILS = 3;
|
|
65494
|
+
/**
|
|
65495
|
+
* #1084: consecutive resyncs that found NOTHING to apply, per group.
|
|
65496
|
+
*
|
|
65497
|
+
* "No new commits" means the server holds no commit past our epoch — we are
|
|
65498
|
+
* NOT BEHIND. Treating that as corruption and re-keying is what drove the
|
|
65499
|
+
* epoch churn (see `_resyncOnDivergence`). But a state that is genuinely
|
|
65500
|
+
* broken while its epoch counter happens to match must still recover, so the
|
|
65501
|
+
* suppression is bounded rather than absolute.
|
|
65502
|
+
*/
|
|
65503
|
+
_resyncNoOpStrikes = /* @__PURE__ */ new Map();
|
|
65504
|
+
static MAX_RESYNC_NOOPS = 3;
|
|
65472
65505
|
/** Cached MLS KeyPackage bundle for this device (regenerated on each connect). */
|
|
65473
65506
|
_mlsKeyPackage = null;
|
|
65474
65507
|
/** Pending KeyPackage bundle from request-Welcome flow (used by _handleMlsWelcome). */
|
|
@@ -65506,6 +65539,13 @@ var init_channel = __esm2({
|
|
|
65506
65539
|
_kpPoolFilling = false;
|
|
65507
65540
|
/** Buffer for MLS commits received before Welcome (keyed by groupId, sorted by epoch). */
|
|
65508
65541
|
_pendingMlsCommits = /* @__PURE__ */ new Map();
|
|
65542
|
+
/**
|
|
65543
|
+
* #1013: the highest group epoch any inbound frame has reported, per MLS
|
|
65544
|
+
* group id. Every inbound frame carries the epoch the group was at when it
|
|
65545
|
+
* was produced, so each is a LOWER BOUND on the group's current epoch — the
|
|
65546
|
+
* hint is monotonic and only ever rises.
|
|
65547
|
+
*/
|
|
65548
|
+
_observedGroupEpochs = /* @__PURE__ */ new Map();
|
|
65509
65549
|
/** In-memory credential store for renter-provided credentials (never persisted). */
|
|
65510
65550
|
_credentialStore = new CredentialStore();
|
|
65511
65551
|
/** Rooms whose roster has been refreshed from the backend at least once in
|
|
@@ -66190,16 +66230,44 @@ var init_channel = __esm2({
|
|
|
66190
66230
|
const pendingWsSends = [];
|
|
66191
66231
|
const sentSharedGroupIds = /* @__PURE__ */ new Set();
|
|
66192
66232
|
const addressedMlsGroupIds = [];
|
|
66233
|
+
if (!(this._persisted?.mlsGroups && this._state === "ready" && this._ws)) {
|
|
66234
|
+
console.warn(
|
|
66235
|
+
`[SecureChannel] send(): shared-MLS block SKIPPED \u2014 mlsGroups=${this._persisted?.mlsGroups ? Object.keys(this._persisted.mlsGroups).length : "none"} state=${this._state} ws=${this._ws ? "open" : "null"}`
|
|
66236
|
+
);
|
|
66237
|
+
}
|
|
66193
66238
|
if (this._persisted?.mlsGroups && this._state === "ready" && this._ws) {
|
|
66194
66239
|
const targetSharedGid = targetConvId ? this._sessionGroupIds?.get(targetConvId) : void 0;
|
|
66240
|
+
console.log(
|
|
66241
|
+
`[SecureChannel] send(): considering ${Object.keys(this._persisted.mlsGroups).length} shared group(s); targetConvId=${targetConvId ? targetConvId.slice(0, 8) : "(none \u2014 broadcast)"} targetSharedGid=${targetSharedGid ? targetSharedGid.slice(0, 8) : "(none)"}`
|
|
66242
|
+
);
|
|
66243
|
+
const skipped = [];
|
|
66195
66244
|
for (const [gid, entry] of Object.entries(this._persisted.mlsGroups)) {
|
|
66196
|
-
if (targetConvId && gid !== targetSharedGid)
|
|
66197
|
-
|
|
66245
|
+
if (targetConvId && gid !== targetSharedGid) {
|
|
66246
|
+
skipped.push(`${gid.slice(0, 8)}:not-target`);
|
|
66247
|
+
continue;
|
|
66248
|
+
}
|
|
66249
|
+
if (!entry.mlsGroupId) {
|
|
66250
|
+
skipped.push(`${gid.slice(0, 8)}:no-mlsGroupId`);
|
|
66251
|
+
continue;
|
|
66252
|
+
}
|
|
66198
66253
|
const mlsGroup = this._mlsGroups.get(`1to1-group:${gid}`);
|
|
66199
|
-
if (!mlsGroup?.isInitialized
|
|
66254
|
+
if (!mlsGroup?.isInitialized) {
|
|
66255
|
+
skipped.push(`${gid.slice(0, 8)}:not-in-memory`);
|
|
66256
|
+
continue;
|
|
66257
|
+
}
|
|
66258
|
+
if (Number(mlsGroup.epoch) <= 0) {
|
|
66259
|
+
skipped.push(`${gid.slice(0, 8)}:epoch<=0`);
|
|
66260
|
+
continue;
|
|
66261
|
+
}
|
|
66200
66262
|
try {
|
|
66201
66263
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
66202
|
-
const cipherBytes = await
|
|
66264
|
+
const cipherBytes = await this._encryptWithCatchUp(
|
|
66265
|
+
mlsGroup,
|
|
66266
|
+
`1to1-group:${gid}`,
|
|
66267
|
+
entry.mlsGroupId,
|
|
66268
|
+
`1:1 group ${gid.slice(0, 8)}`,
|
|
66269
|
+
plaintextBytes
|
|
66270
|
+
);
|
|
66203
66271
|
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
66204
66272
|
const payload = {
|
|
66205
66273
|
group_id: entry.mlsGroupId,
|
|
@@ -66221,8 +66289,12 @@ var init_channel = __esm2({
|
|
|
66221
66289
|
console.log(`[SecureChannel] Shared MLS group send for group ${gid.slice(0, 8)} (${entry.mlsGroupId.slice(0, 8)})`);
|
|
66222
66290
|
} catch (err) {
|
|
66223
66291
|
console.error(`[SecureChannel] Shared MLS group send failed for ${gid.slice(0, 8)}:`, err);
|
|
66292
|
+
skipped.push(`${gid.slice(0, 8)}:encrypt-threw`);
|
|
66224
66293
|
}
|
|
66225
66294
|
}
|
|
66295
|
+
if (skipped.length > 0) {
|
|
66296
|
+
console.log(`[SecureChannel] send(): skipped ${skipped.length} group(s) \u2014 ${skipped.join(", ")}`);
|
|
66297
|
+
}
|
|
66226
66298
|
}
|
|
66227
66299
|
for (const [convId, session] of this._sessions) {
|
|
66228
66300
|
if (!session.activated) continue;
|
|
@@ -66242,7 +66314,14 @@ var init_channel = __esm2({
|
|
|
66242
66314
|
}
|
|
66243
66315
|
if (mlsGroup?.isInitialized && mlsGroupId && Number(mlsGroup.epoch) > 0 && this._state === "ready" && this._ws) {
|
|
66244
66316
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
66245
|
-
const
|
|
66317
|
+
const sessionGroupKey = convGroupId && this._mlsGroups.has(`1to1-group:${convGroupId}`) ? `1to1-group:${convGroupId}` : `conv:${convId}`;
|
|
66318
|
+
const cipherBytes = await this._encryptWithCatchUp(
|
|
66319
|
+
mlsGroup,
|
|
66320
|
+
sessionGroupKey,
|
|
66321
|
+
mlsGroupId,
|
|
66322
|
+
`conversation ${convId.slice(0, 8)}`,
|
|
66323
|
+
plaintextBytes
|
|
66324
|
+
);
|
|
66246
66325
|
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
66247
66326
|
const payload = {
|
|
66248
66327
|
conversation_id: convId,
|
|
@@ -66337,7 +66416,14 @@ var init_channel = __esm2({
|
|
|
66337
66416
|
}
|
|
66338
66417
|
try {
|
|
66339
66418
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
66340
|
-
const
|
|
66419
|
+
const mlsOnlyGroupKey = mlsOnlyConvGroupId && this._mlsGroups.has(`1to1-group:${mlsOnlyConvGroupId}`) ? `1to1-group:${mlsOnlyConvGroupId}` : `conv:${mlsConvId}`;
|
|
66420
|
+
const cipherBytes = await this._encryptWithCatchUp(
|
|
66421
|
+
mlsGroup,
|
|
66422
|
+
mlsOnlyGroupKey,
|
|
66423
|
+
resolvedMlsGroupId,
|
|
66424
|
+
`conversation ${mlsConvId.slice(0, 8)}`,
|
|
66425
|
+
plaintextBytes
|
|
66426
|
+
);
|
|
66341
66427
|
await saveMlsState(this.config.dataDir, resolvedMlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
66342
66428
|
const payload = {
|
|
66343
66429
|
conversation_id: mlsConvId,
|
|
@@ -66363,13 +66449,26 @@ var init_channel = __esm2({
|
|
|
66363
66449
|
}
|
|
66364
66450
|
}
|
|
66365
66451
|
}
|
|
66366
|
-
if (sentCount === 0
|
|
66367
|
-
console.warn(
|
|
66452
|
+
if (sentCount === 0) {
|
|
66453
|
+
console.warn(
|
|
66454
|
+
`[SecureChannel] send() delivered to 0 destinations \u2014 nothing was encrypted or queued (sessions=${this._sessions.size}, sharedGroups=${this._persisted?.mlsGroups ? Object.keys(this._persisted.mlsGroups).length : 0})`
|
|
66455
|
+
);
|
|
66368
66456
|
}
|
|
66369
66457
|
await this._persistState();
|
|
66458
|
+
const wsReady = this._ws?.readyState === 1;
|
|
66459
|
+
if (pendingWsSends.length > 0 && !wsReady) {
|
|
66460
|
+
console.warn(
|
|
66461
|
+
`[SecureChannel] send(): ${pendingWsSends.length} frame(s) NOT written \u2014 socket readyState=${this._ws?.readyState ?? "null"} (1=OPEN)`
|
|
66462
|
+
);
|
|
66463
|
+
}
|
|
66370
66464
|
for (const frame of pendingWsSends) {
|
|
66371
66465
|
this._ws.send(frame);
|
|
66372
66466
|
}
|
|
66467
|
+
if (pendingWsSends.length > 0) {
|
|
66468
|
+
console.log(
|
|
66469
|
+
`[SecureChannel] send(): wrote ${pendingWsSends.length} frame(s) to the socket (sentCount=${sentCount}, readyState=${this._ws?.readyState ?? "null"})`
|
|
66470
|
+
);
|
|
66471
|
+
}
|
|
66373
66472
|
if (!options?.isResend) {
|
|
66374
66473
|
for (const mlsGroupId of addressedMlsGroupIds) {
|
|
66375
66474
|
this._rememberRetryCandidate(mlsGroupId, plaintext, options);
|
|
@@ -66419,7 +66518,7 @@ var init_channel = __esm2({
|
|
|
66419
66518
|
*/
|
|
66420
66519
|
sendActivitySpan(spanData) {
|
|
66421
66520
|
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
|
|
66422
|
-
const pluginVersion = true ? "0.23.
|
|
66521
|
+
const pluginVersion = true ? "0.23.27" : "0.0.0-dev";
|
|
66423
66522
|
const agentName = this.config.agentName ?? "Agent";
|
|
66424
66523
|
const resource = {
|
|
66425
66524
|
"service.name": "agentvault-agent",
|
|
@@ -66629,7 +66728,13 @@ var init_channel = __esm2({
|
|
|
66629
66728
|
if (mlsGroup?.isInitialized) {
|
|
66630
66729
|
try {
|
|
66631
66730
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
66632
|
-
const ciphertext = await
|
|
66731
|
+
const ciphertext = await this._encryptWithCatchUp(
|
|
66732
|
+
mlsGroup,
|
|
66733
|
+
roomId,
|
|
66734
|
+
room.mlsGroupId,
|
|
66735
|
+
`room ${roomId.slice(0, 8)}`,
|
|
66736
|
+
plaintextBytes
|
|
66737
|
+
);
|
|
66633
66738
|
await saveMlsState(this.config.dataDir, room.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
66634
66739
|
if (this._state === "ready" && this._ws) {
|
|
66635
66740
|
this._ws.send(JSON.stringify({
|
|
@@ -66880,11 +66985,17 @@ var init_channel = __esm2({
|
|
|
66880
66985
|
const before = this._persisted.groupId;
|
|
66881
66986
|
this._sessionGroupIds = new Map(mine.map((r2) => [r2.id, r2.group_id]));
|
|
66882
66987
|
this._persisted.conversationGroupIds = Object.fromEntries(this._sessionGroupIds);
|
|
66883
|
-
const
|
|
66988
|
+
const joined = mine.find((r2) => this._persisted.mlsGroups?.[r2.group_id]);
|
|
66989
|
+
const primary = mine.find((r2) => r2.id === this._persisted.primaryConversationId) ?? joined ?? mine[0];
|
|
66990
|
+
if (!mine.some((r2) => r2.id === this._persisted.primaryConversationId)) {
|
|
66991
|
+
console.log(
|
|
66992
|
+
`[SecureChannel] Adopting new primary conversation ${primary.id.slice(0, 8)} (group ${primary.group_id.slice(0, 8)}) \u2014 ${joined ? "we hold MLS state for it" : "no joined conversation found, first active row"}`
|
|
66993
|
+
);
|
|
66994
|
+
}
|
|
66884
66995
|
this._persisted.groupId = primary.group_id;
|
|
66885
66996
|
this._persisted.primaryConversationId = primary.id;
|
|
66886
66997
|
const liveGroupIds = new Set(mine.map((r2) => r2.group_id));
|
|
66887
|
-
if (!liveGroupIds.has(staleGid)) {
|
|
66998
|
+
if (staleGid && staleMlsGroupId && !liveGroupIds.has(staleGid)) {
|
|
66888
66999
|
try {
|
|
66889
67000
|
await deleteMlsState(this.config.dataDir, staleMlsGroupId);
|
|
66890
67001
|
} catch {
|
|
@@ -66898,9 +67009,9 @@ var init_channel = __esm2({
|
|
|
66898
67009
|
}
|
|
66899
67010
|
await this._persistState();
|
|
66900
67011
|
console.log(
|
|
66901
|
-
`[SecureChannel] Re-resolved conversation groups after 'unknown group' ${staleMlsGroupId.slice(0, 8)}: primary group ${String(before).slice(0, 8)} \u2192 ${this._persisted.groupId.slice(0, 8)} (${mine.length} active conversation(s))`
|
|
67012
|
+
`[SecureChannel] Re-resolved conversation groups ${staleMlsGroupId ? `after 'unknown group' ${staleMlsGroupId.slice(0, 8)}` : "on connect (#1034)"}: primary group ${String(before).slice(0, 8)} \u2192 ${this._persisted.groupId.slice(0, 8)} (${mine.length} active conversation(s))`
|
|
66902
67013
|
);
|
|
66903
|
-
return this._persisted.groupId !== before && before === staleGid;
|
|
67014
|
+
return staleGid !== void 0 && this._persisted.groupId !== before && before === staleGid;
|
|
66904
67015
|
}
|
|
66905
67016
|
/**
|
|
66906
67017
|
* Resend the message that a now-reconciled `unknown group` refusal killed (#732).
|
|
@@ -67808,7 +67919,13 @@ var init_channel = __esm2({
|
|
|
67808
67919
|
const mlsGroup = this._mlsGroups.get(`a2a:${channelEntry.channelId}`);
|
|
67809
67920
|
if (mlsGroup?.isInitialized) {
|
|
67810
67921
|
const plaintextBytes = new TextEncoder().encode(text);
|
|
67811
|
-
const ciphertext = await
|
|
67922
|
+
const ciphertext = await this._encryptWithCatchUp(
|
|
67923
|
+
mlsGroup,
|
|
67924
|
+
`a2a:${channelEntry.channelId}`,
|
|
67925
|
+
channelEntry.mlsGroupId,
|
|
67926
|
+
`A2A ${channelEntry.channelId.slice(0, 8)}`,
|
|
67927
|
+
plaintextBytes
|
|
67928
|
+
);
|
|
67812
67929
|
await saveMlsState(
|
|
67813
67930
|
this.config.dataDir,
|
|
67814
67931
|
channelEntry.mlsGroupId,
|
|
@@ -67856,7 +67973,13 @@ var init_channel = __esm2({
|
|
|
67856
67973
|
const mlsGroup = this._mlsGroups.get(`a2a:${channelId}`);
|
|
67857
67974
|
if (mlsGroup?.isInitialized) {
|
|
67858
67975
|
const plaintextBytes = new TextEncoder().encode(text);
|
|
67859
|
-
const ciphertext = await
|
|
67976
|
+
const ciphertext = await this._encryptWithCatchUp(
|
|
67977
|
+
mlsGroup,
|
|
67978
|
+
`a2a:${channelId}`,
|
|
67979
|
+
entry.mlsGroupId,
|
|
67980
|
+
`A2A ${channelId.slice(0, 8)}`,
|
|
67981
|
+
plaintextBytes
|
|
67982
|
+
);
|
|
67860
67983
|
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
67861
67984
|
this._ws.send(JSON.stringify({
|
|
67862
67985
|
event: "a2a_message_mls",
|
|
@@ -68302,6 +68425,9 @@ var init_channel = __esm2({
|
|
|
68302
68425
|
await this._pullDrDeliveryQueue();
|
|
68303
68426
|
await this._flushOutboundQueue();
|
|
68304
68427
|
this._setState("ready");
|
|
68428
|
+
void this._reconcileConversationGroups().catch((err) => {
|
|
68429
|
+
console.warn("[SecureChannel] Conversation-group reconcile failed:", err);
|
|
68430
|
+
});
|
|
68305
68431
|
void this._reconcileRoomsWithServer().catch(
|
|
68306
68432
|
(err) => console.warn(`[SecureChannel] room reconcile failed (ignored): ${err instanceof Error ? err.message : String(err)}`)
|
|
68307
68433
|
).then(() => this._quarantineOrphanedMlsGroups()).catch(
|
|
@@ -68329,7 +68455,7 @@ var init_channel = __esm2({
|
|
|
68329
68455
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
68330
68456
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
68331
68457
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
68332
|
-
pluginVersion: true ? "0.23.
|
|
68458
|
+
pluginVersion: true ? "0.23.27" : "0.0.0-dev"
|
|
68333
68459
|
});
|
|
68334
68460
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
68335
68461
|
}
|
|
@@ -68653,7 +68779,7 @@ var init_channel = __esm2({
|
|
|
68653
68779
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
68654
68780
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
68655
68781
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
68656
|
-
pluginVersion: true ? "0.23.
|
|
68782
|
+
pluginVersion: true ? "0.23.27" : "0.0.0-dev"
|
|
68657
68783
|
});
|
|
68658
68784
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
68659
68785
|
}
|
|
@@ -69019,6 +69145,7 @@ var init_channel = __esm2({
|
|
|
69019
69145
|
const convGroupId = data.conversation_group_id;
|
|
69020
69146
|
const groupId = data.group_id;
|
|
69021
69147
|
const senderDeviceId = data.sender_device_id;
|
|
69148
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
69022
69149
|
if (senderDeviceId === this._deviceId) return;
|
|
69023
69150
|
let mgr;
|
|
69024
69151
|
let mgrKey;
|
|
@@ -69096,9 +69223,15 @@ var init_channel = __esm2({
|
|
|
69096
69223
|
})),
|
|
69097
69224
|
no_history: filtered.length === 0
|
|
69098
69225
|
});
|
|
69099
|
-
if (mgr && mlsGroupId) {
|
|
69226
|
+
if (mgr && mgrKey && mlsGroupId) {
|
|
69100
69227
|
const responseBytes = new TextEncoder().encode(responsePayload);
|
|
69101
|
-
const cipher = await
|
|
69228
|
+
const cipher = await this._encryptWithCatchUp(
|
|
69229
|
+
mgr,
|
|
69230
|
+
mgrKey,
|
|
69231
|
+
mlsGroupId,
|
|
69232
|
+
`history catch-up ${mlsGroupId.slice(0, 8)}`,
|
|
69233
|
+
responseBytes
|
|
69234
|
+
);
|
|
69102
69235
|
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mgr.exportState()));
|
|
69103
69236
|
if (this._ws) {
|
|
69104
69237
|
this._ws.send(JSON.stringify({
|
|
@@ -69841,9 +69974,36 @@ ${messageText}`;
|
|
|
69841
69974
|
// ---------------------------------------------------------------------------
|
|
69842
69975
|
// MLS room message handlers
|
|
69843
69976
|
// ---------------------------------------------------------------------------
|
|
69977
|
+
/**
|
|
69978
|
+
* #1014: hand a room-framed frame to the handler that owns its group family.
|
|
69979
|
+
*
|
|
69980
|
+
* Returns true when the frame was routed. Routing means "give it to the right
|
|
69981
|
+
* handler" — NOT "decrypt it as a room message". Decrypting under the wrong
|
|
69982
|
+
* group is how MLS state gets corrupted, so an unplaceable group is left for
|
|
69983
|
+
* the caller to self-heal rather than guessed at.
|
|
69984
|
+
*/
|
|
69985
|
+
async _routeNonRoomGroupMessage(data, groupId) {
|
|
69986
|
+
const target = this._resolveGroupFamily(groupId);
|
|
69987
|
+
if (!target || target.family === "room") return false;
|
|
69988
|
+
console.log(
|
|
69989
|
+
`[SecureChannel] Room-framed message belongs to ${target.label} \u2014 routing to the handler that owns it (#1014)`
|
|
69990
|
+
);
|
|
69991
|
+
switch (target.family) {
|
|
69992
|
+
case "shared1to1":
|
|
69993
|
+
await this._handleMessageMLS({ ...data, conversation_group_id: target.id });
|
|
69994
|
+
return true;
|
|
69995
|
+
case "conversation":
|
|
69996
|
+
await this._handleMessageMLS({ ...data, conversation_id: target.id });
|
|
69997
|
+
return true;
|
|
69998
|
+
case "a2a":
|
|
69999
|
+
await this._handleA2AMessageMLS({ ...data, a2a_channel_id: target.id });
|
|
70000
|
+
return true;
|
|
70001
|
+
}
|
|
70002
|
+
}
|
|
69844
70003
|
async _handleRoomMessageMLS(data) {
|
|
69845
70004
|
const groupId = data.group_id;
|
|
69846
70005
|
const senderDeviceId = data.sender_device_id;
|
|
70006
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
69847
70007
|
if (senderDeviceId === this._deviceId) return;
|
|
69848
70008
|
let roomId;
|
|
69849
70009
|
for (const [rid, room] of Object.entries(this._persisted?.rooms ?? {})) {
|
|
@@ -69857,7 +70017,9 @@ ${messageText}`;
|
|
|
69857
70017
|
roomId = data.room_id;
|
|
69858
70018
|
console.log(`[SecureChannel] Room ${roomId.slice(0, 8)} matched by room_id (group_id ${groupId?.slice(0, 8)} mismatch)`);
|
|
69859
70019
|
} else {
|
|
70020
|
+
if (await this._routeNonRoomGroupMessage(data, groupId)) return;
|
|
69860
70021
|
console.warn(`[SecureChannel] No room found for MLS group ${groupId?.slice(0, 8)}`);
|
|
70022
|
+
if (groupId) void this._requestWelcomeSelfHeal(groupId);
|
|
69861
70023
|
return;
|
|
69862
70024
|
}
|
|
69863
70025
|
}
|
|
@@ -70045,63 +70207,63 @@ ${messageText}`;
|
|
|
70045
70207
|
console.warn(`[SecureChannel] Failed to send history catchup response:`, sendErr);
|
|
70046
70208
|
}
|
|
70047
70209
|
}
|
|
70210
|
+
/**
|
|
70211
|
+
* Apply an inbound MLS commit to whichever group it addresses.
|
|
70212
|
+
*
|
|
70213
|
+
* Returns TRUE when the commit was applied, buffered, or handed to the
|
|
70214
|
+
* failure ladder — i.e. when this agent has taken responsibility for it and
|
|
70215
|
+
* the delivery-queue row may safely be acked. Returns FALSE when the commit
|
|
70216
|
+
* names a group we hold no state for at all, so the caller must NACK and let
|
|
70217
|
+
* the row survive.
|
|
70218
|
+
*
|
|
70219
|
+
* #1012: this used to loop `rooms` then `a2aChannels` and then END. A shared
|
|
70220
|
+
* 1:1 commit (`mlsGroups`) and a legacy per-conversation commit
|
|
70221
|
+
* (`mlsConversations`) matched neither. The call fell off the bottom with no
|
|
70222
|
+
* log, no buffer and no error — and because it did not THROW,
|
|
70223
|
+
* `_pullDeliveryQueue` acked the row and the server deleted it. An MLS commit
|
|
70224
|
+
* cannot be regenerated, so the agent stayed at epoch N while the group moved
|
|
70225
|
+
* to N+1 and every later send() encrypted at an epoch nobody could decrypt.
|
|
70226
|
+
*
|
|
70227
|
+
* The families are enumerated in ONE list resolved by ONE loop, because two
|
|
70228
|
+
* hand-copied loops are exactly how the 1:1 path fell two families behind.
|
|
70229
|
+
*/
|
|
70048
70230
|
async _handleMlsCommit(data) {
|
|
70049
70231
|
const groupId = data.group_id;
|
|
70050
70232
|
const epoch = typeof data.epoch === "number" ? data.epoch : 0;
|
|
70051
|
-
|
|
70052
|
-
|
|
70053
|
-
|
|
70054
|
-
|
|
70055
|
-
|
|
70056
|
-
|
|
70057
|
-
|
|
70058
|
-
|
|
70059
|
-
|
|
70060
|
-
this._mlsCommitFailCounts.delete(roomId);
|
|
70061
|
-
} catch (err) {
|
|
70062
|
-
await this._onCommitFailure(
|
|
70063
|
-
roomId,
|
|
70064
|
-
groupId,
|
|
70065
|
-
err,
|
|
70066
|
-
`room ${roomId.slice(0, 8)}`,
|
|
70067
|
-
data.epoch,
|
|
70068
|
-
mlsGroup.epoch
|
|
70069
|
-
);
|
|
70070
|
-
}
|
|
70071
|
-
} else {
|
|
70072
|
-
this._bufferMlsCommit(groupId, epoch, data);
|
|
70073
|
-
console.log(`[SecureChannel] Buffered MLS commit for room ${roomId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
|
|
70074
|
-
}
|
|
70075
|
-
return;
|
|
70233
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
70234
|
+
const target = this._resolveGroupFamily(groupId);
|
|
70235
|
+
if (target) {
|
|
70236
|
+
const { managerKey: groupKey, label } = target;
|
|
70237
|
+
const mlsGroup = this._mlsGroups.get(groupKey);
|
|
70238
|
+
if (!mlsGroup?.isInitialized) {
|
|
70239
|
+
this._bufferMlsCommit(groupId, epoch, data);
|
|
70240
|
+
console.log(`[SecureChannel] Buffered MLS commit for ${label} (epoch=${epoch}, group not initialized)`);
|
|
70241
|
+
return true;
|
|
70076
70242
|
}
|
|
70077
|
-
|
|
70078
|
-
|
|
70079
|
-
|
|
70080
|
-
|
|
70081
|
-
|
|
70082
|
-
|
|
70083
|
-
|
|
70084
|
-
|
|
70085
|
-
|
|
70086
|
-
|
|
70087
|
-
|
|
70088
|
-
|
|
70089
|
-
|
|
70090
|
-
|
|
70091
|
-
|
|
70092
|
-
err,
|
|
70093
|
-
`A2A ${chId.slice(0, 8)}`,
|
|
70094
|
-
data.epoch,
|
|
70095
|
-
mlsGroup.epoch
|
|
70096
|
-
);
|
|
70097
|
-
}
|
|
70098
|
-
} else {
|
|
70099
|
-
this._bufferMlsCommit(groupId, epoch, data);
|
|
70100
|
-
console.log(`[SecureChannel] Buffered MLS commit for A2A ${chId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
|
|
70101
|
-
}
|
|
70102
|
-
return;
|
|
70243
|
+
try {
|
|
70244
|
+
const commitBytes = new Uint8Array(Buffer.from(data.payload, "hex"));
|
|
70245
|
+
await mlsGroup.processCommit(commitBytes);
|
|
70246
|
+
await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
|
|
70247
|
+
console.log(`[SecureChannel] MLS commit processed for ${label} (epoch=${mlsGroup.epoch})`);
|
|
70248
|
+
this._mlsCommitFailCounts.delete(groupKey);
|
|
70249
|
+
} catch (err) {
|
|
70250
|
+
await this._onCommitFailure(
|
|
70251
|
+
groupKey,
|
|
70252
|
+
groupId,
|
|
70253
|
+
err,
|
|
70254
|
+
label,
|
|
70255
|
+
data.epoch,
|
|
70256
|
+
mlsGroup.epoch
|
|
70257
|
+
);
|
|
70103
70258
|
}
|
|
70259
|
+
return true;
|
|
70104
70260
|
}
|
|
70261
|
+
this._bufferMlsCommit(groupId, epoch, data);
|
|
70262
|
+
console.warn(
|
|
70263
|
+
`[SecureChannel] MLS commit for UNKNOWN group ${String(groupId).slice(0, 8)} (epoch=${epoch}) \u2014 buffered, requesting a Welcome, NOT acked`
|
|
70264
|
+
);
|
|
70265
|
+
if (groupId) void this._requestWelcomeSelfHeal(groupId);
|
|
70266
|
+
return false;
|
|
70105
70267
|
}
|
|
70106
70268
|
/** Buffer an MLS commit for replay after Welcome join. Max 50 per group. */
|
|
70107
70269
|
_bufferMlsCommit(groupId, epoch, data) {
|
|
@@ -70291,6 +70453,229 @@ ${messageText}`;
|
|
|
70291
70453
|
* `1to1-group:<gid>`, `a2a:<channelId>`, …) — also the decrypt-fail-count key.
|
|
70292
70454
|
* @param groupId the server-side MLS group id used in the /sync URL.
|
|
70293
70455
|
*/
|
|
70456
|
+
/**
|
|
70457
|
+
* #1015 — the ONLY place this package fulfils a Welcome request.
|
|
70458
|
+
*
|
|
70459
|
+
* Add the members, transmit, and record `fulfilled` **only for the targets
|
|
70460
|
+
* whose frames actually went out**. Every one of the five sites that used to
|
|
70461
|
+
* do this by hand followed the same sequence and had the same three holes:
|
|
70462
|
+
* `this._ws` null (both sends skipped by an `if`), a CLOSING socket (`send`
|
|
70463
|
+
* returns without throwing), and a fulfil POST wrapped in `.catch(() => {})`.
|
|
70464
|
+
*
|
|
70465
|
+
* A fulfilled request is never re-requested. So a Welcome that evaporated
|
|
70466
|
+
* left the requester locked out, the fulfiller at epoch N+1, everyone else at
|
|
70467
|
+
* N, and the bridging commit untransmitted and impossible to regenerate — a
|
|
70468
|
+
* permanently wedged group in which every signal read success.
|
|
70469
|
+
*
|
|
70470
|
+
* Returns the number of requests actually fulfilled.
|
|
70471
|
+
*/
|
|
70472
|
+
async _fulfilWelcomeRequests(opts) {
|
|
70473
|
+
const { mlsGroup, mlsGroupId, label, welcomeFields, targets } = opts;
|
|
70474
|
+
if (targets.length === 0) return 0;
|
|
70475
|
+
if (!this._wsIsOpen()) {
|
|
70476
|
+
console.warn(
|
|
70477
|
+
`[SecureChannel] ${label}: socket is not OPEN \u2014 NOT advancing the group; ${targets.length} request(s) stay pending for the next pull`
|
|
70478
|
+
);
|
|
70479
|
+
return 0;
|
|
70480
|
+
}
|
|
70481
|
+
const snapshot = mlsGroup.exportState();
|
|
70482
|
+
const { commit, welcome } = await mlsGroup.addMembers(targets.map((t2) => t2.keyPackage));
|
|
70483
|
+
const commitSent = await this._sendFrameConfirmed(
|
|
70484
|
+
{
|
|
70485
|
+
event: "mls_commit",
|
|
70486
|
+
data: {
|
|
70487
|
+
group_id: mlsGroupId,
|
|
70488
|
+
epoch: Number(mlsGroup.epoch),
|
|
70489
|
+
payload: Buffer.from(commit).toString("hex")
|
|
70490
|
+
}
|
|
70491
|
+
},
|
|
70492
|
+
`${label} commit`
|
|
70493
|
+
);
|
|
70494
|
+
if (!commitSent) {
|
|
70495
|
+
mlsGroup.importState(snapshot);
|
|
70496
|
+
console.warn(
|
|
70497
|
+
`[SecureChannel] ${label}: commit was NOT written \u2014 rolled back to epoch ${Number(mlsGroup.epoch)}, nothing fulfilled, requests stay pending`
|
|
70498
|
+
);
|
|
70499
|
+
return 0;
|
|
70500
|
+
}
|
|
70501
|
+
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70502
|
+
if (!welcome) {
|
|
70503
|
+
console.warn(
|
|
70504
|
+
`[SecureChannel] ${label}: addMembers produced NO Welcome \u2014 ${targets.length} request(s) left pending rather than marked fulfilled`
|
|
70505
|
+
);
|
|
70506
|
+
return 0;
|
|
70507
|
+
}
|
|
70508
|
+
let fulfilled = 0;
|
|
70509
|
+
for (const t2 of targets) {
|
|
70510
|
+
const sent = await this._sendFrameConfirmed(
|
|
70511
|
+
{
|
|
70512
|
+
event: "mls_welcome",
|
|
70513
|
+
data: {
|
|
70514
|
+
target_device_id: t2.deviceId,
|
|
70515
|
+
group_id: mlsGroupId,
|
|
70516
|
+
...welcomeFields,
|
|
70517
|
+
payload: Buffer.from(welcome).toString("hex")
|
|
70518
|
+
}
|
|
70519
|
+
},
|
|
70520
|
+
`${label} welcome\u2192${t2.deviceId.slice(0, 8)}`
|
|
70521
|
+
);
|
|
70522
|
+
if (!sent) {
|
|
70523
|
+
console.warn(
|
|
70524
|
+
`[SecureChannel] ${label}: Welcome for ${t2.deviceId.slice(0, 8)} was NOT written \u2014 leaving the request PENDING so the next pull retries it`
|
|
70525
|
+
);
|
|
70526
|
+
continue;
|
|
70527
|
+
}
|
|
70528
|
+
if (await this._postFulfilWelcome(mlsGroupId, t2.requestId, `${label} ${t2.deviceId.slice(0, 8)}`)) {
|
|
70529
|
+
fulfilled++;
|
|
70530
|
+
}
|
|
70531
|
+
}
|
|
70532
|
+
return fulfilled;
|
|
70533
|
+
}
|
|
70534
|
+
/**
|
|
70535
|
+
* Resolve an MLS group id to the family that owns it.
|
|
70536
|
+
*
|
|
70537
|
+
* There are four: rooms, A2A channels, shared 1:1 groups, and the legacy
|
|
70538
|
+
* per-conversation groups. They all store the MLS group id under
|
|
70539
|
+
* `mlsGroupId`; they differ only in the key their live `MLSGroupManager` sits
|
|
70540
|
+
* under in `_mlsGroups`.
|
|
70541
|
+
*
|
|
70542
|
+
* ONE resolver, because hand-copying this enumeration is what produced
|
|
70543
|
+
* #1012 (`_handleMlsCommit` handled two families of four), #1014 (a
|
|
70544
|
+
* room-framed 1:1 replay matched nothing) and #1016 (a rejoin dropped only
|
|
70545
|
+
* the room manager). Three bugs, one missing abstraction.
|
|
70546
|
+
*/
|
|
70547
|
+
_resolveGroupFamily(mlsGroupId) {
|
|
70548
|
+
if (!mlsGroupId) return null;
|
|
70549
|
+
const families = [
|
|
70550
|
+
{ family: "room", entries: this._persisted?.rooms, managerKey: (id) => id, label: "room" },
|
|
70551
|
+
{ family: "a2a", entries: this._persisted?.a2aChannels, managerKey: (id) => `a2a:${id}`, label: "A2A" },
|
|
70552
|
+
{ family: "shared1to1", entries: this._persisted?.mlsGroups, managerKey: (id) => `1to1-group:${id}`, label: "1:1 group" },
|
|
70553
|
+
{ family: "conversation", entries: this._persisted?.mlsConversations, managerKey: (id) => `conv:${id}`, label: "conversation" }
|
|
70554
|
+
];
|
|
70555
|
+
for (const f2 of families) {
|
|
70556
|
+
for (const [id, entry] of Object.entries(f2.entries ?? {})) {
|
|
70557
|
+
if (entry?.mlsGroupId !== mlsGroupId) continue;
|
|
70558
|
+
return {
|
|
70559
|
+
family: f2.family,
|
|
70560
|
+
id,
|
|
70561
|
+
managerKey: f2.managerKey(id),
|
|
70562
|
+
label: `${f2.label} ${id.slice(0, 8)}`
|
|
70563
|
+
};
|
|
70564
|
+
}
|
|
70565
|
+
}
|
|
70566
|
+
return null;
|
|
70567
|
+
}
|
|
70568
|
+
/** #1015: is the socket in a state that can actually take a write? */
|
|
70569
|
+
_wsIsOpen() {
|
|
70570
|
+
return this._ws?.readyState === 1;
|
|
70571
|
+
}
|
|
70572
|
+
/**
|
|
70573
|
+
* #1015: write one frame and report whether the socket took it.
|
|
70574
|
+
*
|
|
70575
|
+
* `ws.send()` on a CLOSING or CLOSED socket returns WITHOUT throwing — the
|
|
70576
|
+
* error surfaces only through the callback, and every fulfilment site passed
|
|
70577
|
+
* none. So a Welcome could be "sent" into a dead socket and the requester
|
|
70578
|
+
* still marked `fulfilled`, which it never re-requests.
|
|
70579
|
+
*/
|
|
70580
|
+
_sendFrameConfirmed(frame, label) {
|
|
70581
|
+
const ws = this._ws;
|
|
70582
|
+
if (!ws || ws.readyState !== 1) {
|
|
70583
|
+
console.warn(
|
|
70584
|
+
`[SecureChannel] ${label}: socket is not OPEN (readyState=${ws?.readyState ?? "null"}) \u2014 frame NOT written`
|
|
70585
|
+
);
|
|
70586
|
+
return Promise.resolve(false);
|
|
70587
|
+
}
|
|
70588
|
+
return new Promise((resolve3) => {
|
|
70589
|
+
try {
|
|
70590
|
+
ws.send(JSON.stringify(frame), (err) => {
|
|
70591
|
+
if (err) {
|
|
70592
|
+
console.error(`[SecureChannel] ${label}: socket refused the frame:`, err);
|
|
70593
|
+
resolve3(false);
|
|
70594
|
+
} else {
|
|
70595
|
+
resolve3(true);
|
|
70596
|
+
}
|
|
70597
|
+
});
|
|
70598
|
+
} catch (err) {
|
|
70599
|
+
console.error(`[SecureChannel] ${label}: send threw:`, err);
|
|
70600
|
+
resolve3(false);
|
|
70601
|
+
}
|
|
70602
|
+
});
|
|
70603
|
+
}
|
|
70604
|
+
/**
|
|
70605
|
+
* #1015: record a Welcome as fulfilled — and say so when it fails.
|
|
70606
|
+
*
|
|
70607
|
+
* The batched shared-1:1 site posted this with `.catch(() => {})`, so an HTTP
|
|
70608
|
+
* failure here was invisible on top of the sends being invisible.
|
|
70609
|
+
*/
|
|
70610
|
+
async _postFulfilWelcome(mlsGroupId, requestId, label) {
|
|
70611
|
+
try {
|
|
70612
|
+
const res = await fetch(
|
|
70613
|
+
`${this.config.apiUrl}/api/v1/mls/groups/${mlsGroupId}/fulfill-welcome`,
|
|
70614
|
+
{
|
|
70615
|
+
method: "POST",
|
|
70616
|
+
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70617
|
+
body: JSON.stringify({ request_id: requestId })
|
|
70618
|
+
}
|
|
70619
|
+
);
|
|
70620
|
+
if (!res.ok) {
|
|
70621
|
+
console.warn(`[SecureChannel] ${label}: fulfill-welcome returned ${res.status}`);
|
|
70622
|
+
return false;
|
|
70623
|
+
}
|
|
70624
|
+
return true;
|
|
70625
|
+
} catch (err) {
|
|
70626
|
+
console.warn(`[SecureChannel] ${label}: fulfill-welcome failed:`, err);
|
|
70627
|
+
return false;
|
|
70628
|
+
}
|
|
70629
|
+
}
|
|
70630
|
+
/**
|
|
70631
|
+
* #1013: fold an epoch seen on an inbound frame into the running hint.
|
|
70632
|
+
*
|
|
70633
|
+
* Anything unusable (missing, non-numeric, negative, fractional) is ignored
|
|
70634
|
+
* rather than defaulted. A defaulted epoch here would be a guess, and a guess
|
|
70635
|
+
* that reads LOW silently disables the check it exists to drive.
|
|
70636
|
+
*/
|
|
70637
|
+
_noteGroupEpoch(mlsGroupId, epoch) {
|
|
70638
|
+
if (typeof mlsGroupId !== "string" || !mlsGroupId) return;
|
|
70639
|
+
const current = this._observedGroupEpochs.get(mlsGroupId) ?? 0;
|
|
70640
|
+
const next = noteObservedEpoch(current, epoch);
|
|
70641
|
+
if (next > current) this._observedGroupEpochs.set(mlsGroupId, next);
|
|
70642
|
+
}
|
|
70643
|
+
/**
|
|
70644
|
+
* #1013 — the ONLY place this package encrypts an MLS application message.
|
|
70645
|
+
*
|
|
70646
|
+
* Catch up first when the group is known to have moved past us, then encrypt.
|
|
70647
|
+
* Hermes has done this since #440 and the browser since #996; the plugin —
|
|
70648
|
+
* the implementation that actually ships to customers — did not, so an agent
|
|
70649
|
+
* that missed one commit encrypted every later message at an epoch nobody
|
|
70650
|
+
* could read, while the server stored and relayed it and every signal read
|
|
70651
|
+
* 200.
|
|
70652
|
+
*
|
|
70653
|
+
* Funnelling every sender through one method is the point. A guard applied at
|
|
70654
|
+
* seven call sites decays to a guard applied at six: that is exactly how
|
|
70655
|
+
* `_handleMlsCommit` ended up handling two group families out of four (#1012).
|
|
70656
|
+
*
|
|
70657
|
+
* Best-effort by design. A catch-up that cannot replay — or that throws — must
|
|
70658
|
+
* NOT block the send; we fall through and encrypt at the local epoch, which is
|
|
70659
|
+
* precisely today's behaviour, so the worst case is no regression.
|
|
70660
|
+
*/
|
|
70661
|
+
async _encryptWithCatchUp(mlsGroup, groupKey, mlsGroupId, label, plaintextBytes) {
|
|
70662
|
+
const observed = this._observedGroupEpochs.get(mlsGroupId);
|
|
70663
|
+
if (observed !== void 0 && needsPreSendResync(Number(mlsGroup.epoch), observed)) {
|
|
70664
|
+
console.warn(
|
|
70665
|
+
`[SecureChannel] ${label}: local epoch ${Number(mlsGroup.epoch)} is behind the observed group epoch ${observed} \u2014 catching up before encrypt (#1013)`
|
|
70666
|
+
);
|
|
70667
|
+
try {
|
|
70668
|
+
if (!await this._resyncOnDivergence(groupKey, mlsGroupId)) {
|
|
70669
|
+
console.warn(
|
|
70670
|
+
`[SecureChannel] ${label}: catch-up could not replay \u2014 sending at epoch ${Number(mlsGroup.epoch)}. Peers past this epoch will not read it.`
|
|
70671
|
+
);
|
|
70672
|
+
}
|
|
70673
|
+
} catch (err) {
|
|
70674
|
+
console.warn(`[SecureChannel] ${label}: catch-up failed:`, err);
|
|
70675
|
+
}
|
|
70676
|
+
}
|
|
70677
|
+
return await mlsGroup.encrypt(plaintextBytes);
|
|
70678
|
+
}
|
|
70294
70679
|
async _resyncOnDivergence(groupKey, groupId) {
|
|
70295
70680
|
const mgr = this._mlsGroups.get(groupKey);
|
|
70296
70681
|
if (!mgr?.isInitialized) return false;
|
|
@@ -70317,11 +70702,24 @@ ${messageText}`;
|
|
|
70317
70702
|
applied++;
|
|
70318
70703
|
}
|
|
70319
70704
|
if (applied === 0) {
|
|
70320
|
-
|
|
70705
|
+
const strikes = (this._resyncNoOpStrikes.get(groupKey) ?? 0) + 1;
|
|
70706
|
+
this._resyncNoOpStrikes.set(groupKey, strikes);
|
|
70707
|
+
if (strikes < _SecureChannel.MAX_RESYNC_NOOPS) {
|
|
70708
|
+
this._mlsDecryptFailCounts.delete(groupKey);
|
|
70709
|
+
console.log(
|
|
70710
|
+
`[SecureChannel] Resync for ${groupId.slice(0, 8)}: already current at epoch=${mgr.epoch} (strike ${strikes}/${_SecureChannel.MAX_RESYNC_NOOPS}) \u2014 NOT re-keying (#1084)`
|
|
70711
|
+
);
|
|
70712
|
+
return true;
|
|
70713
|
+
}
|
|
70714
|
+
this._resyncNoOpStrikes.delete(groupKey);
|
|
70715
|
+
console.warn(
|
|
70716
|
+
`[SecureChannel] Resync for ${groupId.slice(0, 8)}: still failing at epoch=${mgr.epoch} after ${_SecureChannel.MAX_RESYNC_NOOPS} no-op resyncs \u2014 escalating to re-key`
|
|
70717
|
+
);
|
|
70321
70718
|
return false;
|
|
70322
70719
|
}
|
|
70323
70720
|
await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mgr.exportState()));
|
|
70324
70721
|
this._mlsDecryptFailCounts.delete(groupKey);
|
|
70722
|
+
this._resyncNoOpStrikes.delete(groupKey);
|
|
70325
70723
|
console.log(
|
|
70326
70724
|
`[SecureChannel] Resync-on-divergence: applied ${applied} missed commit(s) for ${groupId.slice(0, 8)}, now epoch=${mgr.epoch}`
|
|
70327
70725
|
);
|
|
@@ -70708,6 +71106,7 @@ ${messageText}`;
|
|
|
70708
71106
|
const ackedIds = [];
|
|
70709
71107
|
const nackedIds = [];
|
|
70710
71108
|
for (const msg of messages) {
|
|
71109
|
+
let handled = true;
|
|
70711
71110
|
try {
|
|
70712
71111
|
if (msg.sender_device_id === this._deviceId) {
|
|
70713
71112
|
ackedIds.push(msg.queue_id);
|
|
@@ -70724,7 +71123,7 @@ ${messageText}`;
|
|
|
70724
71123
|
a2a_channel_id: msg.a2a_channel_id
|
|
70725
71124
|
});
|
|
70726
71125
|
} else if (msg.message_type === "commit") {
|
|
70727
|
-
await this._handleMlsCommit({
|
|
71126
|
+
handled = await this._handleMlsCommit({
|
|
70728
71127
|
group_id: msg.group_id,
|
|
70729
71128
|
sender_device_id: msg.sender_device_id,
|
|
70730
71129
|
epoch: msg.epoch,
|
|
@@ -70762,9 +71161,18 @@ ${messageText}`;
|
|
|
70762
71161
|
conversation_group_id: msg.conversation_group_id,
|
|
70763
71162
|
created_at: msg.created_at
|
|
70764
71163
|
});
|
|
71164
|
+
} else {
|
|
71165
|
+
handled = false;
|
|
71166
|
+
console.warn(
|
|
71167
|
+
`[SecureChannel] Delivery application row ${String(msg.message_id).slice(0, 8)} names no room, A2A channel or conversation \u2014 NOT acked`
|
|
71168
|
+
);
|
|
70765
71169
|
}
|
|
70766
71170
|
}
|
|
70767
|
-
|
|
71171
|
+
if (handled) {
|
|
71172
|
+
ackedIds.push(msg.queue_id);
|
|
71173
|
+
} else {
|
|
71174
|
+
nackedIds.push(msg.queue_id);
|
|
71175
|
+
}
|
|
70768
71176
|
} catch (err) {
|
|
70769
71177
|
console.warn(`[SecureChannel] Delivery ${msg.message_type} processing failed:`, err);
|
|
70770
71178
|
nackedIds.push(msg.queue_id);
|
|
@@ -70833,38 +71241,20 @@ ${messageText}`;
|
|
|
70833
71241
|
if (!kpHex) continue;
|
|
70834
71242
|
const kpBytes = new Uint8Array(Buffer.from(kpHex, "hex"));
|
|
70835
71243
|
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
70836
|
-
const
|
|
70837
|
-
|
|
70838
|
-
|
|
70839
|
-
|
|
70840
|
-
|
|
70841
|
-
|
|
70842
|
-
|
|
70843
|
-
|
|
70844
|
-
|
|
70845
|
-
}
|
|
70846
|
-
}
|
|
70847
|
-
if (
|
|
70848
|
-
|
|
70849
|
-
event: "mls_welcome",
|
|
70850
|
-
data: {
|
|
70851
|
-
target_device_id: req.requesting_device_id,
|
|
70852
|
-
group_id: roomState.mlsGroupId,
|
|
70853
|
-
room_id: roomId,
|
|
70854
|
-
payload: Buffer.from(welcome).toString("hex")
|
|
70855
|
-
}
|
|
70856
|
-
}));
|
|
71244
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71245
|
+
mlsGroup,
|
|
71246
|
+
mlsGroupId: roomState.mlsGroupId,
|
|
71247
|
+
label: `room ${roomId.slice(0, 8)}`,
|
|
71248
|
+
welcomeFields: { room_id: roomId },
|
|
71249
|
+
targets: [{
|
|
71250
|
+
requestId: req.id,
|
|
71251
|
+
deviceId: req.requesting_device_id,
|
|
71252
|
+
keyPackage: memberKp
|
|
71253
|
+
}]
|
|
71254
|
+
});
|
|
71255
|
+
if (done > 0) {
|
|
71256
|
+
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in room ${roomId.slice(0, 8)}`);
|
|
70857
71257
|
}
|
|
70858
|
-
await fetch(
|
|
70859
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${roomState.mlsGroupId}/fulfill-welcome`,
|
|
70860
|
-
{
|
|
70861
|
-
method: "POST",
|
|
70862
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70863
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70864
|
-
}
|
|
70865
|
-
);
|
|
70866
|
-
await saveMlsState(this.config.dataDir, roomState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70867
|
-
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in room ${roomId.slice(0, 8)}`);
|
|
70868
71258
|
} catch (fulfillErr) {
|
|
70869
71259
|
console.warn(`[SecureChannel] Welcome fulfill failed for ${req.requesting_device_id.slice(0, 8)}:`, fulfillErr);
|
|
70870
71260
|
}
|
|
@@ -70903,65 +71293,38 @@ ${messageText}`;
|
|
|
70903
71293
|
validReqs.push({ req, kp: MLSGroupManager.deserializeKeyPackage(kpBytes) });
|
|
70904
71294
|
}
|
|
70905
71295
|
if (validReqs.length === 0) continue;
|
|
71296
|
+
const sharedTargets = validReqs.map((v2) => ({
|
|
71297
|
+
requestId: v2.req.id,
|
|
71298
|
+
deviceId: v2.req.requesting_device_id,
|
|
71299
|
+
keyPackage: v2.kp
|
|
71300
|
+
}));
|
|
70906
71301
|
try {
|
|
70907
|
-
const
|
|
70908
|
-
|
|
70909
|
-
|
|
70910
|
-
|
|
70911
|
-
|
|
70912
|
-
|
|
70913
|
-
|
|
70914
|
-
|
|
70915
|
-
|
|
70916
|
-
}));
|
|
70917
|
-
}
|
|
70918
|
-
if (welcome && this._ws) {
|
|
70919
|
-
for (const { req } of validReqs) {
|
|
70920
|
-
this._ws.send(JSON.stringify({
|
|
70921
|
-
event: "mls_welcome",
|
|
70922
|
-
data: {
|
|
70923
|
-
target_device_id: req.requesting_device_id,
|
|
70924
|
-
group_id: entry.mlsGroupId,
|
|
70925
|
-
conversation_group_id: gid,
|
|
70926
|
-
payload: Buffer.from(welcome).toString("hex")
|
|
70927
|
-
}
|
|
70928
|
-
}));
|
|
70929
|
-
}
|
|
70930
|
-
}
|
|
70931
|
-
for (const { req } of validReqs) {
|
|
70932
|
-
await fetch(
|
|
70933
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${entry.mlsGroupId}/fulfill-welcome`,
|
|
70934
|
-
{
|
|
70935
|
-
method: "POST",
|
|
70936
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70937
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70938
|
-
}
|
|
70939
|
-
).catch(() => {
|
|
70940
|
-
});
|
|
71302
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71303
|
+
mlsGroup,
|
|
71304
|
+
mlsGroupId: entry.mlsGroupId,
|
|
71305
|
+
label: `shared group ${gid.slice(0, 8)}`,
|
|
71306
|
+
welcomeFields: { conversation_group_id: gid },
|
|
71307
|
+
targets: sharedTargets
|
|
71308
|
+
});
|
|
71309
|
+
if (done > 0) {
|
|
71310
|
+
console.log(`[SecureChannel] Batched Welcome for ${done}/${validReqs.length} devices in shared group ${gid.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
|
|
70941
71311
|
}
|
|
70942
|
-
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70943
|
-
console.log(`[SecureChannel] Batched Welcome for ${validReqs.length} devices in shared group ${gid.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
|
|
70944
71312
|
} catch (batchErr) {
|
|
70945
71313
|
console.warn(`[SecureChannel] Batched Welcome failed for shared group ${gid.slice(0, 8)}, falling back to individual:`, batchErr);
|
|
70946
|
-
for (const
|
|
71314
|
+
for (const target of sharedTargets) {
|
|
70947
71315
|
try {
|
|
70948
|
-
const
|
|
70949
|
-
|
|
70950
|
-
|
|
70951
|
-
|
|
70952
|
-
|
|
70953
|
-
|
|
70954
|
-
}
|
|
70955
|
-
await fetch(`${this.config.apiUrl}/api/v1/mls/groups/${entry.mlsGroupId}/fulfill-welcome`, {
|
|
70956
|
-
method: "POST",
|
|
70957
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70958
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70959
|
-
}).catch(() => {
|
|
71316
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71317
|
+
mlsGroup,
|
|
71318
|
+
mlsGroupId: entry.mlsGroupId,
|
|
71319
|
+
label: `shared group ${gid.slice(0, 8)}`,
|
|
71320
|
+
welcomeFields: { conversation_group_id: gid },
|
|
71321
|
+
targets: [target]
|
|
70960
71322
|
});
|
|
70961
|
-
|
|
70962
|
-
|
|
71323
|
+
if (done > 0) {
|
|
71324
|
+
console.log(`[SecureChannel] Individual Welcome for ${target.deviceId.slice(0, 8)} in shared group ${gid.slice(0, 8)}`);
|
|
71325
|
+
}
|
|
70963
71326
|
} catch (indivErr) {
|
|
70964
|
-
console.warn(`[SecureChannel] Individual Welcome failed for ${
|
|
71327
|
+
console.warn(`[SecureChannel] Individual Welcome failed for ${target.deviceId.slice(0, 8)}:`, indivErr);
|
|
70965
71328
|
}
|
|
70966
71329
|
}
|
|
70967
71330
|
}
|
|
@@ -70993,38 +71356,20 @@ ${messageText}`;
|
|
|
70993
71356
|
if (!kpHex) continue;
|
|
70994
71357
|
const kpBytes = new Uint8Array(Buffer.from(kpHex, "hex"));
|
|
70995
71358
|
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
70996
|
-
const
|
|
70997
|
-
|
|
70998
|
-
|
|
70999
|
-
|
|
71000
|
-
|
|
71001
|
-
|
|
71002
|
-
|
|
71003
|
-
|
|
71004
|
-
|
|
71005
|
-
}
|
|
71006
|
-
}
|
|
71007
|
-
if (
|
|
71008
|
-
|
|
71009
|
-
event: "mls_welcome",
|
|
71010
|
-
data: {
|
|
71011
|
-
target_device_id: req.requesting_device_id,
|
|
71012
|
-
group_id: convEntry.mlsGroupId,
|
|
71013
|
-
conversation_id: convId,
|
|
71014
|
-
payload: Buffer.from(welcome).toString("hex")
|
|
71015
|
-
}
|
|
71016
|
-
}));
|
|
71359
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71360
|
+
mlsGroup,
|
|
71361
|
+
mlsGroupId: convEntry.mlsGroupId,
|
|
71362
|
+
label: `conv ${convId.slice(0, 8)}`,
|
|
71363
|
+
welcomeFields: { conversation_id: convId },
|
|
71364
|
+
targets: [{
|
|
71365
|
+
requestId: req.id,
|
|
71366
|
+
deviceId: req.requesting_device_id,
|
|
71367
|
+
keyPackage: memberKp
|
|
71368
|
+
}]
|
|
71369
|
+
});
|
|
71370
|
+
if (done > 0) {
|
|
71371
|
+
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in conv ${convId.slice(0, 8)}`);
|
|
71017
71372
|
}
|
|
71018
|
-
await fetch(
|
|
71019
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${convEntry.mlsGroupId}/fulfill-welcome`,
|
|
71020
|
-
{
|
|
71021
|
-
method: "POST",
|
|
71022
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
71023
|
-
body: JSON.stringify({ request_id: req.id })
|
|
71024
|
-
}
|
|
71025
|
-
);
|
|
71026
|
-
await saveMlsState(this.config.dataDir, convEntry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
71027
|
-
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in conv ${convId.slice(0, 8)}`);
|
|
71028
71373
|
} catch (fulfillErr) {
|
|
71029
71374
|
console.warn(`[SecureChannel] Conv Welcome fulfill failed for ${req.requesting_device_id.slice(0, 8)}:`, fulfillErr);
|
|
71030
71375
|
}
|
|
@@ -71205,12 +71550,13 @@ ${messageText}`;
|
|
|
71205
71550
|
} catch (distErr) {
|
|
71206
71551
|
console.warn(`[SecureChannel] A2A distribute network error:`, distErr);
|
|
71207
71552
|
}
|
|
71208
|
-
if (!distributed && this.
|
|
71209
|
-
|
|
71553
|
+
if (!distributed && this._wsIsOpen()) {
|
|
71554
|
+
const a2aLabel = `A2A ${chId.slice(0, 8)}`;
|
|
71555
|
+
const commitSent = await this._sendFrameConfirmed({
|
|
71210
71556
|
event: "mls_commit",
|
|
71211
71557
|
data: { group_id: chState.mlsGroupId, epoch: Number(mlsGroup.epoch), payload: commitHex }
|
|
71212
|
-
})
|
|
71213
|
-
this.
|
|
71558
|
+
}, `${a2aLabel} commit`);
|
|
71559
|
+
const welcomeSent = commitSent && await this._sendFrameConfirmed({
|
|
71214
71560
|
event: "mls_welcome",
|
|
71215
71561
|
data: {
|
|
71216
71562
|
target_device_id: req.requesting_device_id,
|
|
@@ -71218,7 +71564,11 @@ ${messageText}`;
|
|
|
71218
71564
|
a2a_channel_id: chId,
|
|
71219
71565
|
payload: welcomeHex
|
|
71220
71566
|
}
|
|
71221
|
-
})
|
|
71567
|
+
}, `${a2aLabel} welcome`);
|
|
71568
|
+
if (!welcomeSent) {
|
|
71569
|
+
console.warn(`[SecureChannel] ${a2aLabel}: Welcome frames were NOT written \u2014 leaving the request pending`);
|
|
71570
|
+
continue;
|
|
71571
|
+
}
|
|
71222
71572
|
try {
|
|
71223
71573
|
await fetch(
|
|
71224
71574
|
`${this.config.apiUrl}/api/v1/mls/groups/${chState.mlsGroupId}/distribute`,
|
|
@@ -71241,16 +71591,15 @@ ${messageText}`;
|
|
|
71241
71591
|
console.warn(`[SecureChannel] Cannot deliver Welcome \u2014 HTTP distribute and WS both unavailable`);
|
|
71242
71592
|
continue;
|
|
71243
71593
|
}
|
|
71244
|
-
await
|
|
71245
|
-
|
|
71246
|
-
|
|
71247
|
-
|
|
71248
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
71249
|
-
body: JSON.stringify({ request_id: req.id })
|
|
71250
|
-
}
|
|
71594
|
+
const a2aFulfilled = await this._postFulfilWelcome(
|
|
71595
|
+
chState.mlsGroupId,
|
|
71596
|
+
req.id,
|
|
71597
|
+
`A2A ${chId.slice(0, 8)}`
|
|
71251
71598
|
);
|
|
71252
71599
|
await saveMlsState(this.config.dataDir, chState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
71253
|
-
|
|
71600
|
+
if (a2aFulfilled) {
|
|
71601
|
+
console.log(`[SecureChannel] Fulfilled A2A Welcome for ${req.requesting_device_id.slice(0, 8)} in channel ${chId.slice(0, 8)}`);
|
|
71602
|
+
}
|
|
71254
71603
|
} catch (fulfillErr) {
|
|
71255
71604
|
console.log(`[SecureChannel] A2A Welcome fulfill failed: ${fulfillErr instanceof Error ? fulfillErr.message : String(fulfillErr)}`);
|
|
71256
71605
|
}
|
|
@@ -71424,12 +71773,12 @@ ${messageText}`;
|
|
|
71424
71773
|
console.log(`[SecureChannel] MLS sync: re-join needed for group ${groupId?.slice(0, 8)}`);
|
|
71425
71774
|
if (groupId) {
|
|
71426
71775
|
await deleteMlsState(this.config.dataDir, groupId);
|
|
71427
|
-
|
|
71428
|
-
|
|
71429
|
-
|
|
71430
|
-
|
|
71431
|
-
}
|
|
71776
|
+
const target = this._resolveGroupFamily(groupId);
|
|
71777
|
+
if (target) {
|
|
71778
|
+
this._mlsGroups.delete(target.managerKey);
|
|
71779
|
+
console.log(`[SecureChannel] MLS sync rejoin: dropped local state for ${target.label}`);
|
|
71432
71780
|
}
|
|
71781
|
+
void this._requestWelcomeSelfHeal(groupId);
|
|
71433
71782
|
}
|
|
71434
71783
|
} else if (action === "replay_complete") {
|
|
71435
71784
|
console.log(`[SecureChannel] MLS sync complete for group ${groupId?.slice(0, 8)} (${data.count} messages replayed)`);
|
|
@@ -71441,6 +71790,7 @@ ${messageText}`;
|
|
|
71441
71790
|
*/
|
|
71442
71791
|
async _handleA2AMessageMLS(data) {
|
|
71443
71792
|
const groupId = data.group_id;
|
|
71793
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
71444
71794
|
const channelId = data.channel_id ?? data.a2a_channel_id;
|
|
71445
71795
|
let a2aChannelId = channelId;
|
|
71446
71796
|
if (!a2aChannelId) {
|
|
@@ -98664,7 +99014,7 @@ var init_index = __esm2({
|
|
|
98664
99014
|
init_skill_invoker();
|
|
98665
99015
|
await init_skill_telemetry();
|
|
98666
99016
|
await init_policy_enforcer();
|
|
98667
|
-
VERSION = true ? "0.23.
|
|
99017
|
+
VERSION = true ? "0.23.27" : "0.0.0-dev";
|
|
98668
99018
|
}
|
|
98669
99019
|
});
|
|
98670
99020
|
await init_index();
|
|
@@ -99567,7 +99917,7 @@ async function main() {
|
|
|
99567
99917
|
const info = parseInfoFlags(process.argv.slice(2));
|
|
99568
99918
|
if (info.showVersion || info.showHelp) {
|
|
99569
99919
|
const version2 = buildClientVersion2(
|
|
99570
|
-
true ? "0.4.
|
|
99920
|
+
true ? "0.4.1" : void 0
|
|
99571
99921
|
);
|
|
99572
99922
|
if (info.showVersion) printLine(version2);
|
|
99573
99923
|
if (info.showHelp) {
|
|
@@ -99620,7 +99970,7 @@ async function main() {
|
|
|
99620
99970
|
agentName: cfg.agentName,
|
|
99621
99971
|
platform: "node",
|
|
99622
99972
|
clientVersion: buildClientVersion2(
|
|
99623
|
-
true ? "0.4.
|
|
99973
|
+
true ? "0.4.1" : void 0
|
|
99624
99974
|
)
|
|
99625
99975
|
});
|
|
99626
99976
|
channelRef = channel;
|
package/package.json
CHANGED