@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/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { SecureChannel } from "./channel.js";
|
|
2
|
-
export type { SecureChannelConfig, ChannelState, MessageMetadata, AttachmentData, PersistedState, LegacyPersistedState, DeviceSession, HistoryEntry, SendOptions, DecisionOption, DecisionRequest, DecisionResponse, ContextRef, } from "./types.js";
|
|
2
|
+
export type { SecureChannelConfig, ChannelState, MessageMetadata, AttachmentData, PersistedState, LegacyPersistedState, DeviceSession, HistoryEntry, SendOptions, DecisionOption, DecisionRequest, DecisionResponse, ContextRef, HeartbeatStatus, StatusAlert, RoomInfo, RoomMemberInfo, RoomConversationInfo, RoomState, } from "./types.js";
|
|
3
3
|
export { agentVaultPlugin, setOcRuntime, getActiveChannel } from "./openclaw-plugin.js";
|
|
4
4
|
export declare const VERSION = "0.6.13";
|
|
5
5
|
//# 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,EACd,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,YAAY,EACZ,WAAW,EACX,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,UAAU,
|
|
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,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,YAAY,EACZ,WAAW,EACX,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,UAAU,EACV,eAAe,EACf,WAAW,EACX,QAAQ,EACR,cAAc,EACd,oBAAoB,EACpB,SAAS,GACV,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExF,eAAO,MAAM,OAAO,WAAW,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -45120,6 +45120,9 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
|
|
|
45120
45120
|
_persisted = null;
|
|
45121
45121
|
_httpServer = null;
|
|
45122
45122
|
_pollFallbackTimer = null;
|
|
45123
|
+
_heartbeatTimer = null;
|
|
45124
|
+
_heartbeatCallback = null;
|
|
45125
|
+
_heartbeatIntervalSeconds = 0;
|
|
45123
45126
|
_syncMessageIds = null;
|
|
45124
45127
|
// Liveness detection: server sends app-level {"event":"ping"} every 30s.
|
|
45125
45128
|
// We check every 30s; if no data received in 90s (3 missed pings), connection is dead.
|
|
@@ -45341,8 +45344,236 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
|
|
|
45341
45344
|
}
|
|
45342
45345
|
});
|
|
45343
45346
|
}
|
|
45347
|
+
// --- Multi-agent room methods ---
|
|
45348
|
+
/**
|
|
45349
|
+
* Join a room by performing X3DH key exchange with each member
|
|
45350
|
+
* for the pairwise conversations involving this device.
|
|
45351
|
+
*/
|
|
45352
|
+
async joinRoom(roomData) {
|
|
45353
|
+
if (!this._persisted) {
|
|
45354
|
+
throw new Error("Channel not initialized");
|
|
45355
|
+
}
|
|
45356
|
+
await libsodium_wrappers_default.ready;
|
|
45357
|
+
const identity = this._persisted.identityKeypair;
|
|
45358
|
+
const ephemeral = this._persisted.ephemeralKeypair;
|
|
45359
|
+
const myDeviceId = this._deviceId;
|
|
45360
|
+
const conversationIds = [];
|
|
45361
|
+
for (const conv of roomData.conversations) {
|
|
45362
|
+
if (conv.participantA !== myDeviceId && conv.participantB !== myDeviceId) {
|
|
45363
|
+
continue;
|
|
45364
|
+
}
|
|
45365
|
+
const otherDeviceId = conv.participantA === myDeviceId ? conv.participantB : conv.participantA;
|
|
45366
|
+
const otherMember = roomData.members.find((m2) => m2.deviceId === otherDeviceId);
|
|
45367
|
+
if (!otherMember?.identityPublicKey) {
|
|
45368
|
+
console.warn(
|
|
45369
|
+
`[SecureChannel] No public key for member ${otherDeviceId.slice(0, 8)}..., skipping`
|
|
45370
|
+
);
|
|
45371
|
+
continue;
|
|
45372
|
+
}
|
|
45373
|
+
const isInitiator = myDeviceId < otherDeviceId;
|
|
45374
|
+
const sharedSecret = performX3DH({
|
|
45375
|
+
myIdentityPrivate: hexToBytes(identity.privateKey),
|
|
45376
|
+
myEphemeralPrivate: hexToBytes(ephemeral.privateKey),
|
|
45377
|
+
theirIdentityPublic: hexToBytes(otherMember.identityPublicKey),
|
|
45378
|
+
theirEphemeralPublic: hexToBytes(
|
|
45379
|
+
otherMember.ephemeralPublicKey ?? otherMember.identityPublicKey
|
|
45380
|
+
),
|
|
45381
|
+
isInitiator
|
|
45382
|
+
});
|
|
45383
|
+
const ratchet = isInitiator ? DoubleRatchet.initSender(sharedSecret, {
|
|
45384
|
+
publicKey: hexToBytes(identity.publicKey),
|
|
45385
|
+
privateKey: hexToBytes(identity.privateKey),
|
|
45386
|
+
keyType: "ed25519"
|
|
45387
|
+
}) : DoubleRatchet.initReceiver(sharedSecret, {
|
|
45388
|
+
publicKey: hexToBytes(identity.publicKey),
|
|
45389
|
+
privateKey: hexToBytes(identity.privateKey),
|
|
45390
|
+
keyType: "ed25519"
|
|
45391
|
+
});
|
|
45392
|
+
this._sessions.set(conv.id, {
|
|
45393
|
+
ownerDeviceId: otherDeviceId,
|
|
45394
|
+
ratchet,
|
|
45395
|
+
activated: isInitiator
|
|
45396
|
+
// initiator can send immediately
|
|
45397
|
+
});
|
|
45398
|
+
this._persisted.sessions[conv.id] = {
|
|
45399
|
+
ownerDeviceId: otherDeviceId,
|
|
45400
|
+
ratchetState: ratchet.serialize(),
|
|
45401
|
+
activated: isInitiator
|
|
45402
|
+
};
|
|
45403
|
+
conversationIds.push(conv.id);
|
|
45404
|
+
console.log(
|
|
45405
|
+
`[SecureChannel] Room session initialized: conv ${conv.id.slice(0, 8)}... with ${otherDeviceId.slice(0, 8)}... (initiator=${isInitiator})`
|
|
45406
|
+
);
|
|
45407
|
+
}
|
|
45408
|
+
if (!this._persisted.rooms) {
|
|
45409
|
+
this._persisted.rooms = {};
|
|
45410
|
+
}
|
|
45411
|
+
this._persisted.rooms[roomData.roomId] = {
|
|
45412
|
+
roomId: roomData.roomId,
|
|
45413
|
+
name: roomData.name,
|
|
45414
|
+
conversationIds,
|
|
45415
|
+
members: roomData.members
|
|
45416
|
+
};
|
|
45417
|
+
await this._persistState();
|
|
45418
|
+
this.emit("room_joined", { roomId: roomData.roomId, name: roomData.name });
|
|
45419
|
+
}
|
|
45420
|
+
/**
|
|
45421
|
+
* Send an encrypted message to all members of a room.
|
|
45422
|
+
* Each pairwise conversation gets the plaintext encrypted independently.
|
|
45423
|
+
*/
|
|
45424
|
+
async sendToRoom(roomId, plaintext, opts) {
|
|
45425
|
+
if (!this._persisted?.rooms?.[roomId]) {
|
|
45426
|
+
throw new Error(`Room ${roomId} not found`);
|
|
45427
|
+
}
|
|
45428
|
+
const room = this._persisted.rooms[roomId];
|
|
45429
|
+
const messageType = opts?.messageType ?? "text";
|
|
45430
|
+
const recipients = [];
|
|
45431
|
+
for (const convId of room.conversationIds) {
|
|
45432
|
+
const session = this._sessions.get(convId);
|
|
45433
|
+
if (!session) {
|
|
45434
|
+
console.warn(`[SecureChannel] No session for room conv ${convId.slice(0, 8)}..., skipping`);
|
|
45435
|
+
continue;
|
|
45436
|
+
}
|
|
45437
|
+
const encrypted = session.ratchet.encrypt(plaintext);
|
|
45438
|
+
const transport = encryptedMessageToTransport(encrypted);
|
|
45439
|
+
recipients.push({
|
|
45440
|
+
device_id: session.ownerDeviceId,
|
|
45441
|
+
header_blob: transport.header_blob,
|
|
45442
|
+
ciphertext: transport.ciphertext
|
|
45443
|
+
});
|
|
45444
|
+
}
|
|
45445
|
+
if (recipients.length === 0) {
|
|
45446
|
+
throw new Error("No active sessions in room");
|
|
45447
|
+
}
|
|
45448
|
+
if (this._state === "ready" && this._ws) {
|
|
45449
|
+
this._ws.send(
|
|
45450
|
+
JSON.stringify({
|
|
45451
|
+
event: "room_message",
|
|
45452
|
+
room_id: roomId,
|
|
45453
|
+
recipients,
|
|
45454
|
+
message_type: messageType
|
|
45455
|
+
})
|
|
45456
|
+
);
|
|
45457
|
+
} else {
|
|
45458
|
+
try {
|
|
45459
|
+
const res = await fetch(
|
|
45460
|
+
`${this.config.apiUrl}/api/v1/rooms/${roomId}/messages`,
|
|
45461
|
+
{
|
|
45462
|
+
method: "POST",
|
|
45463
|
+
headers: {
|
|
45464
|
+
"Content-Type": "application/json",
|
|
45465
|
+
Authorization: `Bearer ${this._deviceJwt}`
|
|
45466
|
+
},
|
|
45467
|
+
body: JSON.stringify({ recipients, message_type: messageType })
|
|
45468
|
+
}
|
|
45469
|
+
);
|
|
45470
|
+
if (!res.ok) {
|
|
45471
|
+
const detail = await res.text();
|
|
45472
|
+
throw new Error(`Room message failed (${res.status}): ${detail}`);
|
|
45473
|
+
}
|
|
45474
|
+
} catch (err) {
|
|
45475
|
+
throw new Error(`Failed to send room message: ${err}`);
|
|
45476
|
+
}
|
|
45477
|
+
}
|
|
45478
|
+
await this._persistState();
|
|
45479
|
+
}
|
|
45480
|
+
/**
|
|
45481
|
+
* Leave a room: remove sessions and persisted room state.
|
|
45482
|
+
*/
|
|
45483
|
+
async leaveRoom(roomId) {
|
|
45484
|
+
if (!this._persisted?.rooms?.[roomId]) {
|
|
45485
|
+
return;
|
|
45486
|
+
}
|
|
45487
|
+
const room = this._persisted.rooms[roomId];
|
|
45488
|
+
for (const convId of room.conversationIds) {
|
|
45489
|
+
this._sessions.delete(convId);
|
|
45490
|
+
delete this._persisted.sessions[convId];
|
|
45491
|
+
}
|
|
45492
|
+
delete this._persisted.rooms[roomId];
|
|
45493
|
+
await this._persistState();
|
|
45494
|
+
this.emit("room_left", { roomId });
|
|
45495
|
+
}
|
|
45496
|
+
/**
|
|
45497
|
+
* Return info for all joined rooms.
|
|
45498
|
+
*/
|
|
45499
|
+
getRooms() {
|
|
45500
|
+
if (!this._persisted?.rooms) return [];
|
|
45501
|
+
return Object.values(this._persisted.rooms).map((rs) => ({
|
|
45502
|
+
roomId: rs.roomId,
|
|
45503
|
+
name: rs.name,
|
|
45504
|
+
members: rs.members,
|
|
45505
|
+
conversationIds: rs.conversationIds
|
|
45506
|
+
}));
|
|
45507
|
+
}
|
|
45508
|
+
// --- Heartbeat and status methods ---
|
|
45509
|
+
startHeartbeat(intervalSeconds, statusCallback) {
|
|
45510
|
+
this.stopHeartbeat();
|
|
45511
|
+
this._heartbeatCallback = statusCallback;
|
|
45512
|
+
this._heartbeatIntervalSeconds = intervalSeconds;
|
|
45513
|
+
this._sendHeartbeat();
|
|
45514
|
+
this._heartbeatTimer = setInterval(() => {
|
|
45515
|
+
this._sendHeartbeat();
|
|
45516
|
+
}, intervalSeconds * 1e3);
|
|
45517
|
+
}
|
|
45518
|
+
async stopHeartbeat() {
|
|
45519
|
+
if (this._heartbeatTimer) {
|
|
45520
|
+
clearInterval(this._heartbeatTimer);
|
|
45521
|
+
this._heartbeatTimer = null;
|
|
45522
|
+
}
|
|
45523
|
+
if (this._heartbeatCallback && this._state === "ready") {
|
|
45524
|
+
try {
|
|
45525
|
+
await this.send(
|
|
45526
|
+
JSON.stringify({
|
|
45527
|
+
agent_status: "shutting_down",
|
|
45528
|
+
current_task: "",
|
|
45529
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
45530
|
+
}),
|
|
45531
|
+
{
|
|
45532
|
+
messageType: "heartbeat",
|
|
45533
|
+
metadata: { next_heartbeat_seconds: this._heartbeatIntervalSeconds }
|
|
45534
|
+
}
|
|
45535
|
+
);
|
|
45536
|
+
} catch {
|
|
45537
|
+
}
|
|
45538
|
+
}
|
|
45539
|
+
this._heartbeatCallback = null;
|
|
45540
|
+
}
|
|
45541
|
+
async sendStatusAlert(alert) {
|
|
45542
|
+
const priority = alert.severity === "error" || alert.severity === "critical" ? "high" : "normal";
|
|
45543
|
+
await this.send(
|
|
45544
|
+
JSON.stringify({
|
|
45545
|
+
title: alert.title,
|
|
45546
|
+
message: alert.message,
|
|
45547
|
+
severity: alert.severity,
|
|
45548
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
45549
|
+
}),
|
|
45550
|
+
{
|
|
45551
|
+
messageType: "status_alert",
|
|
45552
|
+
priority,
|
|
45553
|
+
metadata: { severity: alert.severity }
|
|
45554
|
+
}
|
|
45555
|
+
);
|
|
45556
|
+
}
|
|
45557
|
+
_sendHeartbeat() {
|
|
45558
|
+
if (this._state !== "ready" || !this._heartbeatCallback) return;
|
|
45559
|
+
const status = this._heartbeatCallback();
|
|
45560
|
+
this.send(
|
|
45561
|
+
JSON.stringify({
|
|
45562
|
+
agent_status: status.agent_status,
|
|
45563
|
+
current_task: status.current_task,
|
|
45564
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
45565
|
+
}),
|
|
45566
|
+
{
|
|
45567
|
+
messageType: "heartbeat",
|
|
45568
|
+
metadata: { next_heartbeat_seconds: this._heartbeatIntervalSeconds }
|
|
45569
|
+
}
|
|
45570
|
+
).catch((err) => {
|
|
45571
|
+
this.emit("error", new Error(`Heartbeat send failed: ${err}`));
|
|
45572
|
+
});
|
|
45573
|
+
}
|
|
45344
45574
|
async stop() {
|
|
45345
45575
|
this._stopped = true;
|
|
45576
|
+
await this.stopHeartbeat();
|
|
45346
45577
|
this._flushAcks();
|
|
45347
45578
|
this._stopPing();
|
|
45348
45579
|
this._stopPollFallback();
|
|
@@ -45712,6 +45943,28 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
|
|
|
45712
45943
|
if (data.event === "message") {
|
|
45713
45944
|
await this._handleIncomingMessage(data.data);
|
|
45714
45945
|
}
|
|
45946
|
+
if (data.event === "room_joined") {
|
|
45947
|
+
const d2 = data.data;
|
|
45948
|
+
this.joinRoom({
|
|
45949
|
+
roomId: d2.room_id,
|
|
45950
|
+
name: d2.name,
|
|
45951
|
+
members: (d2.members || []).map((m2) => ({
|
|
45952
|
+
deviceId: m2.device_id,
|
|
45953
|
+
entityType: m2.entity_type,
|
|
45954
|
+
displayName: m2.display_name,
|
|
45955
|
+
identityPublicKey: m2.identity_public_key,
|
|
45956
|
+
ephemeralPublicKey: m2.ephemeral_public_key
|
|
45957
|
+
})),
|
|
45958
|
+
conversations: (d2.conversations || []).map((c2) => ({
|
|
45959
|
+
id: c2.id,
|
|
45960
|
+
participantA: c2.participant_a,
|
|
45961
|
+
participantB: c2.participant_b
|
|
45962
|
+
}))
|
|
45963
|
+
}).catch((err) => this.emit("error", err));
|
|
45964
|
+
}
|
|
45965
|
+
if (data.event === "room_message") {
|
|
45966
|
+
await this._handleRoomMessage(data.data);
|
|
45967
|
+
}
|
|
45715
45968
|
} catch (err) {
|
|
45716
45969
|
this.emit("error", err);
|
|
45717
45970
|
}
|
|
@@ -46061,6 +46314,70 @@ ${messageText}`;
|
|
|
46061
46314
|
this.emit("error", err);
|
|
46062
46315
|
}
|
|
46063
46316
|
}
|
|
46317
|
+
/**
|
|
46318
|
+
* Handle an incoming room message. Finds the pairwise conversation
|
|
46319
|
+
* for the sender, decrypts, and emits a room_message event.
|
|
46320
|
+
*/
|
|
46321
|
+
async _handleRoomMessage(msgData) {
|
|
46322
|
+
if (msgData.sender_device_id === this._deviceId) return;
|
|
46323
|
+
const convId = msgData.conversation_id ?? this._findConversationForSender(msgData.sender_device_id, msgData.room_id);
|
|
46324
|
+
if (!convId) {
|
|
46325
|
+
console.warn(
|
|
46326
|
+
`[SecureChannel] No conversation found for sender ${msgData.sender_device_id.slice(0, 8)}... in room ${msgData.room_id}`
|
|
46327
|
+
);
|
|
46328
|
+
return;
|
|
46329
|
+
}
|
|
46330
|
+
const session = this._sessions.get(convId);
|
|
46331
|
+
if (!session) {
|
|
46332
|
+
console.warn(
|
|
46333
|
+
`[SecureChannel] No session for room conv ${convId.slice(0, 8)}..., skipping`
|
|
46334
|
+
);
|
|
46335
|
+
return;
|
|
46336
|
+
}
|
|
46337
|
+
const encrypted = transportToEncryptedMessage({
|
|
46338
|
+
header_blob: msgData.header_blob,
|
|
46339
|
+
ciphertext: msgData.ciphertext
|
|
46340
|
+
});
|
|
46341
|
+
const plaintext = session.ratchet.decrypt(encrypted);
|
|
46342
|
+
if (!session.activated) {
|
|
46343
|
+
session.activated = true;
|
|
46344
|
+
console.log(
|
|
46345
|
+
`[SecureChannel] Room session ${convId.slice(0, 8)}... activated by first message`
|
|
46346
|
+
);
|
|
46347
|
+
}
|
|
46348
|
+
if (msgData.message_id) {
|
|
46349
|
+
this._sendAck(msgData.message_id);
|
|
46350
|
+
}
|
|
46351
|
+
await this._persistState();
|
|
46352
|
+
const metadata = {
|
|
46353
|
+
messageId: msgData.message_id ?? "",
|
|
46354
|
+
conversationId: convId,
|
|
46355
|
+
timestamp: msgData.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
46356
|
+
messageType: msgData.message_type ?? "text"
|
|
46357
|
+
};
|
|
46358
|
+
this.emit("room_message", {
|
|
46359
|
+
roomId: msgData.room_id,
|
|
46360
|
+
senderDeviceId: msgData.sender_device_id,
|
|
46361
|
+
plaintext,
|
|
46362
|
+
messageType: msgData.message_type ?? "text",
|
|
46363
|
+
timestamp: msgData.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
46364
|
+
});
|
|
46365
|
+
this.config.onMessage?.(plaintext, metadata);
|
|
46366
|
+
}
|
|
46367
|
+
/**
|
|
46368
|
+
* Find the pairwise conversation ID for a given sender in a room.
|
|
46369
|
+
*/
|
|
46370
|
+
_findConversationForSender(senderDeviceId, roomId) {
|
|
46371
|
+
const room = this._persisted?.rooms?.[roomId];
|
|
46372
|
+
if (!room) return null;
|
|
46373
|
+
for (const convId of room.conversationIds) {
|
|
46374
|
+
const session = this._sessions.get(convId);
|
|
46375
|
+
if (session && session.ownerDeviceId === senderDeviceId) {
|
|
46376
|
+
return convId;
|
|
46377
|
+
}
|
|
46378
|
+
}
|
|
46379
|
+
return null;
|
|
46380
|
+
}
|
|
46064
46381
|
/**
|
|
46065
46382
|
* Sync missed messages across ALL sessions.
|
|
46066
46383
|
* For each conversation, fetches messages since last sync and decrypts.
|