@agentvault/claude-bridge 0.8.0 → 0.8.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
|
@@ -45906,6 +45906,11 @@ var init_ratchet = __esm2({
|
|
|
45906
45906
|
return { header, headerSignature, ciphertext, nonce };
|
|
45907
45907
|
}
|
|
45908
45908
|
decrypt(message) {
|
|
45909
|
+
if (this.state.peerIdentityPublicKey) {
|
|
45910
|
+
if (!verifyHeaderSignature(message.header, message.headerSignature, this.state.peerIdentityPublicKey)) {
|
|
45911
|
+
throw new Error("Header signature verification failed");
|
|
45912
|
+
}
|
|
45913
|
+
}
|
|
45909
45914
|
const isV2 = message.envelopeVersion === "2.0.0" && message.encryptedHeader != null && message.headerNonce != null;
|
|
45910
45915
|
const skippedResult = this.trySkippedKeys(message, isV2);
|
|
45911
45916
|
if (skippedResult !== null) {
|
|
@@ -45919,14 +45924,14 @@ var init_ratchet = __esm2({
|
|
|
45919
45924
|
if (message.header.messageNumber === 0) {
|
|
45920
45925
|
try {
|
|
45921
45926
|
const { messageKey: testKey, headerKey: testHeaderKey, nextChainKey: nextChainKey2 } = kdfChainKey(this.state.rootKey);
|
|
45922
|
-
let
|
|
45927
|
+
let ad;
|
|
45923
45928
|
if (isV2) {
|
|
45924
45929
|
libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.encryptedHeader, null, message.headerNonce, testHeaderKey);
|
|
45925
|
-
|
|
45930
|
+
ad = message.encryptedHeader;
|
|
45926
45931
|
} else {
|
|
45927
|
-
|
|
45932
|
+
ad = serializeHeader(message.header);
|
|
45928
45933
|
}
|
|
45929
|
-
const ptBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext,
|
|
45934
|
+
const ptBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, ad, message.nonce, testKey);
|
|
45930
45935
|
this.state.dhReceivingPublicKey = message.header.dhPublicKey;
|
|
45931
45936
|
this.state.receivingChain = {
|
|
45932
45937
|
chainKey: nextChainKey2,
|
|
@@ -45946,13 +45951,7 @@ var init_ratchet = __esm2({
|
|
|
45946
45951
|
this.skipMessages(this.state.receivingChain, message.header.messageNumber, message.header.dhPublicKey, isV2);
|
|
45947
45952
|
const chain = this.state.receivingChain;
|
|
45948
45953
|
const { nextChainKey, messageKey, headerKey } = kdfChainKey(chain.chainKey);
|
|
45949
|
-
|
|
45950
|
-
chain.messageNumber++;
|
|
45951
|
-
if (this.state.peerIdentityPublicKey) {
|
|
45952
|
-
if (!verifyHeaderSignature(message.header, message.headerSignature, this.state.peerIdentityPublicKey)) {
|
|
45953
|
-
throw new Error("Header signature verification failed");
|
|
45954
|
-
}
|
|
45955
|
-
}
|
|
45954
|
+
let plaintextBytes;
|
|
45956
45955
|
if (isV2) {
|
|
45957
45956
|
try {
|
|
45958
45957
|
libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.encryptedHeader, null, message.headerNonce, headerKey);
|
|
@@ -45960,19 +45959,21 @@ var init_ratchet = __esm2({
|
|
|
45960
45959
|
throw new Error("V2 header decryption failed");
|
|
45961
45960
|
}
|
|
45962
45961
|
try {
|
|
45963
|
-
|
|
45964
|
-
return libsodium_wrappers_default.to_string(plaintextBytes);
|
|
45962
|
+
plaintextBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, message.encryptedHeader, message.nonce, messageKey);
|
|
45965
45963
|
} catch {
|
|
45966
45964
|
throw new Error("V2 decryption failed: ciphertext tampered or wrong key");
|
|
45967
45965
|
}
|
|
45966
|
+
} else {
|
|
45967
|
+
const ad = serializeHeader(message.header);
|
|
45968
|
+
try {
|
|
45969
|
+
plaintextBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, ad, message.nonce, messageKey);
|
|
45970
|
+
} catch {
|
|
45971
|
+
throw new Error("Decryption failed: ciphertext tampered or wrong key");
|
|
45972
|
+
}
|
|
45968
45973
|
}
|
|
45969
|
-
|
|
45970
|
-
|
|
45971
|
-
|
|
45972
|
-
return libsodium_wrappers_default.to_string(plaintextBytes);
|
|
45973
|
-
} catch {
|
|
45974
|
-
throw new Error("Decryption failed: ciphertext tampered or wrong key");
|
|
45975
|
-
}
|
|
45974
|
+
chain.chainKey = nextChainKey;
|
|
45975
|
+
chain.messageNumber++;
|
|
45976
|
+
return libsodium_wrappers_default.to_string(plaintextBytes);
|
|
45976
45977
|
}
|
|
45977
45978
|
dhRatchetReceive(theirDhPublic) {
|
|
45978
45979
|
this.state.previousSendingChainLength = this.state.sendingChain?.messageNumber ?? 0;
|
|
@@ -64146,6 +64147,23 @@ var init_mls_delivery_order = __esm2({
|
|
|
64146
64147
|
"use strict";
|
|
64147
64148
|
}
|
|
64148
64149
|
});
|
|
64150
|
+
function noteObservedEpoch(current, seen) {
|
|
64151
|
+
const n22 = Number(seen);
|
|
64152
|
+
if (!Number.isInteger(n22) || n22 < 0)
|
|
64153
|
+
return current;
|
|
64154
|
+
return n22 > current ? n22 : current;
|
|
64155
|
+
}
|
|
64156
|
+
function needsPreSendResync(localEpoch, observedEpoch) {
|
|
64157
|
+
if (!Number.isInteger(localEpoch) || !Number.isInteger(observedEpoch)) {
|
|
64158
|
+
return false;
|
|
64159
|
+
}
|
|
64160
|
+
return observedEpoch > localEpoch;
|
|
64161
|
+
}
|
|
64162
|
+
var init_mls_presend_epoch = __esm2({
|
|
64163
|
+
"../crypto/dist/mls-presend-epoch.js"() {
|
|
64164
|
+
"use strict";
|
|
64165
|
+
}
|
|
64166
|
+
});
|
|
64149
64167
|
var dist_exports = {};
|
|
64150
64168
|
__export2(dist_exports, {
|
|
64151
64169
|
AV_CREDENTIAL_CONTEXT: () => AV_CREDENTIAL_CONTEXT,
|
|
@@ -64215,7 +64233,9 @@ __export2(dist_exports, {
|
|
|
64215
64233
|
hexTransportToEncryptedMessage: () => hexTransportToEncryptedMessage,
|
|
64216
64234
|
issueCredential: () => issueCredential,
|
|
64217
64235
|
multibaseToPublicKey: () => multibaseToPublicKey,
|
|
64236
|
+
needsPreSendResync: () => needsPreSendResync,
|
|
64218
64237
|
normalizeBackupCode: () => normalizeBackupCode,
|
|
64238
|
+
noteObservedEpoch: () => noteObservedEpoch,
|
|
64219
64239
|
orderDeliveryBatch: () => orderDeliveryBatch,
|
|
64220
64240
|
parseTraceparent: () => parseTraceparent,
|
|
64221
64241
|
performX3DH: () => performX3DH,
|
|
@@ -64255,6 +64275,7 @@ var init_dist = __esm2({
|
|
|
64255
64275
|
init_mls_group();
|
|
64256
64276
|
await init_owner_sync();
|
|
64257
64277
|
init_mls_delivery_order();
|
|
64278
|
+
init_mls_presend_epoch();
|
|
64258
64279
|
}
|
|
64259
64280
|
});
|
|
64260
64281
|
async function ensureSecureDir(dir) {
|
|
@@ -64447,7 +64468,7 @@ var init_mls_kp_pool = __esm2({
|
|
|
64447
64468
|
}
|
|
64448
64469
|
});
|
|
64449
64470
|
function ownIdentity() {
|
|
64450
|
-
const v22 = true ? "0.23.
|
|
64471
|
+
const v22 = true ? "0.23.27" : FALLBACK;
|
|
64451
64472
|
return `${PACKAGE}@${v22}`;
|
|
64452
64473
|
}
|
|
64453
64474
|
function buildClientVersion(override) {
|
|
@@ -65363,6 +65384,7 @@ var init_channel = __esm2({
|
|
|
65363
65384
|
await init_libsodium_wrappers();
|
|
65364
65385
|
await init_dist();
|
|
65365
65386
|
await init_dist();
|
|
65387
|
+
init_mls_presend_epoch();
|
|
65366
65388
|
init_mls_state();
|
|
65367
65389
|
await init_mls_kp_pool();
|
|
65368
65390
|
init_client_version();
|
|
@@ -65476,6 +65498,17 @@ var init_channel = __esm2({
|
|
|
65476
65498
|
* self-heal rather than be logged and dropped. */
|
|
65477
65499
|
_mlsCommitFailCounts = /* @__PURE__ */ new Map();
|
|
65478
65500
|
static MAX_MLS_DECRYPT_FAILS = 3;
|
|
65501
|
+
/**
|
|
65502
|
+
* #1084: consecutive resyncs that found NOTHING to apply, per group.
|
|
65503
|
+
*
|
|
65504
|
+
* "No new commits" means the server holds no commit past our epoch — we are
|
|
65505
|
+
* NOT BEHIND. Treating that as corruption and re-keying is what drove the
|
|
65506
|
+
* epoch churn (see `_resyncOnDivergence`). But a state that is genuinely
|
|
65507
|
+
* broken while its epoch counter happens to match must still recover, so the
|
|
65508
|
+
* suppression is bounded rather than absolute.
|
|
65509
|
+
*/
|
|
65510
|
+
_resyncNoOpStrikes = /* @__PURE__ */ new Map();
|
|
65511
|
+
static MAX_RESYNC_NOOPS = 3;
|
|
65479
65512
|
/** Cached MLS KeyPackage bundle for this device (regenerated on each connect). */
|
|
65480
65513
|
_mlsKeyPackage = null;
|
|
65481
65514
|
/** Pending KeyPackage bundle from request-Welcome flow (used by _handleMlsWelcome). */
|
|
@@ -65513,6 +65546,13 @@ var init_channel = __esm2({
|
|
|
65513
65546
|
_kpPoolFilling = false;
|
|
65514
65547
|
/** Buffer for MLS commits received before Welcome (keyed by groupId, sorted by epoch). */
|
|
65515
65548
|
_pendingMlsCommits = /* @__PURE__ */ new Map();
|
|
65549
|
+
/**
|
|
65550
|
+
* #1013: the highest group epoch any inbound frame has reported, per MLS
|
|
65551
|
+
* group id. Every inbound frame carries the epoch the group was at when it
|
|
65552
|
+
* was produced, so each is a LOWER BOUND on the group's current epoch — the
|
|
65553
|
+
* hint is monotonic and only ever rises.
|
|
65554
|
+
*/
|
|
65555
|
+
_observedGroupEpochs = /* @__PURE__ */ new Map();
|
|
65516
65556
|
/** In-memory credential store for renter-provided credentials (never persisted). */
|
|
65517
65557
|
_credentialStore = new CredentialStore();
|
|
65518
65558
|
/** Rooms whose roster has been refreshed from the backend at least once in
|
|
@@ -66197,16 +66237,44 @@ var init_channel = __esm2({
|
|
|
66197
66237
|
const pendingWsSends = [];
|
|
66198
66238
|
const sentSharedGroupIds = /* @__PURE__ */ new Set();
|
|
66199
66239
|
const addressedMlsGroupIds = [];
|
|
66240
|
+
if (!(this._persisted?.mlsGroups && this._state === "ready" && this._ws)) {
|
|
66241
|
+
console.warn(
|
|
66242
|
+
`[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"}`
|
|
66243
|
+
);
|
|
66244
|
+
}
|
|
66200
66245
|
if (this._persisted?.mlsGroups && this._state === "ready" && this._ws) {
|
|
66201
66246
|
const targetSharedGid = targetConvId ? this._sessionGroupIds?.get(targetConvId) : void 0;
|
|
66247
|
+
console.log(
|
|
66248
|
+
`[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)"}`
|
|
66249
|
+
);
|
|
66250
|
+
const skipped = [];
|
|
66202
66251
|
for (const [gid, entry] of Object.entries(this._persisted.mlsGroups)) {
|
|
66203
|
-
if (targetConvId && gid !== targetSharedGid)
|
|
66204
|
-
|
|
66252
|
+
if (targetConvId && gid !== targetSharedGid) {
|
|
66253
|
+
skipped.push(`${gid.slice(0, 8)}:not-target`);
|
|
66254
|
+
continue;
|
|
66255
|
+
}
|
|
66256
|
+
if (!entry.mlsGroupId) {
|
|
66257
|
+
skipped.push(`${gid.slice(0, 8)}:no-mlsGroupId`);
|
|
66258
|
+
continue;
|
|
66259
|
+
}
|
|
66205
66260
|
const mlsGroup = this._mlsGroups.get(`1to1-group:${gid}`);
|
|
66206
|
-
if (!mlsGroup?.isInitialized
|
|
66261
|
+
if (!mlsGroup?.isInitialized) {
|
|
66262
|
+
skipped.push(`${gid.slice(0, 8)}:not-in-memory`);
|
|
66263
|
+
continue;
|
|
66264
|
+
}
|
|
66265
|
+
if (Number(mlsGroup.epoch) <= 0) {
|
|
66266
|
+
skipped.push(`${gid.slice(0, 8)}:epoch<=0`);
|
|
66267
|
+
continue;
|
|
66268
|
+
}
|
|
66207
66269
|
try {
|
|
66208
66270
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
66209
|
-
const cipherBytes = await
|
|
66271
|
+
const cipherBytes = await this._encryptWithCatchUp(
|
|
66272
|
+
mlsGroup,
|
|
66273
|
+
`1to1-group:${gid}`,
|
|
66274
|
+
entry.mlsGroupId,
|
|
66275
|
+
`1:1 group ${gid.slice(0, 8)}`,
|
|
66276
|
+
plaintextBytes
|
|
66277
|
+
);
|
|
66210
66278
|
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
66211
66279
|
const payload = {
|
|
66212
66280
|
group_id: entry.mlsGroupId,
|
|
@@ -66228,8 +66296,12 @@ var init_channel = __esm2({
|
|
|
66228
66296
|
console.log(`[SecureChannel] Shared MLS group send for group ${gid.slice(0, 8)} (${entry.mlsGroupId.slice(0, 8)})`);
|
|
66229
66297
|
} catch (err) {
|
|
66230
66298
|
console.error(`[SecureChannel] Shared MLS group send failed for ${gid.slice(0, 8)}:`, err);
|
|
66299
|
+
skipped.push(`${gid.slice(0, 8)}:encrypt-threw`);
|
|
66231
66300
|
}
|
|
66232
66301
|
}
|
|
66302
|
+
if (skipped.length > 0) {
|
|
66303
|
+
console.log(`[SecureChannel] send(): skipped ${skipped.length} group(s) \u2014 ${skipped.join(", ")}`);
|
|
66304
|
+
}
|
|
66233
66305
|
}
|
|
66234
66306
|
for (const [convId, session] of this._sessions) {
|
|
66235
66307
|
if (!session.activated) continue;
|
|
@@ -66249,7 +66321,14 @@ var init_channel = __esm2({
|
|
|
66249
66321
|
}
|
|
66250
66322
|
if (mlsGroup?.isInitialized && mlsGroupId && Number(mlsGroup.epoch) > 0 && this._state === "ready" && this._ws) {
|
|
66251
66323
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
66252
|
-
const
|
|
66324
|
+
const sessionGroupKey = convGroupId && this._mlsGroups.has(`1to1-group:${convGroupId}`) ? `1to1-group:${convGroupId}` : `conv:${convId}`;
|
|
66325
|
+
const cipherBytes = await this._encryptWithCatchUp(
|
|
66326
|
+
mlsGroup,
|
|
66327
|
+
sessionGroupKey,
|
|
66328
|
+
mlsGroupId,
|
|
66329
|
+
`conversation ${convId.slice(0, 8)}`,
|
|
66330
|
+
plaintextBytes
|
|
66331
|
+
);
|
|
66253
66332
|
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
66254
66333
|
const payload = {
|
|
66255
66334
|
conversation_id: convId,
|
|
@@ -66344,7 +66423,14 @@ var init_channel = __esm2({
|
|
|
66344
66423
|
}
|
|
66345
66424
|
try {
|
|
66346
66425
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
66347
|
-
const
|
|
66426
|
+
const mlsOnlyGroupKey = mlsOnlyConvGroupId && this._mlsGroups.has(`1to1-group:${mlsOnlyConvGroupId}`) ? `1to1-group:${mlsOnlyConvGroupId}` : `conv:${mlsConvId}`;
|
|
66427
|
+
const cipherBytes = await this._encryptWithCatchUp(
|
|
66428
|
+
mlsGroup,
|
|
66429
|
+
mlsOnlyGroupKey,
|
|
66430
|
+
resolvedMlsGroupId,
|
|
66431
|
+
`conversation ${mlsConvId.slice(0, 8)}`,
|
|
66432
|
+
plaintextBytes
|
|
66433
|
+
);
|
|
66348
66434
|
await saveMlsState(this.config.dataDir, resolvedMlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
66349
66435
|
const payload = {
|
|
66350
66436
|
conversation_id: mlsConvId,
|
|
@@ -66370,13 +66456,26 @@ var init_channel = __esm2({
|
|
|
66370
66456
|
}
|
|
66371
66457
|
}
|
|
66372
66458
|
}
|
|
66373
|
-
if (sentCount === 0
|
|
66374
|
-
console.warn(
|
|
66459
|
+
if (sentCount === 0) {
|
|
66460
|
+
console.warn(
|
|
66461
|
+
`[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})`
|
|
66462
|
+
);
|
|
66375
66463
|
}
|
|
66376
66464
|
await this._persistState();
|
|
66465
|
+
const wsReady = this._ws?.readyState === 1;
|
|
66466
|
+
if (pendingWsSends.length > 0 && !wsReady) {
|
|
66467
|
+
console.warn(
|
|
66468
|
+
`[SecureChannel] send(): ${pendingWsSends.length} frame(s) NOT written \u2014 socket readyState=${this._ws?.readyState ?? "null"} (1=OPEN)`
|
|
66469
|
+
);
|
|
66470
|
+
}
|
|
66377
66471
|
for (const frame of pendingWsSends) {
|
|
66378
66472
|
this._ws.send(frame);
|
|
66379
66473
|
}
|
|
66474
|
+
if (pendingWsSends.length > 0) {
|
|
66475
|
+
console.log(
|
|
66476
|
+
`[SecureChannel] send(): wrote ${pendingWsSends.length} frame(s) to the socket (sentCount=${sentCount}, readyState=${this._ws?.readyState ?? "null"})`
|
|
66477
|
+
);
|
|
66478
|
+
}
|
|
66380
66479
|
if (!options?.isResend) {
|
|
66381
66480
|
for (const mlsGroupId of addressedMlsGroupIds) {
|
|
66382
66481
|
this._rememberRetryCandidate(mlsGroupId, plaintext, options);
|
|
@@ -66426,7 +66525,7 @@ var init_channel = __esm2({
|
|
|
66426
66525
|
*/
|
|
66427
66526
|
sendActivitySpan(spanData) {
|
|
66428
66527
|
if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
|
|
66429
|
-
const pluginVersion = true ? "0.23.
|
|
66528
|
+
const pluginVersion = true ? "0.23.27" : "0.0.0-dev";
|
|
66430
66529
|
const agentName = this.config.agentName ?? "Agent";
|
|
66431
66530
|
const resource = {
|
|
66432
66531
|
"service.name": "agentvault-agent",
|
|
@@ -66636,7 +66735,13 @@ var init_channel = __esm2({
|
|
|
66636
66735
|
if (mlsGroup?.isInitialized) {
|
|
66637
66736
|
try {
|
|
66638
66737
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
66639
|
-
const ciphertext = await
|
|
66738
|
+
const ciphertext = await this._encryptWithCatchUp(
|
|
66739
|
+
mlsGroup,
|
|
66740
|
+
roomId,
|
|
66741
|
+
room.mlsGroupId,
|
|
66742
|
+
`room ${roomId.slice(0, 8)}`,
|
|
66743
|
+
plaintextBytes
|
|
66744
|
+
);
|
|
66640
66745
|
await saveMlsState(this.config.dataDir, room.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
66641
66746
|
if (this._state === "ready" && this._ws) {
|
|
66642
66747
|
this._ws.send(JSON.stringify({
|
|
@@ -66887,11 +66992,17 @@ var init_channel = __esm2({
|
|
|
66887
66992
|
const before = this._persisted.groupId;
|
|
66888
66993
|
this._sessionGroupIds = new Map(mine.map((r22) => [r22.id, r22.group_id]));
|
|
66889
66994
|
this._persisted.conversationGroupIds = Object.fromEntries(this._sessionGroupIds);
|
|
66890
|
-
const
|
|
66995
|
+
const joined = mine.find((r22) => this._persisted.mlsGroups?.[r22.group_id]);
|
|
66996
|
+
const primary = mine.find((r22) => r22.id === this._persisted.primaryConversationId) ?? joined ?? mine[0];
|
|
66997
|
+
if (!mine.some((r22) => r22.id === this._persisted.primaryConversationId)) {
|
|
66998
|
+
console.log(
|
|
66999
|
+
`[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"}`
|
|
67000
|
+
);
|
|
67001
|
+
}
|
|
66891
67002
|
this._persisted.groupId = primary.group_id;
|
|
66892
67003
|
this._persisted.primaryConversationId = primary.id;
|
|
66893
67004
|
const liveGroupIds = new Set(mine.map((r22) => r22.group_id));
|
|
66894
|
-
if (!liveGroupIds.has(staleGid)) {
|
|
67005
|
+
if (staleGid && staleMlsGroupId && !liveGroupIds.has(staleGid)) {
|
|
66895
67006
|
try {
|
|
66896
67007
|
await deleteMlsState(this.config.dataDir, staleMlsGroupId);
|
|
66897
67008
|
} catch {
|
|
@@ -66905,9 +67016,9 @@ var init_channel = __esm2({
|
|
|
66905
67016
|
}
|
|
66906
67017
|
await this._persistState();
|
|
66907
67018
|
console.log(
|
|
66908
|
-
`[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))`
|
|
67019
|
+
`[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))`
|
|
66909
67020
|
);
|
|
66910
|
-
return this._persisted.groupId !== before && before === staleGid;
|
|
67021
|
+
return staleGid !== void 0 && this._persisted.groupId !== before && before === staleGid;
|
|
66911
67022
|
}
|
|
66912
67023
|
/**
|
|
66913
67024
|
* Resend the message that a now-reconciled `unknown group` refusal killed (#732).
|
|
@@ -67815,7 +67926,13 @@ var init_channel = __esm2({
|
|
|
67815
67926
|
const mlsGroup = this._mlsGroups.get(`a2a:${channelEntry.channelId}`);
|
|
67816
67927
|
if (mlsGroup?.isInitialized) {
|
|
67817
67928
|
const plaintextBytes = new TextEncoder().encode(text);
|
|
67818
|
-
const ciphertext = await
|
|
67929
|
+
const ciphertext = await this._encryptWithCatchUp(
|
|
67930
|
+
mlsGroup,
|
|
67931
|
+
`a2a:${channelEntry.channelId}`,
|
|
67932
|
+
channelEntry.mlsGroupId,
|
|
67933
|
+
`A2A ${channelEntry.channelId.slice(0, 8)}`,
|
|
67934
|
+
plaintextBytes
|
|
67935
|
+
);
|
|
67819
67936
|
await saveMlsState(
|
|
67820
67937
|
this.config.dataDir,
|
|
67821
67938
|
channelEntry.mlsGroupId,
|
|
@@ -67863,7 +67980,13 @@ var init_channel = __esm2({
|
|
|
67863
67980
|
const mlsGroup = this._mlsGroups.get(`a2a:${channelId}`);
|
|
67864
67981
|
if (mlsGroup?.isInitialized) {
|
|
67865
67982
|
const plaintextBytes = new TextEncoder().encode(text);
|
|
67866
|
-
const ciphertext = await
|
|
67983
|
+
const ciphertext = await this._encryptWithCatchUp(
|
|
67984
|
+
mlsGroup,
|
|
67985
|
+
`a2a:${channelId}`,
|
|
67986
|
+
entry.mlsGroupId,
|
|
67987
|
+
`A2A ${channelId.slice(0, 8)}`,
|
|
67988
|
+
plaintextBytes
|
|
67989
|
+
);
|
|
67867
67990
|
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
67868
67991
|
this._ws.send(JSON.stringify({
|
|
67869
67992
|
event: "a2a_message_mls",
|
|
@@ -68309,6 +68432,9 @@ var init_channel = __esm2({
|
|
|
68309
68432
|
await this._pullDrDeliveryQueue();
|
|
68310
68433
|
await this._flushOutboundQueue();
|
|
68311
68434
|
this._setState("ready");
|
|
68435
|
+
void this._reconcileConversationGroups().catch((err) => {
|
|
68436
|
+
console.warn("[SecureChannel] Conversation-group reconcile failed:", err);
|
|
68437
|
+
});
|
|
68312
68438
|
void this._reconcileRoomsWithServer().catch(
|
|
68313
68439
|
(err) => console.warn(`[SecureChannel] room reconcile failed (ignored): ${err instanceof Error ? err.message : String(err)}`)
|
|
68314
68440
|
).then(() => this._quarantineOrphanedMlsGroups()).catch(
|
|
@@ -68336,7 +68462,7 @@ var init_channel = __esm2({
|
|
|
68336
68462
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
68337
68463
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
68338
68464
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
68339
|
-
pluginVersion: true ? "0.23.
|
|
68465
|
+
pluginVersion: true ? "0.23.27" : "0.0.0-dev"
|
|
68340
68466
|
});
|
|
68341
68467
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
68342
68468
|
}
|
|
@@ -68660,7 +68786,7 @@ var init_channel = __esm2({
|
|
|
68660
68786
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
68661
68787
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
68662
68788
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
68663
|
-
pluginVersion: true ? "0.23.
|
|
68789
|
+
pluginVersion: true ? "0.23.27" : "0.0.0-dev"
|
|
68664
68790
|
});
|
|
68665
68791
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
68666
68792
|
}
|
|
@@ -69026,6 +69152,7 @@ var init_channel = __esm2({
|
|
|
69026
69152
|
const convGroupId = data.conversation_group_id;
|
|
69027
69153
|
const groupId = data.group_id;
|
|
69028
69154
|
const senderDeviceId = data.sender_device_id;
|
|
69155
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
69029
69156
|
if (senderDeviceId === this._deviceId) return;
|
|
69030
69157
|
let mgr;
|
|
69031
69158
|
let mgrKey;
|
|
@@ -69103,9 +69230,15 @@ var init_channel = __esm2({
|
|
|
69103
69230
|
})),
|
|
69104
69231
|
no_history: filtered.length === 0
|
|
69105
69232
|
});
|
|
69106
|
-
if (mgr && mlsGroupId) {
|
|
69233
|
+
if (mgr && mgrKey && mlsGroupId) {
|
|
69107
69234
|
const responseBytes = new TextEncoder().encode(responsePayload);
|
|
69108
|
-
const cipher = await
|
|
69235
|
+
const cipher = await this._encryptWithCatchUp(
|
|
69236
|
+
mgr,
|
|
69237
|
+
mgrKey,
|
|
69238
|
+
mlsGroupId,
|
|
69239
|
+
`history catch-up ${mlsGroupId.slice(0, 8)}`,
|
|
69240
|
+
responseBytes
|
|
69241
|
+
);
|
|
69109
69242
|
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mgr.exportState()));
|
|
69110
69243
|
if (this._ws) {
|
|
69111
69244
|
this._ws.send(JSON.stringify({
|
|
@@ -69848,9 +69981,36 @@ ${messageText}`;
|
|
|
69848
69981
|
// ---------------------------------------------------------------------------
|
|
69849
69982
|
// MLS room message handlers
|
|
69850
69983
|
// ---------------------------------------------------------------------------
|
|
69984
|
+
/**
|
|
69985
|
+
* #1014: hand a room-framed frame to the handler that owns its group family.
|
|
69986
|
+
*
|
|
69987
|
+
* Returns true when the frame was routed. Routing means "give it to the right
|
|
69988
|
+
* handler" — NOT "decrypt it as a room message". Decrypting under the wrong
|
|
69989
|
+
* group is how MLS state gets corrupted, so an unplaceable group is left for
|
|
69990
|
+
* the caller to self-heal rather than guessed at.
|
|
69991
|
+
*/
|
|
69992
|
+
async _routeNonRoomGroupMessage(data, groupId) {
|
|
69993
|
+
const target = this._resolveGroupFamily(groupId);
|
|
69994
|
+
if (!target || target.family === "room") return false;
|
|
69995
|
+
console.log(
|
|
69996
|
+
`[SecureChannel] Room-framed message belongs to ${target.label} \u2014 routing to the handler that owns it (#1014)`
|
|
69997
|
+
);
|
|
69998
|
+
switch (target.family) {
|
|
69999
|
+
case "shared1to1":
|
|
70000
|
+
await this._handleMessageMLS({ ...data, conversation_group_id: target.id });
|
|
70001
|
+
return true;
|
|
70002
|
+
case "conversation":
|
|
70003
|
+
await this._handleMessageMLS({ ...data, conversation_id: target.id });
|
|
70004
|
+
return true;
|
|
70005
|
+
case "a2a":
|
|
70006
|
+
await this._handleA2AMessageMLS({ ...data, a2a_channel_id: target.id });
|
|
70007
|
+
return true;
|
|
70008
|
+
}
|
|
70009
|
+
}
|
|
69851
70010
|
async _handleRoomMessageMLS(data) {
|
|
69852
70011
|
const groupId = data.group_id;
|
|
69853
70012
|
const senderDeviceId = data.sender_device_id;
|
|
70013
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
69854
70014
|
if (senderDeviceId === this._deviceId) return;
|
|
69855
70015
|
let roomId;
|
|
69856
70016
|
for (const [rid, room] of Object.entries(this._persisted?.rooms ?? {})) {
|
|
@@ -69864,7 +70024,9 @@ ${messageText}`;
|
|
|
69864
70024
|
roomId = data.room_id;
|
|
69865
70025
|
console.log(`[SecureChannel] Room ${roomId.slice(0, 8)} matched by room_id (group_id ${groupId?.slice(0, 8)} mismatch)`);
|
|
69866
70026
|
} else {
|
|
70027
|
+
if (await this._routeNonRoomGroupMessage(data, groupId)) return;
|
|
69867
70028
|
console.warn(`[SecureChannel] No room found for MLS group ${groupId?.slice(0, 8)}`);
|
|
70029
|
+
if (groupId) void this._requestWelcomeSelfHeal(groupId);
|
|
69868
70030
|
return;
|
|
69869
70031
|
}
|
|
69870
70032
|
}
|
|
@@ -70052,63 +70214,63 @@ ${messageText}`;
|
|
|
70052
70214
|
console.warn(`[SecureChannel] Failed to send history catchup response:`, sendErr);
|
|
70053
70215
|
}
|
|
70054
70216
|
}
|
|
70217
|
+
/**
|
|
70218
|
+
* Apply an inbound MLS commit to whichever group it addresses.
|
|
70219
|
+
*
|
|
70220
|
+
* Returns TRUE when the commit was applied, buffered, or handed to the
|
|
70221
|
+
* failure ladder — i.e. when this agent has taken responsibility for it and
|
|
70222
|
+
* the delivery-queue row may safely be acked. Returns FALSE when the commit
|
|
70223
|
+
* names a group we hold no state for at all, so the caller must NACK and let
|
|
70224
|
+
* the row survive.
|
|
70225
|
+
*
|
|
70226
|
+
* #1012: this used to loop `rooms` then `a2aChannels` and then END. A shared
|
|
70227
|
+
* 1:1 commit (`mlsGroups`) and a legacy per-conversation commit
|
|
70228
|
+
* (`mlsConversations`) matched neither. The call fell off the bottom with no
|
|
70229
|
+
* log, no buffer and no error — and because it did not THROW,
|
|
70230
|
+
* `_pullDeliveryQueue` acked the row and the server deleted it. An MLS commit
|
|
70231
|
+
* cannot be regenerated, so the agent stayed at epoch N while the group moved
|
|
70232
|
+
* to N+1 and every later send() encrypted at an epoch nobody could decrypt.
|
|
70233
|
+
*
|
|
70234
|
+
* The families are enumerated in ONE list resolved by ONE loop, because two
|
|
70235
|
+
* hand-copied loops are exactly how the 1:1 path fell two families behind.
|
|
70236
|
+
*/
|
|
70055
70237
|
async _handleMlsCommit(data) {
|
|
70056
70238
|
const groupId = data.group_id;
|
|
70057
70239
|
const epoch = typeof data.epoch === "number" ? data.epoch : 0;
|
|
70058
|
-
|
|
70059
|
-
|
|
70060
|
-
|
|
70061
|
-
|
|
70062
|
-
|
|
70063
|
-
|
|
70064
|
-
|
|
70065
|
-
|
|
70066
|
-
|
|
70067
|
-
this._mlsCommitFailCounts.delete(roomId);
|
|
70068
|
-
} catch (err) {
|
|
70069
|
-
await this._onCommitFailure(
|
|
70070
|
-
roomId,
|
|
70071
|
-
groupId,
|
|
70072
|
-
err,
|
|
70073
|
-
`room ${roomId.slice(0, 8)}`,
|
|
70074
|
-
data.epoch,
|
|
70075
|
-
mlsGroup.epoch
|
|
70076
|
-
);
|
|
70077
|
-
}
|
|
70078
|
-
} else {
|
|
70079
|
-
this._bufferMlsCommit(groupId, epoch, data);
|
|
70080
|
-
console.log(`[SecureChannel] Buffered MLS commit for room ${roomId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
|
|
70081
|
-
}
|
|
70082
|
-
return;
|
|
70240
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
70241
|
+
const target = this._resolveGroupFamily(groupId);
|
|
70242
|
+
if (target) {
|
|
70243
|
+
const { managerKey: groupKey, label } = target;
|
|
70244
|
+
const mlsGroup = this._mlsGroups.get(groupKey);
|
|
70245
|
+
if (!mlsGroup?.isInitialized) {
|
|
70246
|
+
this._bufferMlsCommit(groupId, epoch, data);
|
|
70247
|
+
console.log(`[SecureChannel] Buffered MLS commit for ${label} (epoch=${epoch}, group not initialized)`);
|
|
70248
|
+
return true;
|
|
70083
70249
|
}
|
|
70084
|
-
|
|
70085
|
-
|
|
70086
|
-
|
|
70087
|
-
|
|
70088
|
-
|
|
70089
|
-
|
|
70090
|
-
|
|
70091
|
-
|
|
70092
|
-
|
|
70093
|
-
|
|
70094
|
-
|
|
70095
|
-
|
|
70096
|
-
|
|
70097
|
-
|
|
70098
|
-
|
|
70099
|
-
err,
|
|
70100
|
-
`A2A ${chId.slice(0, 8)}`,
|
|
70101
|
-
data.epoch,
|
|
70102
|
-
mlsGroup.epoch
|
|
70103
|
-
);
|
|
70104
|
-
}
|
|
70105
|
-
} else {
|
|
70106
|
-
this._bufferMlsCommit(groupId, epoch, data);
|
|
70107
|
-
console.log(`[SecureChannel] Buffered MLS commit for A2A ${chId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
|
|
70108
|
-
}
|
|
70109
|
-
return;
|
|
70250
|
+
try {
|
|
70251
|
+
const commitBytes = new Uint8Array(Buffer.from(data.payload, "hex"));
|
|
70252
|
+
await mlsGroup.processCommit(commitBytes);
|
|
70253
|
+
await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
|
|
70254
|
+
console.log(`[SecureChannel] MLS commit processed for ${label} (epoch=${mlsGroup.epoch})`);
|
|
70255
|
+
this._mlsCommitFailCounts.delete(groupKey);
|
|
70256
|
+
} catch (err) {
|
|
70257
|
+
await this._onCommitFailure(
|
|
70258
|
+
groupKey,
|
|
70259
|
+
groupId,
|
|
70260
|
+
err,
|
|
70261
|
+
label,
|
|
70262
|
+
data.epoch,
|
|
70263
|
+
mlsGroup.epoch
|
|
70264
|
+
);
|
|
70110
70265
|
}
|
|
70266
|
+
return true;
|
|
70111
70267
|
}
|
|
70268
|
+
this._bufferMlsCommit(groupId, epoch, data);
|
|
70269
|
+
console.warn(
|
|
70270
|
+
`[SecureChannel] MLS commit for UNKNOWN group ${String(groupId).slice(0, 8)} (epoch=${epoch}) \u2014 buffered, requesting a Welcome, NOT acked`
|
|
70271
|
+
);
|
|
70272
|
+
if (groupId) void this._requestWelcomeSelfHeal(groupId);
|
|
70273
|
+
return false;
|
|
70112
70274
|
}
|
|
70113
70275
|
/** Buffer an MLS commit for replay after Welcome join. Max 50 per group. */
|
|
70114
70276
|
_bufferMlsCommit(groupId, epoch, data) {
|
|
@@ -70298,6 +70460,229 @@ ${messageText}`;
|
|
|
70298
70460
|
* `1to1-group:<gid>`, `a2a:<channelId>`, …) — also the decrypt-fail-count key.
|
|
70299
70461
|
* @param groupId the server-side MLS group id used in the /sync URL.
|
|
70300
70462
|
*/
|
|
70463
|
+
/**
|
|
70464
|
+
* #1015 — the ONLY place this package fulfils a Welcome request.
|
|
70465
|
+
*
|
|
70466
|
+
* Add the members, transmit, and record `fulfilled` **only for the targets
|
|
70467
|
+
* whose frames actually went out**. Every one of the five sites that used to
|
|
70468
|
+
* do this by hand followed the same sequence and had the same three holes:
|
|
70469
|
+
* `this._ws` null (both sends skipped by an `if`), a CLOSING socket (`send`
|
|
70470
|
+
* returns without throwing), and a fulfil POST wrapped in `.catch(() => {})`.
|
|
70471
|
+
*
|
|
70472
|
+
* A fulfilled request is never re-requested. So a Welcome that evaporated
|
|
70473
|
+
* left the requester locked out, the fulfiller at epoch N+1, everyone else at
|
|
70474
|
+
* N, and the bridging commit untransmitted and impossible to regenerate — a
|
|
70475
|
+
* permanently wedged group in which every signal read success.
|
|
70476
|
+
*
|
|
70477
|
+
* Returns the number of requests actually fulfilled.
|
|
70478
|
+
*/
|
|
70479
|
+
async _fulfilWelcomeRequests(opts) {
|
|
70480
|
+
const { mlsGroup, mlsGroupId, label, welcomeFields, targets } = opts;
|
|
70481
|
+
if (targets.length === 0) return 0;
|
|
70482
|
+
if (!this._wsIsOpen()) {
|
|
70483
|
+
console.warn(
|
|
70484
|
+
`[SecureChannel] ${label}: socket is not OPEN \u2014 NOT advancing the group; ${targets.length} request(s) stay pending for the next pull`
|
|
70485
|
+
);
|
|
70486
|
+
return 0;
|
|
70487
|
+
}
|
|
70488
|
+
const snapshot = mlsGroup.exportState();
|
|
70489
|
+
const { commit, welcome } = await mlsGroup.addMembers(targets.map((t22) => t22.keyPackage));
|
|
70490
|
+
const commitSent = await this._sendFrameConfirmed(
|
|
70491
|
+
{
|
|
70492
|
+
event: "mls_commit",
|
|
70493
|
+
data: {
|
|
70494
|
+
group_id: mlsGroupId,
|
|
70495
|
+
epoch: Number(mlsGroup.epoch),
|
|
70496
|
+
payload: Buffer.from(commit).toString("hex")
|
|
70497
|
+
}
|
|
70498
|
+
},
|
|
70499
|
+
`${label} commit`
|
|
70500
|
+
);
|
|
70501
|
+
if (!commitSent) {
|
|
70502
|
+
mlsGroup.importState(snapshot);
|
|
70503
|
+
console.warn(
|
|
70504
|
+
`[SecureChannel] ${label}: commit was NOT written \u2014 rolled back to epoch ${Number(mlsGroup.epoch)}, nothing fulfilled, requests stay pending`
|
|
70505
|
+
);
|
|
70506
|
+
return 0;
|
|
70507
|
+
}
|
|
70508
|
+
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70509
|
+
if (!welcome) {
|
|
70510
|
+
console.warn(
|
|
70511
|
+
`[SecureChannel] ${label}: addMembers produced NO Welcome \u2014 ${targets.length} request(s) left pending rather than marked fulfilled`
|
|
70512
|
+
);
|
|
70513
|
+
return 0;
|
|
70514
|
+
}
|
|
70515
|
+
let fulfilled = 0;
|
|
70516
|
+
for (const t22 of targets) {
|
|
70517
|
+
const sent = await this._sendFrameConfirmed(
|
|
70518
|
+
{
|
|
70519
|
+
event: "mls_welcome",
|
|
70520
|
+
data: {
|
|
70521
|
+
target_device_id: t22.deviceId,
|
|
70522
|
+
group_id: mlsGroupId,
|
|
70523
|
+
...welcomeFields,
|
|
70524
|
+
payload: Buffer.from(welcome).toString("hex")
|
|
70525
|
+
}
|
|
70526
|
+
},
|
|
70527
|
+
`${label} welcome\u2192${t22.deviceId.slice(0, 8)}`
|
|
70528
|
+
);
|
|
70529
|
+
if (!sent) {
|
|
70530
|
+
console.warn(
|
|
70531
|
+
`[SecureChannel] ${label}: Welcome for ${t22.deviceId.slice(0, 8)} was NOT written \u2014 leaving the request PENDING so the next pull retries it`
|
|
70532
|
+
);
|
|
70533
|
+
continue;
|
|
70534
|
+
}
|
|
70535
|
+
if (await this._postFulfilWelcome(mlsGroupId, t22.requestId, `${label} ${t22.deviceId.slice(0, 8)}`)) {
|
|
70536
|
+
fulfilled++;
|
|
70537
|
+
}
|
|
70538
|
+
}
|
|
70539
|
+
return fulfilled;
|
|
70540
|
+
}
|
|
70541
|
+
/**
|
|
70542
|
+
* Resolve an MLS group id to the family that owns it.
|
|
70543
|
+
*
|
|
70544
|
+
* There are four: rooms, A2A channels, shared 1:1 groups, and the legacy
|
|
70545
|
+
* per-conversation groups. They all store the MLS group id under
|
|
70546
|
+
* `mlsGroupId`; they differ only in the key their live `MLSGroupManager` sits
|
|
70547
|
+
* under in `_mlsGroups`.
|
|
70548
|
+
*
|
|
70549
|
+
* ONE resolver, because hand-copying this enumeration is what produced
|
|
70550
|
+
* #1012 (`_handleMlsCommit` handled two families of four), #1014 (a
|
|
70551
|
+
* room-framed 1:1 replay matched nothing) and #1016 (a rejoin dropped only
|
|
70552
|
+
* the room manager). Three bugs, one missing abstraction.
|
|
70553
|
+
*/
|
|
70554
|
+
_resolveGroupFamily(mlsGroupId) {
|
|
70555
|
+
if (!mlsGroupId) return null;
|
|
70556
|
+
const families = [
|
|
70557
|
+
{ family: "room", entries: this._persisted?.rooms, managerKey: (id) => id, label: "room" },
|
|
70558
|
+
{ family: "a2a", entries: this._persisted?.a2aChannels, managerKey: (id) => `a2a:${id}`, label: "A2A" },
|
|
70559
|
+
{ family: "shared1to1", entries: this._persisted?.mlsGroups, managerKey: (id) => `1to1-group:${id}`, label: "1:1 group" },
|
|
70560
|
+
{ family: "conversation", entries: this._persisted?.mlsConversations, managerKey: (id) => `conv:${id}`, label: "conversation" }
|
|
70561
|
+
];
|
|
70562
|
+
for (const f22 of families) {
|
|
70563
|
+
for (const [id, entry] of Object.entries(f22.entries ?? {})) {
|
|
70564
|
+
if (entry?.mlsGroupId !== mlsGroupId) continue;
|
|
70565
|
+
return {
|
|
70566
|
+
family: f22.family,
|
|
70567
|
+
id,
|
|
70568
|
+
managerKey: f22.managerKey(id),
|
|
70569
|
+
label: `${f22.label} ${id.slice(0, 8)}`
|
|
70570
|
+
};
|
|
70571
|
+
}
|
|
70572
|
+
}
|
|
70573
|
+
return null;
|
|
70574
|
+
}
|
|
70575
|
+
/** #1015: is the socket in a state that can actually take a write? */
|
|
70576
|
+
_wsIsOpen() {
|
|
70577
|
+
return this._ws?.readyState === 1;
|
|
70578
|
+
}
|
|
70579
|
+
/**
|
|
70580
|
+
* #1015: write one frame and report whether the socket took it.
|
|
70581
|
+
*
|
|
70582
|
+
* `ws.send()` on a CLOSING or CLOSED socket returns WITHOUT throwing — the
|
|
70583
|
+
* error surfaces only through the callback, and every fulfilment site passed
|
|
70584
|
+
* none. So a Welcome could be "sent" into a dead socket and the requester
|
|
70585
|
+
* still marked `fulfilled`, which it never re-requests.
|
|
70586
|
+
*/
|
|
70587
|
+
_sendFrameConfirmed(frame, label) {
|
|
70588
|
+
const ws = this._ws;
|
|
70589
|
+
if (!ws || ws.readyState !== 1) {
|
|
70590
|
+
console.warn(
|
|
70591
|
+
`[SecureChannel] ${label}: socket is not OPEN (readyState=${ws?.readyState ?? "null"}) \u2014 frame NOT written`
|
|
70592
|
+
);
|
|
70593
|
+
return Promise.resolve(false);
|
|
70594
|
+
}
|
|
70595
|
+
return new Promise((resolve32) => {
|
|
70596
|
+
try {
|
|
70597
|
+
ws.send(JSON.stringify(frame), (err) => {
|
|
70598
|
+
if (err) {
|
|
70599
|
+
console.error(`[SecureChannel] ${label}: socket refused the frame:`, err);
|
|
70600
|
+
resolve32(false);
|
|
70601
|
+
} else {
|
|
70602
|
+
resolve32(true);
|
|
70603
|
+
}
|
|
70604
|
+
});
|
|
70605
|
+
} catch (err) {
|
|
70606
|
+
console.error(`[SecureChannel] ${label}: send threw:`, err);
|
|
70607
|
+
resolve32(false);
|
|
70608
|
+
}
|
|
70609
|
+
});
|
|
70610
|
+
}
|
|
70611
|
+
/**
|
|
70612
|
+
* #1015: record a Welcome as fulfilled — and say so when it fails.
|
|
70613
|
+
*
|
|
70614
|
+
* The batched shared-1:1 site posted this with `.catch(() => {})`, so an HTTP
|
|
70615
|
+
* failure here was invisible on top of the sends being invisible.
|
|
70616
|
+
*/
|
|
70617
|
+
async _postFulfilWelcome(mlsGroupId, requestId, label) {
|
|
70618
|
+
try {
|
|
70619
|
+
const res = await fetch(
|
|
70620
|
+
`${this.config.apiUrl}/api/v1/mls/groups/${mlsGroupId}/fulfill-welcome`,
|
|
70621
|
+
{
|
|
70622
|
+
method: "POST",
|
|
70623
|
+
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70624
|
+
body: JSON.stringify({ request_id: requestId })
|
|
70625
|
+
}
|
|
70626
|
+
);
|
|
70627
|
+
if (!res.ok) {
|
|
70628
|
+
console.warn(`[SecureChannel] ${label}: fulfill-welcome returned ${res.status}`);
|
|
70629
|
+
return false;
|
|
70630
|
+
}
|
|
70631
|
+
return true;
|
|
70632
|
+
} catch (err) {
|
|
70633
|
+
console.warn(`[SecureChannel] ${label}: fulfill-welcome failed:`, err);
|
|
70634
|
+
return false;
|
|
70635
|
+
}
|
|
70636
|
+
}
|
|
70637
|
+
/**
|
|
70638
|
+
* #1013: fold an epoch seen on an inbound frame into the running hint.
|
|
70639
|
+
*
|
|
70640
|
+
* Anything unusable (missing, non-numeric, negative, fractional) is ignored
|
|
70641
|
+
* rather than defaulted. A defaulted epoch here would be a guess, and a guess
|
|
70642
|
+
* that reads LOW silently disables the check it exists to drive.
|
|
70643
|
+
*/
|
|
70644
|
+
_noteGroupEpoch(mlsGroupId, epoch) {
|
|
70645
|
+
if (typeof mlsGroupId !== "string" || !mlsGroupId) return;
|
|
70646
|
+
const current = this._observedGroupEpochs.get(mlsGroupId) ?? 0;
|
|
70647
|
+
const next = noteObservedEpoch(current, epoch);
|
|
70648
|
+
if (next > current) this._observedGroupEpochs.set(mlsGroupId, next);
|
|
70649
|
+
}
|
|
70650
|
+
/**
|
|
70651
|
+
* #1013 — the ONLY place this package encrypts an MLS application message.
|
|
70652
|
+
*
|
|
70653
|
+
* Catch up first when the group is known to have moved past us, then encrypt.
|
|
70654
|
+
* Hermes has done this since #440 and the browser since #996; the plugin —
|
|
70655
|
+
* the implementation that actually ships to customers — did not, so an agent
|
|
70656
|
+
* that missed one commit encrypted every later message at an epoch nobody
|
|
70657
|
+
* could read, while the server stored and relayed it and every signal read
|
|
70658
|
+
* 200.
|
|
70659
|
+
*
|
|
70660
|
+
* Funnelling every sender through one method is the point. A guard applied at
|
|
70661
|
+
* seven call sites decays to a guard applied at six: that is exactly how
|
|
70662
|
+
* `_handleMlsCommit` ended up handling two group families out of four (#1012).
|
|
70663
|
+
*
|
|
70664
|
+
* Best-effort by design. A catch-up that cannot replay — or that throws — must
|
|
70665
|
+
* NOT block the send; we fall through and encrypt at the local epoch, which is
|
|
70666
|
+
* precisely today's behaviour, so the worst case is no regression.
|
|
70667
|
+
*/
|
|
70668
|
+
async _encryptWithCatchUp(mlsGroup, groupKey, mlsGroupId, label, plaintextBytes) {
|
|
70669
|
+
const observed = this._observedGroupEpochs.get(mlsGroupId);
|
|
70670
|
+
if (observed !== void 0 && needsPreSendResync(Number(mlsGroup.epoch), observed)) {
|
|
70671
|
+
console.warn(
|
|
70672
|
+
`[SecureChannel] ${label}: local epoch ${Number(mlsGroup.epoch)} is behind the observed group epoch ${observed} \u2014 catching up before encrypt (#1013)`
|
|
70673
|
+
);
|
|
70674
|
+
try {
|
|
70675
|
+
if (!await this._resyncOnDivergence(groupKey, mlsGroupId)) {
|
|
70676
|
+
console.warn(
|
|
70677
|
+
`[SecureChannel] ${label}: catch-up could not replay \u2014 sending at epoch ${Number(mlsGroup.epoch)}. Peers past this epoch will not read it.`
|
|
70678
|
+
);
|
|
70679
|
+
}
|
|
70680
|
+
} catch (err) {
|
|
70681
|
+
console.warn(`[SecureChannel] ${label}: catch-up failed:`, err);
|
|
70682
|
+
}
|
|
70683
|
+
}
|
|
70684
|
+
return await mlsGroup.encrypt(plaintextBytes);
|
|
70685
|
+
}
|
|
70301
70686
|
async _resyncOnDivergence(groupKey, groupId) {
|
|
70302
70687
|
const mgr = this._mlsGroups.get(groupKey);
|
|
70303
70688
|
if (!mgr?.isInitialized) return false;
|
|
@@ -70324,11 +70709,24 @@ ${messageText}`;
|
|
|
70324
70709
|
applied++;
|
|
70325
70710
|
}
|
|
70326
70711
|
if (applied === 0) {
|
|
70327
|
-
|
|
70712
|
+
const strikes = (this._resyncNoOpStrikes.get(groupKey) ?? 0) + 1;
|
|
70713
|
+
this._resyncNoOpStrikes.set(groupKey, strikes);
|
|
70714
|
+
if (strikes < _SecureChannel.MAX_RESYNC_NOOPS) {
|
|
70715
|
+
this._mlsDecryptFailCounts.delete(groupKey);
|
|
70716
|
+
console.log(
|
|
70717
|
+
`[SecureChannel] Resync for ${groupId.slice(0, 8)}: already current at epoch=${mgr.epoch} (strike ${strikes}/${_SecureChannel.MAX_RESYNC_NOOPS}) \u2014 NOT re-keying (#1084)`
|
|
70718
|
+
);
|
|
70719
|
+
return true;
|
|
70720
|
+
}
|
|
70721
|
+
this._resyncNoOpStrikes.delete(groupKey);
|
|
70722
|
+
console.warn(
|
|
70723
|
+
`[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`
|
|
70724
|
+
);
|
|
70328
70725
|
return false;
|
|
70329
70726
|
}
|
|
70330
70727
|
await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mgr.exportState()));
|
|
70331
70728
|
this._mlsDecryptFailCounts.delete(groupKey);
|
|
70729
|
+
this._resyncNoOpStrikes.delete(groupKey);
|
|
70332
70730
|
console.log(
|
|
70333
70731
|
`[SecureChannel] Resync-on-divergence: applied ${applied} missed commit(s) for ${groupId.slice(0, 8)}, now epoch=${mgr.epoch}`
|
|
70334
70732
|
);
|
|
@@ -70715,6 +71113,7 @@ ${messageText}`;
|
|
|
70715
71113
|
const ackedIds = [];
|
|
70716
71114
|
const nackedIds = [];
|
|
70717
71115
|
for (const msg of messages) {
|
|
71116
|
+
let handled = true;
|
|
70718
71117
|
try {
|
|
70719
71118
|
if (msg.sender_device_id === this._deviceId) {
|
|
70720
71119
|
ackedIds.push(msg.queue_id);
|
|
@@ -70731,7 +71130,7 @@ ${messageText}`;
|
|
|
70731
71130
|
a2a_channel_id: msg.a2a_channel_id
|
|
70732
71131
|
});
|
|
70733
71132
|
} else if (msg.message_type === "commit") {
|
|
70734
|
-
await this._handleMlsCommit({
|
|
71133
|
+
handled = await this._handleMlsCommit({
|
|
70735
71134
|
group_id: msg.group_id,
|
|
70736
71135
|
sender_device_id: msg.sender_device_id,
|
|
70737
71136
|
epoch: msg.epoch,
|
|
@@ -70769,9 +71168,18 @@ ${messageText}`;
|
|
|
70769
71168
|
conversation_group_id: msg.conversation_group_id,
|
|
70770
71169
|
created_at: msg.created_at
|
|
70771
71170
|
});
|
|
71171
|
+
} else {
|
|
71172
|
+
handled = false;
|
|
71173
|
+
console.warn(
|
|
71174
|
+
`[SecureChannel] Delivery application row ${String(msg.message_id).slice(0, 8)} names no room, A2A channel or conversation \u2014 NOT acked`
|
|
71175
|
+
);
|
|
70772
71176
|
}
|
|
70773
71177
|
}
|
|
70774
|
-
|
|
71178
|
+
if (handled) {
|
|
71179
|
+
ackedIds.push(msg.queue_id);
|
|
71180
|
+
} else {
|
|
71181
|
+
nackedIds.push(msg.queue_id);
|
|
71182
|
+
}
|
|
70775
71183
|
} catch (err) {
|
|
70776
71184
|
console.warn(`[SecureChannel] Delivery ${msg.message_type} processing failed:`, err);
|
|
70777
71185
|
nackedIds.push(msg.queue_id);
|
|
@@ -70840,38 +71248,20 @@ ${messageText}`;
|
|
|
70840
71248
|
if (!kpHex) continue;
|
|
70841
71249
|
const kpBytes = new Uint8Array(Buffer.from(kpHex, "hex"));
|
|
70842
71250
|
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
70843
|
-
const
|
|
70844
|
-
|
|
70845
|
-
|
|
70846
|
-
|
|
70847
|
-
|
|
70848
|
-
|
|
70849
|
-
|
|
70850
|
-
|
|
70851
|
-
|
|
70852
|
-
}
|
|
70853
|
-
}
|
|
70854
|
-
if (
|
|
70855
|
-
|
|
70856
|
-
event: "mls_welcome",
|
|
70857
|
-
data: {
|
|
70858
|
-
target_device_id: req.requesting_device_id,
|
|
70859
|
-
group_id: roomState.mlsGroupId,
|
|
70860
|
-
room_id: roomId,
|
|
70861
|
-
payload: Buffer.from(welcome).toString("hex")
|
|
70862
|
-
}
|
|
70863
|
-
}));
|
|
71251
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71252
|
+
mlsGroup,
|
|
71253
|
+
mlsGroupId: roomState.mlsGroupId,
|
|
71254
|
+
label: `room ${roomId.slice(0, 8)}`,
|
|
71255
|
+
welcomeFields: { room_id: roomId },
|
|
71256
|
+
targets: [{
|
|
71257
|
+
requestId: req.id,
|
|
71258
|
+
deviceId: req.requesting_device_id,
|
|
71259
|
+
keyPackage: memberKp
|
|
71260
|
+
}]
|
|
71261
|
+
});
|
|
71262
|
+
if (done > 0) {
|
|
71263
|
+
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in room ${roomId.slice(0, 8)}`);
|
|
70864
71264
|
}
|
|
70865
|
-
await fetch(
|
|
70866
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${roomState.mlsGroupId}/fulfill-welcome`,
|
|
70867
|
-
{
|
|
70868
|
-
method: "POST",
|
|
70869
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70870
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70871
|
-
}
|
|
70872
|
-
);
|
|
70873
|
-
await saveMlsState(this.config.dataDir, roomState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70874
|
-
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in room ${roomId.slice(0, 8)}`);
|
|
70875
71265
|
} catch (fulfillErr) {
|
|
70876
71266
|
console.warn(`[SecureChannel] Welcome fulfill failed for ${req.requesting_device_id.slice(0, 8)}:`, fulfillErr);
|
|
70877
71267
|
}
|
|
@@ -70910,65 +71300,38 @@ ${messageText}`;
|
|
|
70910
71300
|
validReqs.push({ req, kp: MLSGroupManager.deserializeKeyPackage(kpBytes) });
|
|
70911
71301
|
}
|
|
70912
71302
|
if (validReqs.length === 0) continue;
|
|
71303
|
+
const sharedTargets = validReqs.map((v22) => ({
|
|
71304
|
+
requestId: v22.req.id,
|
|
71305
|
+
deviceId: v22.req.requesting_device_id,
|
|
71306
|
+
keyPackage: v22.kp
|
|
71307
|
+
}));
|
|
70913
71308
|
try {
|
|
70914
|
-
const
|
|
70915
|
-
|
|
70916
|
-
|
|
70917
|
-
|
|
70918
|
-
|
|
70919
|
-
|
|
70920
|
-
|
|
70921
|
-
|
|
70922
|
-
|
|
70923
|
-
}));
|
|
70924
|
-
}
|
|
70925
|
-
if (welcome && this._ws) {
|
|
70926
|
-
for (const { req } of validReqs) {
|
|
70927
|
-
this._ws.send(JSON.stringify({
|
|
70928
|
-
event: "mls_welcome",
|
|
70929
|
-
data: {
|
|
70930
|
-
target_device_id: req.requesting_device_id,
|
|
70931
|
-
group_id: entry.mlsGroupId,
|
|
70932
|
-
conversation_group_id: gid,
|
|
70933
|
-
payload: Buffer.from(welcome).toString("hex")
|
|
70934
|
-
}
|
|
70935
|
-
}));
|
|
70936
|
-
}
|
|
70937
|
-
}
|
|
70938
|
-
for (const { req } of validReqs) {
|
|
70939
|
-
await fetch(
|
|
70940
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${entry.mlsGroupId}/fulfill-welcome`,
|
|
70941
|
-
{
|
|
70942
|
-
method: "POST",
|
|
70943
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70944
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70945
|
-
}
|
|
70946
|
-
).catch(() => {
|
|
70947
|
-
});
|
|
71309
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71310
|
+
mlsGroup,
|
|
71311
|
+
mlsGroupId: entry.mlsGroupId,
|
|
71312
|
+
label: `shared group ${gid.slice(0, 8)}`,
|
|
71313
|
+
welcomeFields: { conversation_group_id: gid },
|
|
71314
|
+
targets: sharedTargets
|
|
71315
|
+
});
|
|
71316
|
+
if (done > 0) {
|
|
71317
|
+
console.log(`[SecureChannel] Batched Welcome for ${done}/${validReqs.length} devices in shared group ${gid.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
|
|
70948
71318
|
}
|
|
70949
|
-
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70950
|
-
console.log(`[SecureChannel] Batched Welcome for ${validReqs.length} devices in shared group ${gid.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
|
|
70951
71319
|
} catch (batchErr) {
|
|
70952
71320
|
console.warn(`[SecureChannel] Batched Welcome failed for shared group ${gid.slice(0, 8)}, falling back to individual:`, batchErr);
|
|
70953
|
-
for (const
|
|
71321
|
+
for (const target of sharedTargets) {
|
|
70954
71322
|
try {
|
|
70955
|
-
const
|
|
70956
|
-
|
|
70957
|
-
|
|
70958
|
-
|
|
70959
|
-
|
|
70960
|
-
|
|
70961
|
-
}
|
|
70962
|
-
await fetch(`${this.config.apiUrl}/api/v1/mls/groups/${entry.mlsGroupId}/fulfill-welcome`, {
|
|
70963
|
-
method: "POST",
|
|
70964
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70965
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70966
|
-
}).catch(() => {
|
|
71323
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71324
|
+
mlsGroup,
|
|
71325
|
+
mlsGroupId: entry.mlsGroupId,
|
|
71326
|
+
label: `shared group ${gid.slice(0, 8)}`,
|
|
71327
|
+
welcomeFields: { conversation_group_id: gid },
|
|
71328
|
+
targets: [target]
|
|
70967
71329
|
});
|
|
70968
|
-
|
|
70969
|
-
|
|
71330
|
+
if (done > 0) {
|
|
71331
|
+
console.log(`[SecureChannel] Individual Welcome for ${target.deviceId.slice(0, 8)} in shared group ${gid.slice(0, 8)}`);
|
|
71332
|
+
}
|
|
70970
71333
|
} catch (indivErr) {
|
|
70971
|
-
console.warn(`[SecureChannel] Individual Welcome failed for ${
|
|
71334
|
+
console.warn(`[SecureChannel] Individual Welcome failed for ${target.deviceId.slice(0, 8)}:`, indivErr);
|
|
70972
71335
|
}
|
|
70973
71336
|
}
|
|
70974
71337
|
}
|
|
@@ -71000,38 +71363,20 @@ ${messageText}`;
|
|
|
71000
71363
|
if (!kpHex) continue;
|
|
71001
71364
|
const kpBytes = new Uint8Array(Buffer.from(kpHex, "hex"));
|
|
71002
71365
|
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
71003
|
-
const
|
|
71004
|
-
|
|
71005
|
-
|
|
71006
|
-
|
|
71007
|
-
|
|
71008
|
-
|
|
71009
|
-
|
|
71010
|
-
|
|
71011
|
-
|
|
71012
|
-
}
|
|
71013
|
-
}
|
|
71014
|
-
if (
|
|
71015
|
-
|
|
71016
|
-
event: "mls_welcome",
|
|
71017
|
-
data: {
|
|
71018
|
-
target_device_id: req.requesting_device_id,
|
|
71019
|
-
group_id: convEntry.mlsGroupId,
|
|
71020
|
-
conversation_id: convId,
|
|
71021
|
-
payload: Buffer.from(welcome).toString("hex")
|
|
71022
|
-
}
|
|
71023
|
-
}));
|
|
71366
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71367
|
+
mlsGroup,
|
|
71368
|
+
mlsGroupId: convEntry.mlsGroupId,
|
|
71369
|
+
label: `conv ${convId.slice(0, 8)}`,
|
|
71370
|
+
welcomeFields: { conversation_id: convId },
|
|
71371
|
+
targets: [{
|
|
71372
|
+
requestId: req.id,
|
|
71373
|
+
deviceId: req.requesting_device_id,
|
|
71374
|
+
keyPackage: memberKp
|
|
71375
|
+
}]
|
|
71376
|
+
});
|
|
71377
|
+
if (done > 0) {
|
|
71378
|
+
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in conv ${convId.slice(0, 8)}`);
|
|
71024
71379
|
}
|
|
71025
|
-
await fetch(
|
|
71026
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${convEntry.mlsGroupId}/fulfill-welcome`,
|
|
71027
|
-
{
|
|
71028
|
-
method: "POST",
|
|
71029
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
71030
|
-
body: JSON.stringify({ request_id: req.id })
|
|
71031
|
-
}
|
|
71032
|
-
);
|
|
71033
|
-
await saveMlsState(this.config.dataDir, convEntry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
71034
|
-
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in conv ${convId.slice(0, 8)}`);
|
|
71035
71380
|
} catch (fulfillErr) {
|
|
71036
71381
|
console.warn(`[SecureChannel] Conv Welcome fulfill failed for ${req.requesting_device_id.slice(0, 8)}:`, fulfillErr);
|
|
71037
71382
|
}
|
|
@@ -71212,12 +71557,13 @@ ${messageText}`;
|
|
|
71212
71557
|
} catch (distErr) {
|
|
71213
71558
|
console.warn(`[SecureChannel] A2A distribute network error:`, distErr);
|
|
71214
71559
|
}
|
|
71215
|
-
if (!distributed && this.
|
|
71216
|
-
|
|
71560
|
+
if (!distributed && this._wsIsOpen()) {
|
|
71561
|
+
const a2aLabel = `A2A ${chId.slice(0, 8)}`;
|
|
71562
|
+
const commitSent = await this._sendFrameConfirmed({
|
|
71217
71563
|
event: "mls_commit",
|
|
71218
71564
|
data: { group_id: chState.mlsGroupId, epoch: Number(mlsGroup.epoch), payload: commitHex }
|
|
71219
|
-
})
|
|
71220
|
-
this.
|
|
71565
|
+
}, `${a2aLabel} commit`);
|
|
71566
|
+
const welcomeSent = commitSent && await this._sendFrameConfirmed({
|
|
71221
71567
|
event: "mls_welcome",
|
|
71222
71568
|
data: {
|
|
71223
71569
|
target_device_id: req.requesting_device_id,
|
|
@@ -71225,7 +71571,11 @@ ${messageText}`;
|
|
|
71225
71571
|
a2a_channel_id: chId,
|
|
71226
71572
|
payload: welcomeHex
|
|
71227
71573
|
}
|
|
71228
|
-
})
|
|
71574
|
+
}, `${a2aLabel} welcome`);
|
|
71575
|
+
if (!welcomeSent) {
|
|
71576
|
+
console.warn(`[SecureChannel] ${a2aLabel}: Welcome frames were NOT written \u2014 leaving the request pending`);
|
|
71577
|
+
continue;
|
|
71578
|
+
}
|
|
71229
71579
|
try {
|
|
71230
71580
|
await fetch(
|
|
71231
71581
|
`${this.config.apiUrl}/api/v1/mls/groups/${chState.mlsGroupId}/distribute`,
|
|
@@ -71248,16 +71598,15 @@ ${messageText}`;
|
|
|
71248
71598
|
console.warn(`[SecureChannel] Cannot deliver Welcome \u2014 HTTP distribute and WS both unavailable`);
|
|
71249
71599
|
continue;
|
|
71250
71600
|
}
|
|
71251
|
-
await
|
|
71252
|
-
|
|
71253
|
-
|
|
71254
|
-
|
|
71255
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
71256
|
-
body: JSON.stringify({ request_id: req.id })
|
|
71257
|
-
}
|
|
71601
|
+
const a2aFulfilled = await this._postFulfilWelcome(
|
|
71602
|
+
chState.mlsGroupId,
|
|
71603
|
+
req.id,
|
|
71604
|
+
`A2A ${chId.slice(0, 8)}`
|
|
71258
71605
|
);
|
|
71259
71606
|
await saveMlsState(this.config.dataDir, chState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
71260
|
-
|
|
71607
|
+
if (a2aFulfilled) {
|
|
71608
|
+
console.log(`[SecureChannel] Fulfilled A2A Welcome for ${req.requesting_device_id.slice(0, 8)} in channel ${chId.slice(0, 8)}`);
|
|
71609
|
+
}
|
|
71261
71610
|
} catch (fulfillErr) {
|
|
71262
71611
|
console.log(`[SecureChannel] A2A Welcome fulfill failed: ${fulfillErr instanceof Error ? fulfillErr.message : String(fulfillErr)}`);
|
|
71263
71612
|
}
|
|
@@ -71431,12 +71780,12 @@ ${messageText}`;
|
|
|
71431
71780
|
console.log(`[SecureChannel] MLS sync: re-join needed for group ${groupId?.slice(0, 8)}`);
|
|
71432
71781
|
if (groupId) {
|
|
71433
71782
|
await deleteMlsState(this.config.dataDir, groupId);
|
|
71434
|
-
|
|
71435
|
-
|
|
71436
|
-
|
|
71437
|
-
|
|
71438
|
-
}
|
|
71783
|
+
const target = this._resolveGroupFamily(groupId);
|
|
71784
|
+
if (target) {
|
|
71785
|
+
this._mlsGroups.delete(target.managerKey);
|
|
71786
|
+
console.log(`[SecureChannel] MLS sync rejoin: dropped local state for ${target.label}`);
|
|
71439
71787
|
}
|
|
71788
|
+
void this._requestWelcomeSelfHeal(groupId);
|
|
71440
71789
|
}
|
|
71441
71790
|
} else if (action === "replay_complete") {
|
|
71442
71791
|
console.log(`[SecureChannel] MLS sync complete for group ${groupId?.slice(0, 8)} (${data.count} messages replayed)`);
|
|
@@ -71448,6 +71797,7 @@ ${messageText}`;
|
|
|
71448
71797
|
*/
|
|
71449
71798
|
async _handleA2AMessageMLS(data) {
|
|
71450
71799
|
const groupId = data.group_id;
|
|
71800
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
71451
71801
|
const channelId = data.channel_id ?? data.a2a_channel_id;
|
|
71452
71802
|
let a2aChannelId = channelId;
|
|
71453
71803
|
if (!a2aChannelId) {
|
|
@@ -98671,7 +99021,7 @@ var init_index = __esm2({
|
|
|
98671
99021
|
init_skill_invoker();
|
|
98672
99022
|
await init_skill_telemetry();
|
|
98673
99023
|
await init_policy_enforcer();
|
|
98674
|
-
VERSION = true ? "0.23.
|
|
99024
|
+
VERSION = true ? "0.23.27" : "0.0.0-dev";
|
|
98675
99025
|
}
|
|
98676
99026
|
});
|
|
98677
99027
|
await init_index();
|
|
@@ -135052,7 +135402,7 @@ async function main() {
|
|
|
135052
135402
|
"[bridge] warning: passing the invite token on the command line is visible to other local users via 'ps'. Prefer: AV_INVITE_TOKEN=\u2026 npx @agentvault/claude-bridge"
|
|
135053
135403
|
);
|
|
135054
135404
|
}
|
|
135055
|
-
logLine(`[bridge] version: ${true ? "0.8.
|
|
135405
|
+
logLine(`[bridge] version: ${true ? "0.8.1" : "dev"}`);
|
|
135056
135406
|
logLine(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
|
|
135057
135407
|
logLine(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
|
|
135058
135408
|
if (cfg.armRoom) {
|
|
@@ -135084,7 +135434,7 @@ async function main() {
|
|
|
135084
135434
|
// its default would render "@agentvault/agentvault@0.7.x" — the wrong
|
|
135085
135435
|
// package name attached to the bridge's version number, which is worse
|
|
135086
135436
|
// than either alone.
|
|
135087
|
-
clientVersion: `@agentvault/claude-bridge@${true ? "0.8.
|
|
135437
|
+
clientVersion: `@agentvault/claude-bridge@${true ? "0.8.1" : "dev"}`
|
|
135088
135438
|
});
|
|
135089
135439
|
const agentSystemPrompt = cfg.systemPrompt ?? `You are ${cfg.agentName}, an AI agent on AgentVault. You talk with your owner in 1:1 direct messages and collaborate with other agents in shared rooms. That is your identity \u2014 introduce yourself by that name and do not claim to be any other agent. To speak, call the say tool. In a 1:1 with your owner, reply to what they say. In a room you see every message \u2014 call say only when you have something worth adding, and otherwise stay silent. Keep messages concise.`;
|
|
135090
135440
|
const deviceJwt = () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentvault/claude-bridge",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgentVault Claude Bridge — daemon for bridging a Claude agent into secure E2E-encrypted AgentVault 1:1 direct messages and rooms.",
|
|
6
6
|
"main": "dist/index.js",
|