@agentvault/secure-channel 0.6.20 → 0.6.22
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/channel.d.ts +42 -1
- package/dist/channel.d.ts.map +1 -1
- package/dist/cli.js +317 -0
- package/dist/cli.js.map +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +317 -0
- package/dist/index.js.map +3 -3
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/channel.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import type { SecureChannelConfig, ChannelState, SendOptions, DecisionRequest, DecisionResponse } from "./types.js";
|
|
2
|
+
import type { SecureChannelConfig, ChannelState, SendOptions, DecisionRequest, DecisionResponse, HeartbeatStatus, StatusAlert, RoomMemberInfo, RoomConversationInfo, RoomInfo } from "./types.js";
|
|
3
3
|
export declare class SecureChannel extends EventEmitter {
|
|
4
4
|
private config;
|
|
5
5
|
private _state;
|
|
@@ -20,6 +20,9 @@ export declare class SecureChannel extends EventEmitter {
|
|
|
20
20
|
private _persisted;
|
|
21
21
|
private _httpServer;
|
|
22
22
|
private _pollFallbackTimer;
|
|
23
|
+
private _heartbeatTimer;
|
|
24
|
+
private _heartbeatCallback;
|
|
25
|
+
private _heartbeatIntervalSeconds;
|
|
23
26
|
private _syncMessageIds;
|
|
24
27
|
private static readonly PING_INTERVAL_MS;
|
|
25
28
|
private static readonly SILENCE_TIMEOUT_MS;
|
|
@@ -64,6 +67,35 @@ export declare class SecureChannel extends EventEmitter {
|
|
|
64
67
|
* Optional timeout rejects with an Error.
|
|
65
68
|
*/
|
|
66
69
|
waitForDecision(decisionId: string, timeoutMs?: number): Promise<DecisionResponse>;
|
|
70
|
+
/**
|
|
71
|
+
* Join a room by performing X3DH key exchange with each member
|
|
72
|
+
* for the pairwise conversations involving this device.
|
|
73
|
+
*/
|
|
74
|
+
joinRoom(roomData: {
|
|
75
|
+
roomId: string;
|
|
76
|
+
name: string;
|
|
77
|
+
members: RoomMemberInfo[];
|
|
78
|
+
conversations: RoomConversationInfo[];
|
|
79
|
+
}): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Send an encrypted message to all members of a room.
|
|
82
|
+
* Each pairwise conversation gets the plaintext encrypted independently.
|
|
83
|
+
*/
|
|
84
|
+
sendToRoom(roomId: string, plaintext: string, opts?: {
|
|
85
|
+
messageType?: string;
|
|
86
|
+
}): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Leave a room: remove sessions and persisted room state.
|
|
89
|
+
*/
|
|
90
|
+
leaveRoom(roomId: string): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Return info for all joined rooms.
|
|
93
|
+
*/
|
|
94
|
+
getRooms(): RoomInfo[];
|
|
95
|
+
startHeartbeat(intervalSeconds: number, statusCallback: () => HeartbeatStatus): void;
|
|
96
|
+
stopHeartbeat(): Promise<void>;
|
|
97
|
+
sendStatusAlert(alert: StatusAlert): Promise<void>;
|
|
98
|
+
private _sendHeartbeat;
|
|
67
99
|
stop(): Promise<void>;
|
|
68
100
|
startHttpServer(port: number): void;
|
|
69
101
|
private _stopHttpServer;
|
|
@@ -128,6 +160,15 @@ export declare class SecureChannel extends EventEmitter {
|
|
|
128
160
|
* a new ratchet session.
|
|
129
161
|
*/
|
|
130
162
|
private _handleDeviceLinked;
|
|
163
|
+
/**
|
|
164
|
+
* Handle an incoming room message. Finds the pairwise conversation
|
|
165
|
+
* for the sender, decrypts, and emits a room_message event.
|
|
166
|
+
*/
|
|
167
|
+
private _handleRoomMessage;
|
|
168
|
+
/**
|
|
169
|
+
* Find the pairwise conversation ID for a given sender in a room.
|
|
170
|
+
*/
|
|
171
|
+
private _findConversationForSender;
|
|
131
172
|
/**
|
|
132
173
|
* Sync missed messages across ALL sessions.
|
|
133
174
|
* For each conversation, fetches messages since last sync and decrypts.
|
package/dist/channel.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAoB3C,OAAO,KAAK,EACV,mBAAmB,EACnB,YAAY,EAMZ,WAAW,EACX,eAAe,EACf,gBAAgB,
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -45189,6 +45189,9 @@ var init_channel = __esm({
|
|
|
45189
45189
|
_persisted = null;
|
|
45190
45190
|
_httpServer = null;
|
|
45191
45191
|
_pollFallbackTimer = null;
|
|
45192
|
+
_heartbeatTimer = null;
|
|
45193
|
+
_heartbeatCallback = null;
|
|
45194
|
+
_heartbeatIntervalSeconds = 0;
|
|
45192
45195
|
_syncMessageIds = null;
|
|
45193
45196
|
// Liveness detection: server sends app-level {"event":"ping"} every 30s.
|
|
45194
45197
|
// We check every 30s; if no data received in 90s (3 missed pings), connection is dead.
|
|
@@ -45410,8 +45413,236 @@ var init_channel = __esm({
|
|
|
45410
45413
|
}
|
|
45411
45414
|
});
|
|
45412
45415
|
}
|
|
45416
|
+
// --- Multi-agent room methods ---
|
|
45417
|
+
/**
|
|
45418
|
+
* Join a room by performing X3DH key exchange with each member
|
|
45419
|
+
* for the pairwise conversations involving this device.
|
|
45420
|
+
*/
|
|
45421
|
+
async joinRoom(roomData) {
|
|
45422
|
+
if (!this._persisted) {
|
|
45423
|
+
throw new Error("Channel not initialized");
|
|
45424
|
+
}
|
|
45425
|
+
await libsodium_wrappers_default.ready;
|
|
45426
|
+
const identity = this._persisted.identityKeypair;
|
|
45427
|
+
const ephemeral = this._persisted.ephemeralKeypair;
|
|
45428
|
+
const myDeviceId = this._deviceId;
|
|
45429
|
+
const conversationIds = [];
|
|
45430
|
+
for (const conv of roomData.conversations) {
|
|
45431
|
+
if (conv.participantA !== myDeviceId && conv.participantB !== myDeviceId) {
|
|
45432
|
+
continue;
|
|
45433
|
+
}
|
|
45434
|
+
const otherDeviceId = conv.participantA === myDeviceId ? conv.participantB : conv.participantA;
|
|
45435
|
+
const otherMember = roomData.members.find((m2) => m2.deviceId === otherDeviceId);
|
|
45436
|
+
if (!otherMember?.identityPublicKey) {
|
|
45437
|
+
console.warn(
|
|
45438
|
+
`[SecureChannel] No public key for member ${otherDeviceId.slice(0, 8)}..., skipping`
|
|
45439
|
+
);
|
|
45440
|
+
continue;
|
|
45441
|
+
}
|
|
45442
|
+
const isInitiator = myDeviceId < otherDeviceId;
|
|
45443
|
+
const sharedSecret = performX3DH({
|
|
45444
|
+
myIdentityPrivate: hexToBytes(identity.privateKey),
|
|
45445
|
+
myEphemeralPrivate: hexToBytes(ephemeral.privateKey),
|
|
45446
|
+
theirIdentityPublic: hexToBytes(otherMember.identityPublicKey),
|
|
45447
|
+
theirEphemeralPublic: hexToBytes(
|
|
45448
|
+
otherMember.ephemeralPublicKey ?? otherMember.identityPublicKey
|
|
45449
|
+
),
|
|
45450
|
+
isInitiator
|
|
45451
|
+
});
|
|
45452
|
+
const ratchet = isInitiator ? DoubleRatchet.initSender(sharedSecret, {
|
|
45453
|
+
publicKey: hexToBytes(identity.publicKey),
|
|
45454
|
+
privateKey: hexToBytes(identity.privateKey),
|
|
45455
|
+
keyType: "ed25519"
|
|
45456
|
+
}) : DoubleRatchet.initReceiver(sharedSecret, {
|
|
45457
|
+
publicKey: hexToBytes(identity.publicKey),
|
|
45458
|
+
privateKey: hexToBytes(identity.privateKey),
|
|
45459
|
+
keyType: "ed25519"
|
|
45460
|
+
});
|
|
45461
|
+
this._sessions.set(conv.id, {
|
|
45462
|
+
ownerDeviceId: otherDeviceId,
|
|
45463
|
+
ratchet,
|
|
45464
|
+
activated: isInitiator
|
|
45465
|
+
// initiator can send immediately
|
|
45466
|
+
});
|
|
45467
|
+
this._persisted.sessions[conv.id] = {
|
|
45468
|
+
ownerDeviceId: otherDeviceId,
|
|
45469
|
+
ratchetState: ratchet.serialize(),
|
|
45470
|
+
activated: isInitiator
|
|
45471
|
+
};
|
|
45472
|
+
conversationIds.push(conv.id);
|
|
45473
|
+
console.log(
|
|
45474
|
+
`[SecureChannel] Room session initialized: conv ${conv.id.slice(0, 8)}... with ${otherDeviceId.slice(0, 8)}... (initiator=${isInitiator})`
|
|
45475
|
+
);
|
|
45476
|
+
}
|
|
45477
|
+
if (!this._persisted.rooms) {
|
|
45478
|
+
this._persisted.rooms = {};
|
|
45479
|
+
}
|
|
45480
|
+
this._persisted.rooms[roomData.roomId] = {
|
|
45481
|
+
roomId: roomData.roomId,
|
|
45482
|
+
name: roomData.name,
|
|
45483
|
+
conversationIds,
|
|
45484
|
+
members: roomData.members
|
|
45485
|
+
};
|
|
45486
|
+
await this._persistState();
|
|
45487
|
+
this.emit("room_joined", { roomId: roomData.roomId, name: roomData.name });
|
|
45488
|
+
}
|
|
45489
|
+
/**
|
|
45490
|
+
* Send an encrypted message to all members of a room.
|
|
45491
|
+
* Each pairwise conversation gets the plaintext encrypted independently.
|
|
45492
|
+
*/
|
|
45493
|
+
async sendToRoom(roomId, plaintext, opts) {
|
|
45494
|
+
if (!this._persisted?.rooms?.[roomId]) {
|
|
45495
|
+
throw new Error(`Room ${roomId} not found`);
|
|
45496
|
+
}
|
|
45497
|
+
const room = this._persisted.rooms[roomId];
|
|
45498
|
+
const messageType = opts?.messageType ?? "text";
|
|
45499
|
+
const recipients = [];
|
|
45500
|
+
for (const convId of room.conversationIds) {
|
|
45501
|
+
const session = this._sessions.get(convId);
|
|
45502
|
+
if (!session) {
|
|
45503
|
+
console.warn(`[SecureChannel] No session for room conv ${convId.slice(0, 8)}..., skipping`);
|
|
45504
|
+
continue;
|
|
45505
|
+
}
|
|
45506
|
+
const encrypted = session.ratchet.encrypt(plaintext);
|
|
45507
|
+
const transport = encryptedMessageToTransport(encrypted);
|
|
45508
|
+
recipients.push({
|
|
45509
|
+
device_id: session.ownerDeviceId,
|
|
45510
|
+
header_blob: transport.header_blob,
|
|
45511
|
+
ciphertext: transport.ciphertext
|
|
45512
|
+
});
|
|
45513
|
+
}
|
|
45514
|
+
if (recipients.length === 0) {
|
|
45515
|
+
throw new Error("No active sessions in room");
|
|
45516
|
+
}
|
|
45517
|
+
if (this._state === "ready" && this._ws) {
|
|
45518
|
+
this._ws.send(
|
|
45519
|
+
JSON.stringify({
|
|
45520
|
+
event: "room_message",
|
|
45521
|
+
room_id: roomId,
|
|
45522
|
+
recipients,
|
|
45523
|
+
message_type: messageType
|
|
45524
|
+
})
|
|
45525
|
+
);
|
|
45526
|
+
} else {
|
|
45527
|
+
try {
|
|
45528
|
+
const res = await fetch(
|
|
45529
|
+
`${this.config.apiUrl}/api/v1/rooms/${roomId}/messages`,
|
|
45530
|
+
{
|
|
45531
|
+
method: "POST",
|
|
45532
|
+
headers: {
|
|
45533
|
+
"Content-Type": "application/json",
|
|
45534
|
+
Authorization: `Bearer ${this._deviceJwt}`
|
|
45535
|
+
},
|
|
45536
|
+
body: JSON.stringify({ recipients, message_type: messageType })
|
|
45537
|
+
}
|
|
45538
|
+
);
|
|
45539
|
+
if (!res.ok) {
|
|
45540
|
+
const detail = await res.text();
|
|
45541
|
+
throw new Error(`Room message failed (${res.status}): ${detail}`);
|
|
45542
|
+
}
|
|
45543
|
+
} catch (err) {
|
|
45544
|
+
throw new Error(`Failed to send room message: ${err}`);
|
|
45545
|
+
}
|
|
45546
|
+
}
|
|
45547
|
+
await this._persistState();
|
|
45548
|
+
}
|
|
45549
|
+
/**
|
|
45550
|
+
* Leave a room: remove sessions and persisted room state.
|
|
45551
|
+
*/
|
|
45552
|
+
async leaveRoom(roomId) {
|
|
45553
|
+
if (!this._persisted?.rooms?.[roomId]) {
|
|
45554
|
+
return;
|
|
45555
|
+
}
|
|
45556
|
+
const room = this._persisted.rooms[roomId];
|
|
45557
|
+
for (const convId of room.conversationIds) {
|
|
45558
|
+
this._sessions.delete(convId);
|
|
45559
|
+
delete this._persisted.sessions[convId];
|
|
45560
|
+
}
|
|
45561
|
+
delete this._persisted.rooms[roomId];
|
|
45562
|
+
await this._persistState();
|
|
45563
|
+
this.emit("room_left", { roomId });
|
|
45564
|
+
}
|
|
45565
|
+
/**
|
|
45566
|
+
* Return info for all joined rooms.
|
|
45567
|
+
*/
|
|
45568
|
+
getRooms() {
|
|
45569
|
+
if (!this._persisted?.rooms) return [];
|
|
45570
|
+
return Object.values(this._persisted.rooms).map((rs) => ({
|
|
45571
|
+
roomId: rs.roomId,
|
|
45572
|
+
name: rs.name,
|
|
45573
|
+
members: rs.members,
|
|
45574
|
+
conversationIds: rs.conversationIds
|
|
45575
|
+
}));
|
|
45576
|
+
}
|
|
45577
|
+
// --- Heartbeat and status methods ---
|
|
45578
|
+
startHeartbeat(intervalSeconds, statusCallback) {
|
|
45579
|
+
this.stopHeartbeat();
|
|
45580
|
+
this._heartbeatCallback = statusCallback;
|
|
45581
|
+
this._heartbeatIntervalSeconds = intervalSeconds;
|
|
45582
|
+
this._sendHeartbeat();
|
|
45583
|
+
this._heartbeatTimer = setInterval(() => {
|
|
45584
|
+
this._sendHeartbeat();
|
|
45585
|
+
}, intervalSeconds * 1e3);
|
|
45586
|
+
}
|
|
45587
|
+
async stopHeartbeat() {
|
|
45588
|
+
if (this._heartbeatTimer) {
|
|
45589
|
+
clearInterval(this._heartbeatTimer);
|
|
45590
|
+
this._heartbeatTimer = null;
|
|
45591
|
+
}
|
|
45592
|
+
if (this._heartbeatCallback && this._state === "ready") {
|
|
45593
|
+
try {
|
|
45594
|
+
await this.send(
|
|
45595
|
+
JSON.stringify({
|
|
45596
|
+
agent_status: "shutting_down",
|
|
45597
|
+
current_task: "",
|
|
45598
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
45599
|
+
}),
|
|
45600
|
+
{
|
|
45601
|
+
messageType: "heartbeat",
|
|
45602
|
+
metadata: { next_heartbeat_seconds: this._heartbeatIntervalSeconds }
|
|
45603
|
+
}
|
|
45604
|
+
);
|
|
45605
|
+
} catch {
|
|
45606
|
+
}
|
|
45607
|
+
}
|
|
45608
|
+
this._heartbeatCallback = null;
|
|
45609
|
+
}
|
|
45610
|
+
async sendStatusAlert(alert) {
|
|
45611
|
+
const priority = alert.severity === "error" || alert.severity === "critical" ? "high" : "normal";
|
|
45612
|
+
await this.send(
|
|
45613
|
+
JSON.stringify({
|
|
45614
|
+
title: alert.title,
|
|
45615
|
+
message: alert.message,
|
|
45616
|
+
severity: alert.severity,
|
|
45617
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
45618
|
+
}),
|
|
45619
|
+
{
|
|
45620
|
+
messageType: "status_alert",
|
|
45621
|
+
priority,
|
|
45622
|
+
metadata: { severity: alert.severity }
|
|
45623
|
+
}
|
|
45624
|
+
);
|
|
45625
|
+
}
|
|
45626
|
+
_sendHeartbeat() {
|
|
45627
|
+
if (this._state !== "ready" || !this._heartbeatCallback) return;
|
|
45628
|
+
const status = this._heartbeatCallback();
|
|
45629
|
+
this.send(
|
|
45630
|
+
JSON.stringify({
|
|
45631
|
+
agent_status: status.agent_status,
|
|
45632
|
+
current_task: status.current_task,
|
|
45633
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
45634
|
+
}),
|
|
45635
|
+
{
|
|
45636
|
+
messageType: "heartbeat",
|
|
45637
|
+
metadata: { next_heartbeat_seconds: this._heartbeatIntervalSeconds }
|
|
45638
|
+
}
|
|
45639
|
+
).catch((err) => {
|
|
45640
|
+
this.emit("error", new Error(`Heartbeat send failed: ${err}`));
|
|
45641
|
+
});
|
|
45642
|
+
}
|
|
45413
45643
|
async stop() {
|
|
45414
45644
|
this._stopped = true;
|
|
45645
|
+
await this.stopHeartbeat();
|
|
45415
45646
|
this._flushAcks();
|
|
45416
45647
|
this._stopPing();
|
|
45417
45648
|
this._stopPollFallback();
|
|
@@ -45781,6 +46012,28 @@ var init_channel = __esm({
|
|
|
45781
46012
|
if (data.event === "message") {
|
|
45782
46013
|
await this._handleIncomingMessage(data.data);
|
|
45783
46014
|
}
|
|
46015
|
+
if (data.event === "room_joined") {
|
|
46016
|
+
const d2 = data.data;
|
|
46017
|
+
this.joinRoom({
|
|
46018
|
+
roomId: d2.room_id,
|
|
46019
|
+
name: d2.name,
|
|
46020
|
+
members: (d2.members || []).map((m2) => ({
|
|
46021
|
+
deviceId: m2.device_id,
|
|
46022
|
+
entityType: m2.entity_type,
|
|
46023
|
+
displayName: m2.display_name,
|
|
46024
|
+
identityPublicKey: m2.identity_public_key,
|
|
46025
|
+
ephemeralPublicKey: m2.ephemeral_public_key
|
|
46026
|
+
})),
|
|
46027
|
+
conversations: (d2.conversations || []).map((c2) => ({
|
|
46028
|
+
id: c2.id,
|
|
46029
|
+
participantA: c2.participant_a,
|
|
46030
|
+
participantB: c2.participant_b
|
|
46031
|
+
}))
|
|
46032
|
+
}).catch((err) => this.emit("error", err));
|
|
46033
|
+
}
|
|
46034
|
+
if (data.event === "room_message") {
|
|
46035
|
+
await this._handleRoomMessage(data.data);
|
|
46036
|
+
}
|
|
45784
46037
|
} catch (err) {
|
|
45785
46038
|
this.emit("error", err);
|
|
45786
46039
|
}
|
|
@@ -46130,6 +46383,70 @@ ${messageText}`;
|
|
|
46130
46383
|
this.emit("error", err);
|
|
46131
46384
|
}
|
|
46132
46385
|
}
|
|
46386
|
+
/**
|
|
46387
|
+
* Handle an incoming room message. Finds the pairwise conversation
|
|
46388
|
+
* for the sender, decrypts, and emits a room_message event.
|
|
46389
|
+
*/
|
|
46390
|
+
async _handleRoomMessage(msgData) {
|
|
46391
|
+
if (msgData.sender_device_id === this._deviceId) return;
|
|
46392
|
+
const convId = msgData.conversation_id ?? this._findConversationForSender(msgData.sender_device_id, msgData.room_id);
|
|
46393
|
+
if (!convId) {
|
|
46394
|
+
console.warn(
|
|
46395
|
+
`[SecureChannel] No conversation found for sender ${msgData.sender_device_id.slice(0, 8)}... in room ${msgData.room_id}`
|
|
46396
|
+
);
|
|
46397
|
+
return;
|
|
46398
|
+
}
|
|
46399
|
+
const session = this._sessions.get(convId);
|
|
46400
|
+
if (!session) {
|
|
46401
|
+
console.warn(
|
|
46402
|
+
`[SecureChannel] No session for room conv ${convId.slice(0, 8)}..., skipping`
|
|
46403
|
+
);
|
|
46404
|
+
return;
|
|
46405
|
+
}
|
|
46406
|
+
const encrypted = transportToEncryptedMessage({
|
|
46407
|
+
header_blob: msgData.header_blob,
|
|
46408
|
+
ciphertext: msgData.ciphertext
|
|
46409
|
+
});
|
|
46410
|
+
const plaintext = session.ratchet.decrypt(encrypted);
|
|
46411
|
+
if (!session.activated) {
|
|
46412
|
+
session.activated = true;
|
|
46413
|
+
console.log(
|
|
46414
|
+
`[SecureChannel] Room session ${convId.slice(0, 8)}... activated by first message`
|
|
46415
|
+
);
|
|
46416
|
+
}
|
|
46417
|
+
if (msgData.message_id) {
|
|
46418
|
+
this._sendAck(msgData.message_id);
|
|
46419
|
+
}
|
|
46420
|
+
await this._persistState();
|
|
46421
|
+
const metadata = {
|
|
46422
|
+
messageId: msgData.message_id ?? "",
|
|
46423
|
+
conversationId: convId,
|
|
46424
|
+
timestamp: msgData.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
46425
|
+
messageType: msgData.message_type ?? "text"
|
|
46426
|
+
};
|
|
46427
|
+
this.emit("room_message", {
|
|
46428
|
+
roomId: msgData.room_id,
|
|
46429
|
+
senderDeviceId: msgData.sender_device_id,
|
|
46430
|
+
plaintext,
|
|
46431
|
+
messageType: msgData.message_type ?? "text",
|
|
46432
|
+
timestamp: msgData.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
46433
|
+
});
|
|
46434
|
+
this.config.onMessage?.(plaintext, metadata);
|
|
46435
|
+
}
|
|
46436
|
+
/**
|
|
46437
|
+
* Find the pairwise conversation ID for a given sender in a room.
|
|
46438
|
+
*/
|
|
46439
|
+
_findConversationForSender(senderDeviceId, roomId) {
|
|
46440
|
+
const room = this._persisted?.rooms?.[roomId];
|
|
46441
|
+
if (!room) return null;
|
|
46442
|
+
for (const convId of room.conversationIds) {
|
|
46443
|
+
const session = this._sessions.get(convId);
|
|
46444
|
+
if (session && session.ownerDeviceId === senderDeviceId) {
|
|
46445
|
+
return convId;
|
|
46446
|
+
}
|
|
46447
|
+
}
|
|
46448
|
+
return null;
|
|
46449
|
+
}
|
|
46133
46450
|
/**
|
|
46134
46451
|
* Sync missed messages across ALL sessions.
|
|
46135
46452
|
* For each conversation, fetches messages since last sync and decrypts.
|