@agentvault/secure-channel 0.1.2 → 0.2.0
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/__tests__/functional.test.d.ts +21 -0
- package/dist/__tests__/functional.test.d.ts.map +1 -0
- package/dist/__tests__/multi-session.test.d.ts +2 -0
- package/dist/__tests__/multi-session.test.d.ts.map +1 -0
- package/dist/channel.d.ts +36 -2
- package/dist/channel.d.ts.map +1 -1
- package/dist/cli.js +278 -59
- 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 +278 -59
- package/dist/index.js.map +3 -3
- package/dist/state.d.ts +6 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { SecureChannel } from "./channel.js";
|
|
2
|
-
export type { SecureChannelConfig, ChannelState, MessageMetadata, PersistedState, } from "./types.js";
|
|
2
|
+
export type { SecureChannelConfig, ChannelState, MessageMetadata, PersistedState, LegacyPersistedState, DeviceSession, } from "./types.js";
|
|
3
3
|
export declare const VERSION = "0.1.0";
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EACV,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EACV,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,cAAc,EACd,oBAAoB,EACpB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,eAAO,MAAM,OAAO,UAAU,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/channel.ts
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
3
4
|
|
|
4
5
|
// ../../node_modules/libsodium-sumo/dist/modules-sumo-esm/libsodium-sumo.mjs
|
|
5
6
|
var __filename;
|
|
@@ -45033,6 +45034,27 @@ async function activateDevice(apiUrl, deviceId) {
|
|
|
45033
45034
|
var POLL_INTERVAL_MS = 5e3;
|
|
45034
45035
|
var RECONNECT_BASE_MS = 1e3;
|
|
45035
45036
|
var RECONNECT_MAX_MS = 3e4;
|
|
45037
|
+
function migratePersistedState(raw) {
|
|
45038
|
+
if (raw.sessions && raw.primaryConversationId) {
|
|
45039
|
+
return raw;
|
|
45040
|
+
}
|
|
45041
|
+
const legacy = raw;
|
|
45042
|
+
return {
|
|
45043
|
+
deviceId: legacy.deviceId,
|
|
45044
|
+
deviceJwt: legacy.deviceJwt,
|
|
45045
|
+
primaryConversationId: legacy.conversationId,
|
|
45046
|
+
sessions: {
|
|
45047
|
+
[legacy.conversationId]: {
|
|
45048
|
+
ownerDeviceId: "",
|
|
45049
|
+
ratchetState: legacy.ratchetState
|
|
45050
|
+
}
|
|
45051
|
+
},
|
|
45052
|
+
identityKeypair: legacy.identityKeypair,
|
|
45053
|
+
ephemeralKeypair: legacy.ephemeralKeypair,
|
|
45054
|
+
fingerprint: legacy.fingerprint,
|
|
45055
|
+
lastMessageTimestamp: legacy.lastMessageTimestamp
|
|
45056
|
+
};
|
|
45057
|
+
}
|
|
45036
45058
|
var SecureChannel = class extends EventEmitter {
|
|
45037
45059
|
constructor(config) {
|
|
45038
45060
|
super();
|
|
@@ -45041,9 +45063,9 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45041
45063
|
_state = "idle";
|
|
45042
45064
|
_deviceId = null;
|
|
45043
45065
|
_fingerprint = null;
|
|
45044
|
-
|
|
45066
|
+
_primaryConversationId = "";
|
|
45045
45067
|
_deviceJwt = null;
|
|
45046
|
-
|
|
45068
|
+
_sessions = /* @__PURE__ */ new Map();
|
|
45047
45069
|
_ws = null;
|
|
45048
45070
|
_pollTimer = null;
|
|
45049
45071
|
_reconnectAttempt = 0;
|
|
@@ -45059,41 +45081,68 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45059
45081
|
get fingerprint() {
|
|
45060
45082
|
return this._fingerprint;
|
|
45061
45083
|
}
|
|
45084
|
+
/** Returns the primary conversation ID (backward-compatible). */
|
|
45062
45085
|
get conversationId() {
|
|
45063
|
-
return this.
|
|
45086
|
+
return this._primaryConversationId || null;
|
|
45087
|
+
}
|
|
45088
|
+
/** Returns all active conversation IDs. */
|
|
45089
|
+
get conversationIds() {
|
|
45090
|
+
return Array.from(this._sessions.keys());
|
|
45091
|
+
}
|
|
45092
|
+
/** Returns the number of active sessions. */
|
|
45093
|
+
get sessionCount() {
|
|
45094
|
+
return this._sessions.size;
|
|
45064
45095
|
}
|
|
45065
45096
|
async start() {
|
|
45066
45097
|
this._stopped = false;
|
|
45067
45098
|
await libsodium_wrappers_default.ready;
|
|
45068
|
-
const
|
|
45069
|
-
if (
|
|
45070
|
-
this._persisted =
|
|
45071
|
-
this._deviceId =
|
|
45072
|
-
this._deviceJwt =
|
|
45073
|
-
this.
|
|
45074
|
-
this._fingerprint =
|
|
45075
|
-
|
|
45099
|
+
const raw = await loadState(this.config.dataDir);
|
|
45100
|
+
if (raw) {
|
|
45101
|
+
this._persisted = migratePersistedState(raw);
|
|
45102
|
+
this._deviceId = this._persisted.deviceId;
|
|
45103
|
+
this._deviceJwt = this._persisted.deviceJwt;
|
|
45104
|
+
this._primaryConversationId = this._persisted.primaryConversationId;
|
|
45105
|
+
this._fingerprint = this._persisted.fingerprint;
|
|
45106
|
+
for (const [convId, sessionData] of Object.entries(
|
|
45107
|
+
this._persisted.sessions
|
|
45108
|
+
)) {
|
|
45109
|
+
if (sessionData.ratchetState) {
|
|
45110
|
+
const ratchet = DoubleRatchet.deserialize(sessionData.ratchetState);
|
|
45111
|
+
this._sessions.set(convId, {
|
|
45112
|
+
ownerDeviceId: sessionData.ownerDeviceId,
|
|
45113
|
+
ratchet
|
|
45114
|
+
});
|
|
45115
|
+
}
|
|
45116
|
+
}
|
|
45076
45117
|
this._connect();
|
|
45077
45118
|
return;
|
|
45078
45119
|
}
|
|
45079
45120
|
await this._enroll();
|
|
45080
45121
|
}
|
|
45122
|
+
/**
|
|
45123
|
+
* Encrypt and send a message to ALL owner devices (fanout).
|
|
45124
|
+
* Each session gets the same plaintext encrypted independently.
|
|
45125
|
+
*/
|
|
45081
45126
|
async send(plaintext) {
|
|
45082
|
-
if (this._state !== "ready" ||
|
|
45127
|
+
if (this._state !== "ready" || this._sessions.size === 0 || !this._ws) {
|
|
45083
45128
|
throw new Error("Channel is not ready");
|
|
45084
45129
|
}
|
|
45085
|
-
const
|
|
45086
|
-
const
|
|
45087
|
-
|
|
45088
|
-
|
|
45089
|
-
|
|
45090
|
-
|
|
45091
|
-
|
|
45092
|
-
|
|
45093
|
-
|
|
45094
|
-
|
|
45095
|
-
|
|
45096
|
-
|
|
45130
|
+
const messageGroupId = randomUUID();
|
|
45131
|
+
for (const [convId, session] of this._sessions) {
|
|
45132
|
+
const encrypted = session.ratchet.encrypt(plaintext);
|
|
45133
|
+
const transport = encryptedMessageToTransport(encrypted);
|
|
45134
|
+
this._ws.send(
|
|
45135
|
+
JSON.stringify({
|
|
45136
|
+
event: "message",
|
|
45137
|
+
data: {
|
|
45138
|
+
conversation_id: convId,
|
|
45139
|
+
header_blob: transport.header_blob,
|
|
45140
|
+
ciphertext: transport.ciphertext,
|
|
45141
|
+
message_group_id: messageGroupId
|
|
45142
|
+
}
|
|
45143
|
+
})
|
|
45144
|
+
);
|
|
45145
|
+
}
|
|
45097
45146
|
await this._persistState();
|
|
45098
45147
|
}
|
|
45099
45148
|
async stop() {
|
|
@@ -45138,8 +45187,10 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45138
45187
|
deviceId: result.device_id,
|
|
45139
45188
|
deviceJwt: "",
|
|
45140
45189
|
// set after activation
|
|
45141
|
-
|
|
45190
|
+
primaryConversationId: "",
|
|
45142
45191
|
// set after activation
|
|
45192
|
+
sessions: {},
|
|
45193
|
+
// populated after activation
|
|
45143
45194
|
identityKeypair: {
|
|
45144
45195
|
publicKey: bytesToHex(identity.publicKey),
|
|
45145
45196
|
privateKey: bytesToHex(identity.privateKey)
|
|
@@ -45148,9 +45199,7 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45148
45199
|
publicKey: bytesToHex(ephemeral.publicKey),
|
|
45149
45200
|
privateKey: bytesToHex(ephemeral.privateKey)
|
|
45150
45201
|
},
|
|
45151
|
-
fingerprint: result.fingerprint
|
|
45152
|
-
ratchetState: ""
|
|
45153
|
-
// set after activation
|
|
45202
|
+
fingerprint: result.fingerprint
|
|
45154
45203
|
};
|
|
45155
45204
|
this._poll();
|
|
45156
45205
|
} catch (err) {
|
|
@@ -45189,11 +45238,19 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45189
45238
|
this.config.apiUrl,
|
|
45190
45239
|
this._deviceId
|
|
45191
45240
|
);
|
|
45192
|
-
|
|
45241
|
+
const conversations = result.conversations || [
|
|
45242
|
+
{
|
|
45243
|
+
conversation_id: result.conversation_id,
|
|
45244
|
+
owner_device_id: "",
|
|
45245
|
+
is_primary: true
|
|
45246
|
+
}
|
|
45247
|
+
];
|
|
45248
|
+
const primary = conversations.find((c2) => c2.is_primary) || conversations[0];
|
|
45249
|
+
this._primaryConversationId = primary.conversation_id;
|
|
45193
45250
|
this._deviceJwt = result.device_jwt;
|
|
45194
45251
|
const identity = this._persisted.identityKeypair;
|
|
45195
45252
|
const ephemeral = this._persisted.ephemeralKeypair;
|
|
45196
|
-
const sharedSecret =
|
|
45253
|
+
const sharedSecret = performX3DH({
|
|
45197
45254
|
myIdentityPrivate: hexToBytes(identity.privateKey),
|
|
45198
45255
|
myEphemeralPrivate: hexToBytes(ephemeral.privateKey),
|
|
45199
45256
|
theirIdentityPublic: hexToBytes(result.owner_identity_public_key),
|
|
@@ -45202,16 +45259,25 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45202
45259
|
),
|
|
45203
45260
|
isInitiator: false
|
|
45204
45261
|
});
|
|
45205
|
-
|
|
45262
|
+
const ratchet = DoubleRatchet.initReceiver(sharedSecret, {
|
|
45206
45263
|
publicKey: hexToBytes(identity.publicKey),
|
|
45207
45264
|
privateKey: hexToBytes(identity.privateKey),
|
|
45208
45265
|
keyType: "ed25519"
|
|
45209
45266
|
});
|
|
45267
|
+
this._sessions.set(primary.conversation_id, {
|
|
45268
|
+
ownerDeviceId: primary.owner_device_id,
|
|
45269
|
+
ratchet
|
|
45270
|
+
});
|
|
45210
45271
|
this._persisted = {
|
|
45211
45272
|
...this._persisted,
|
|
45212
45273
|
deviceJwt: result.device_jwt,
|
|
45213
|
-
|
|
45214
|
-
|
|
45274
|
+
primaryConversationId: primary.conversation_id,
|
|
45275
|
+
sessions: {
|
|
45276
|
+
[primary.conversation_id]: {
|
|
45277
|
+
ownerDeviceId: primary.owner_device_id,
|
|
45278
|
+
ratchetState: ratchet.serialize()
|
|
45279
|
+
}
|
|
45280
|
+
}
|
|
45215
45281
|
};
|
|
45216
45282
|
await saveState(this.config.dataDir, this._persisted);
|
|
45217
45283
|
this._connect();
|
|
@@ -45244,23 +45310,12 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45244
45310
|
this._handleError(new Error("Device was revoked"));
|
|
45245
45311
|
return;
|
|
45246
45312
|
}
|
|
45313
|
+
if (data.event === "device_linked") {
|
|
45314
|
+
await this._handleDeviceLinked(data.data);
|
|
45315
|
+
return;
|
|
45316
|
+
}
|
|
45247
45317
|
if (data.event === "message") {
|
|
45248
|
-
|
|
45249
|
-
if (msgData.sender_device_id === this._deviceId) return;
|
|
45250
|
-
const encrypted = transportToEncryptedMessage({
|
|
45251
|
-
header_blob: msgData.header_blob,
|
|
45252
|
-
ciphertext: msgData.ciphertext
|
|
45253
|
-
});
|
|
45254
|
-
const plaintext = this._ratchet.decrypt(encrypted);
|
|
45255
|
-
await this._persistState();
|
|
45256
|
-
const metadata = {
|
|
45257
|
-
messageId: msgData.message_id,
|
|
45258
|
-
conversationId: msgData.conversation_id,
|
|
45259
|
-
timestamp: msgData.created_at
|
|
45260
|
-
};
|
|
45261
|
-
this.emit("message", plaintext, metadata);
|
|
45262
|
-
this.config.onMessage?.(plaintext, metadata);
|
|
45263
|
-
this._persisted.lastMessageTimestamp = msgData.created_at;
|
|
45318
|
+
await this._handleIncomingMessage(data.data);
|
|
45264
45319
|
}
|
|
45265
45320
|
} catch (err) {
|
|
45266
45321
|
this.emit("error", err);
|
|
@@ -45275,6 +45330,142 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45275
45330
|
this.emit("error", err);
|
|
45276
45331
|
});
|
|
45277
45332
|
}
|
|
45333
|
+
/**
|
|
45334
|
+
* Handle an incoming encrypted message from a specific conversation.
|
|
45335
|
+
* Decrypts using the appropriate session ratchet, emits to the agent,
|
|
45336
|
+
* and relays as sync messages to sibling sessions.
|
|
45337
|
+
*/
|
|
45338
|
+
async _handleIncomingMessage(msgData) {
|
|
45339
|
+
if (msgData.sender_device_id === this._deviceId) return;
|
|
45340
|
+
const convId = msgData.conversation_id;
|
|
45341
|
+
const session = this._sessions.get(convId);
|
|
45342
|
+
if (!session) {
|
|
45343
|
+
console.warn(
|
|
45344
|
+
`[SecureChannel] No session for conversation ${convId}, skipping`
|
|
45345
|
+
);
|
|
45346
|
+
return;
|
|
45347
|
+
}
|
|
45348
|
+
const encrypted = transportToEncryptedMessage({
|
|
45349
|
+
header_blob: msgData.header_blob,
|
|
45350
|
+
ciphertext: msgData.ciphertext
|
|
45351
|
+
});
|
|
45352
|
+
const plaintext = session.ratchet.decrypt(encrypted);
|
|
45353
|
+
let messageText;
|
|
45354
|
+
let messageType;
|
|
45355
|
+
try {
|
|
45356
|
+
const parsed = JSON.parse(plaintext);
|
|
45357
|
+
messageType = parsed.type || "message";
|
|
45358
|
+
messageText = parsed.text || plaintext;
|
|
45359
|
+
} catch {
|
|
45360
|
+
messageType = "message";
|
|
45361
|
+
messageText = plaintext;
|
|
45362
|
+
}
|
|
45363
|
+
if (messageType === "message") {
|
|
45364
|
+
const metadata = {
|
|
45365
|
+
messageId: msgData.message_id,
|
|
45366
|
+
conversationId: convId,
|
|
45367
|
+
timestamp: msgData.created_at
|
|
45368
|
+
};
|
|
45369
|
+
this.emit("message", messageText, metadata);
|
|
45370
|
+
this.config.onMessage?.(messageText, metadata);
|
|
45371
|
+
await this._relaySyncToSiblings(convId, session.ownerDeviceId, messageText);
|
|
45372
|
+
}
|
|
45373
|
+
if (this._persisted) {
|
|
45374
|
+
this._persisted.lastMessageTimestamp = msgData.created_at;
|
|
45375
|
+
}
|
|
45376
|
+
await this._persistState();
|
|
45377
|
+
}
|
|
45378
|
+
/**
|
|
45379
|
+
* Relay an owner's message to all sibling sessions as encrypted sync messages.
|
|
45380
|
+
* This allows all owner devices to see messages from any single device.
|
|
45381
|
+
*/
|
|
45382
|
+
async _relaySyncToSiblings(sourceConvId, senderOwnerDeviceId, messageText) {
|
|
45383
|
+
if (!this._ws || this._sessions.size <= 1) return;
|
|
45384
|
+
const syncPayload = JSON.stringify({
|
|
45385
|
+
type: "sync",
|
|
45386
|
+
sender: senderOwnerDeviceId,
|
|
45387
|
+
text: messageText,
|
|
45388
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
45389
|
+
});
|
|
45390
|
+
for (const [siblingConvId, siblingSession] of this._sessions) {
|
|
45391
|
+
if (siblingConvId === sourceConvId) continue;
|
|
45392
|
+
const syncEncrypted = siblingSession.ratchet.encrypt(syncPayload);
|
|
45393
|
+
const syncTransport = encryptedMessageToTransport(syncEncrypted);
|
|
45394
|
+
this._ws.send(
|
|
45395
|
+
JSON.stringify({
|
|
45396
|
+
event: "message",
|
|
45397
|
+
data: {
|
|
45398
|
+
conversation_id: siblingConvId,
|
|
45399
|
+
header_blob: syncTransport.header_blob,
|
|
45400
|
+
ciphertext: syncTransport.ciphertext
|
|
45401
|
+
}
|
|
45402
|
+
})
|
|
45403
|
+
);
|
|
45404
|
+
}
|
|
45405
|
+
}
|
|
45406
|
+
/**
|
|
45407
|
+
* Handle a device_linked event: a new owner device has joined.
|
|
45408
|
+
* Fetches the new device's public keys, performs X3DH, and initializes
|
|
45409
|
+
* a new ratchet session.
|
|
45410
|
+
*/
|
|
45411
|
+
async _handleDeviceLinked(event) {
|
|
45412
|
+
console.log(
|
|
45413
|
+
`[SecureChannel] New owner device linked: ${event.owner_device_id.slice(0, 8)}...`
|
|
45414
|
+
);
|
|
45415
|
+
try {
|
|
45416
|
+
const keysRes = await fetch(
|
|
45417
|
+
`${this.config.apiUrl}/api/v1/conversations/${event.conversation_id}/keys`,
|
|
45418
|
+
{
|
|
45419
|
+
headers: { Authorization: `Bearer ${this._deviceJwt}` }
|
|
45420
|
+
}
|
|
45421
|
+
);
|
|
45422
|
+
if (!keysRes.ok) {
|
|
45423
|
+
console.error(
|
|
45424
|
+
`[SecureChannel] Failed to fetch keys for linked device: ${keysRes.status}`
|
|
45425
|
+
);
|
|
45426
|
+
return;
|
|
45427
|
+
}
|
|
45428
|
+
const keys = await keysRes.json();
|
|
45429
|
+
const identity = this._persisted.identityKeypair;
|
|
45430
|
+
const ephemeral = this._persisted.ephemeralKeypair;
|
|
45431
|
+
const sharedSecret = performX3DH({
|
|
45432
|
+
myIdentityPrivate: hexToBytes(identity.privateKey),
|
|
45433
|
+
myEphemeralPrivate: hexToBytes(ephemeral.privateKey),
|
|
45434
|
+
theirIdentityPublic: hexToBytes(keys.identity_public_key),
|
|
45435
|
+
theirEphemeralPublic: hexToBytes(
|
|
45436
|
+
keys.ephemeral_public_key ?? keys.identity_public_key
|
|
45437
|
+
),
|
|
45438
|
+
isInitiator: false
|
|
45439
|
+
});
|
|
45440
|
+
const ratchet = DoubleRatchet.initReceiver(sharedSecret, {
|
|
45441
|
+
publicKey: hexToBytes(identity.publicKey),
|
|
45442
|
+
privateKey: hexToBytes(identity.privateKey),
|
|
45443
|
+
keyType: "ed25519"
|
|
45444
|
+
});
|
|
45445
|
+
this._sessions.set(event.conversation_id, {
|
|
45446
|
+
ownerDeviceId: event.owner_device_id,
|
|
45447
|
+
ratchet
|
|
45448
|
+
});
|
|
45449
|
+
this._persisted.sessions[event.conversation_id] = {
|
|
45450
|
+
ownerDeviceId: event.owner_device_id,
|
|
45451
|
+
ratchetState: ratchet.serialize()
|
|
45452
|
+
};
|
|
45453
|
+
await this._persistState();
|
|
45454
|
+
console.log(
|
|
45455
|
+
`[SecureChannel] Session initialized for device ${event.owner_device_id.slice(0, 8)}...`
|
|
45456
|
+
);
|
|
45457
|
+
} catch (err) {
|
|
45458
|
+
console.error(
|
|
45459
|
+
`[SecureChannel] Failed to handle device_linked:`,
|
|
45460
|
+
err
|
|
45461
|
+
);
|
|
45462
|
+
this.emit("error", err);
|
|
45463
|
+
}
|
|
45464
|
+
}
|
|
45465
|
+
/**
|
|
45466
|
+
* Sync missed messages across ALL sessions.
|
|
45467
|
+
* For each conversation, fetches messages since last sync and decrypts.
|
|
45468
|
+
*/
|
|
45278
45469
|
async _syncMissedMessages() {
|
|
45279
45470
|
if (!this._persisted?.lastMessageTimestamp || !this._deviceJwt) return;
|
|
45280
45471
|
try {
|
|
@@ -45287,19 +45478,38 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45287
45478
|
const messages = await res.json();
|
|
45288
45479
|
for (const msg of messages) {
|
|
45289
45480
|
if (msg.sender_device_id === this._deviceId) continue;
|
|
45481
|
+
const session = this._sessions.get(msg.conversation_id);
|
|
45482
|
+
if (!session) {
|
|
45483
|
+
console.warn(
|
|
45484
|
+
`[SecureChannel] No session for conversation ${msg.conversation_id} during sync, skipping`
|
|
45485
|
+
);
|
|
45486
|
+
continue;
|
|
45487
|
+
}
|
|
45290
45488
|
try {
|
|
45291
45489
|
const encrypted = transportToEncryptedMessage({
|
|
45292
45490
|
header_blob: msg.header_blob,
|
|
45293
45491
|
ciphertext: msg.ciphertext
|
|
45294
45492
|
});
|
|
45295
|
-
const plaintext =
|
|
45296
|
-
|
|
45297
|
-
|
|
45298
|
-
|
|
45299
|
-
|
|
45300
|
-
|
|
45301
|
-
|
|
45302
|
-
|
|
45493
|
+
const plaintext = session.ratchet.decrypt(encrypted);
|
|
45494
|
+
let messageText;
|
|
45495
|
+
let messageType;
|
|
45496
|
+
try {
|
|
45497
|
+
const parsed = JSON.parse(plaintext);
|
|
45498
|
+
messageType = parsed.type || "message";
|
|
45499
|
+
messageText = parsed.text || plaintext;
|
|
45500
|
+
} catch {
|
|
45501
|
+
messageType = "message";
|
|
45502
|
+
messageText = plaintext;
|
|
45503
|
+
}
|
|
45504
|
+
if (messageType === "message") {
|
|
45505
|
+
const metadata = {
|
|
45506
|
+
messageId: msg.id,
|
|
45507
|
+
conversationId: msg.conversation_id,
|
|
45508
|
+
timestamp: msg.created_at
|
|
45509
|
+
};
|
|
45510
|
+
this.emit("message", messageText, metadata);
|
|
45511
|
+
this.config.onMessage?.(messageText, metadata);
|
|
45512
|
+
}
|
|
45303
45513
|
this._persisted.lastMessageTimestamp = msg.created_at;
|
|
45304
45514
|
} catch (err) {
|
|
45305
45515
|
this.emit("error", err);
|
|
@@ -45333,9 +45543,18 @@ var SecureChannel = class extends EventEmitter {
|
|
|
45333
45543
|
this._setState("error");
|
|
45334
45544
|
this.emit("error", err);
|
|
45335
45545
|
}
|
|
45546
|
+
/**
|
|
45547
|
+
* Persist all ratchet session states to disk.
|
|
45548
|
+
* Syncs live ratchet states back into the persisted sessions map.
|
|
45549
|
+
*/
|
|
45336
45550
|
async _persistState() {
|
|
45337
|
-
if (!this._persisted ||
|
|
45338
|
-
|
|
45551
|
+
if (!this._persisted || this._sessions.size === 0) return;
|
|
45552
|
+
for (const [convId, session] of this._sessions) {
|
|
45553
|
+
this._persisted.sessions[convId] = {
|
|
45554
|
+
ownerDeviceId: session.ownerDeviceId,
|
|
45555
|
+
ratchetState: session.ratchet.serialize()
|
|
45556
|
+
};
|
|
45557
|
+
}
|
|
45339
45558
|
await saveState(this.config.dataDir, this._persisted);
|
|
45340
45559
|
}
|
|
45341
45560
|
};
|