@agentvault/secure-channel 0.6.21 → 0.6.23

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.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, HeartbeatStatus, StatusAlert, } 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
@@ -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,EACV,eAAe,EACf,WAAW,GACZ,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExF,eAAO,MAAM,OAAO,WAAW,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,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
@@ -45344,6 +45344,168 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45344
45344
  }
45345
45345
  });
45346
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 ---
45347
45509
  startHeartbeat(intervalSeconds, statusCallback) {
45348
45510
  this.stopHeartbeat();
45349
45511
  this._heartbeatCallback = statusCallback;
@@ -45378,13 +45540,17 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45378
45540
  }
45379
45541
  async sendStatusAlert(alert) {
45380
45542
  const priority = alert.severity === "error" || alert.severity === "critical" ? "high" : "normal";
45543
+ const envelope = {
45544
+ title: alert.title,
45545
+ message: alert.message,
45546
+ severity: alert.severity,
45547
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
45548
+ };
45549
+ if (alert.detail !== void 0) envelope.detail = alert.detail;
45550
+ if (alert.detailFormat !== void 0) envelope.detail_format = alert.detailFormat;
45551
+ if (alert.category !== void 0) envelope.category = alert.category;
45381
45552
  await this.send(
45382
- JSON.stringify({
45383
- title: alert.title,
45384
- message: alert.message,
45385
- severity: alert.severity,
45386
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
45387
- }),
45553
+ JSON.stringify(envelope),
45388
45554
  {
45389
45555
  messageType: "status_alert",
45390
45556
  priority,
@@ -45392,6 +45558,57 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45392
45558
  }
45393
45559
  );
45394
45560
  }
45561
+ async sendArtifact(artifact) {
45562
+ if (this._state !== "ready" || this._sessions.size === 0 || !this._ws) {
45563
+ throw new Error("Channel is not ready");
45564
+ }
45565
+ const attachMeta = await this._uploadAttachment(artifact.filePath, this._primaryConversationId);
45566
+ const envelope = JSON.stringify({
45567
+ type: "artifact",
45568
+ blob_id: attachMeta.blobId,
45569
+ blob_url: attachMeta.blobUrl,
45570
+ filename: artifact.filename,
45571
+ mime_type: artifact.mimeType,
45572
+ size_bytes: attachMeta.size,
45573
+ description: artifact.description,
45574
+ attachment: attachMeta
45575
+ });
45576
+ const messageGroupId = randomUUID();
45577
+ for (const [convId, session] of this._sessions) {
45578
+ if (!session.activated) continue;
45579
+ const encrypted = session.ratchet.encrypt(envelope);
45580
+ const transport = encryptedMessageToTransport(encrypted);
45581
+ this._ws.send(
45582
+ JSON.stringify({
45583
+ event: "message",
45584
+ data: {
45585
+ conversation_id: convId,
45586
+ header_blob: transport.header_blob,
45587
+ ciphertext: transport.ciphertext,
45588
+ message_group_id: messageGroupId,
45589
+ message_type: "artifact_share"
45590
+ }
45591
+ })
45592
+ );
45593
+ }
45594
+ await this._persistState();
45595
+ }
45596
+ async sendActionConfirmation(confirmation) {
45597
+ const envelope = {
45598
+ type: "action_confirmation",
45599
+ action: confirmation.action,
45600
+ status: confirmation.status
45601
+ };
45602
+ if (confirmation.decisionId !== void 0) envelope.decision_id = confirmation.decisionId;
45603
+ if (confirmation.detail !== void 0) envelope.detail = confirmation.detail;
45604
+ await this.send(
45605
+ JSON.stringify(envelope),
45606
+ {
45607
+ messageType: "action_confirmation",
45608
+ metadata: { status: confirmation.status }
45609
+ }
45610
+ );
45611
+ }
45395
45612
  _sendHeartbeat() {
45396
45613
  if (this._state !== "ready" || !this._heartbeatCallback) return;
45397
45614
  const status = this._heartbeatCallback();
@@ -45781,6 +45998,28 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45781
45998
  if (data.event === "message") {
45782
45999
  await this._handleIncomingMessage(data.data);
45783
46000
  }
46001
+ if (data.event === "room_joined") {
46002
+ const d2 = data.data;
46003
+ this.joinRoom({
46004
+ roomId: d2.room_id,
46005
+ name: d2.name,
46006
+ members: (d2.members || []).map((m2) => ({
46007
+ deviceId: m2.device_id,
46008
+ entityType: m2.entity_type,
46009
+ displayName: m2.display_name,
46010
+ identityPublicKey: m2.identity_public_key,
46011
+ ephemeralPublicKey: m2.ephemeral_public_key
46012
+ })),
46013
+ conversations: (d2.conversations || []).map((c2) => ({
46014
+ id: c2.id,
46015
+ participantA: c2.participant_a,
46016
+ participantB: c2.participant_b
46017
+ }))
46018
+ }).catch((err) => this.emit("error", err));
46019
+ }
46020
+ if (data.event === "room_message") {
46021
+ await this._handleRoomMessage(data.data);
46022
+ }
45784
46023
  } catch (err) {
45785
46024
  this.emit("error", err);
45786
46025
  }
@@ -46130,6 +46369,70 @@ ${messageText}`;
46130
46369
  this.emit("error", err);
46131
46370
  }
46132
46371
  }
46372
+ /**
46373
+ * Handle an incoming room message. Finds the pairwise conversation
46374
+ * for the sender, decrypts, and emits a room_message event.
46375
+ */
46376
+ async _handleRoomMessage(msgData) {
46377
+ if (msgData.sender_device_id === this._deviceId) return;
46378
+ const convId = msgData.conversation_id ?? this._findConversationForSender(msgData.sender_device_id, msgData.room_id);
46379
+ if (!convId) {
46380
+ console.warn(
46381
+ `[SecureChannel] No conversation found for sender ${msgData.sender_device_id.slice(0, 8)}... in room ${msgData.room_id}`
46382
+ );
46383
+ return;
46384
+ }
46385
+ const session = this._sessions.get(convId);
46386
+ if (!session) {
46387
+ console.warn(
46388
+ `[SecureChannel] No session for room conv ${convId.slice(0, 8)}..., skipping`
46389
+ );
46390
+ return;
46391
+ }
46392
+ const encrypted = transportToEncryptedMessage({
46393
+ header_blob: msgData.header_blob,
46394
+ ciphertext: msgData.ciphertext
46395
+ });
46396
+ const plaintext = session.ratchet.decrypt(encrypted);
46397
+ if (!session.activated) {
46398
+ session.activated = true;
46399
+ console.log(
46400
+ `[SecureChannel] Room session ${convId.slice(0, 8)}... activated by first message`
46401
+ );
46402
+ }
46403
+ if (msgData.message_id) {
46404
+ this._sendAck(msgData.message_id);
46405
+ }
46406
+ await this._persistState();
46407
+ const metadata = {
46408
+ messageId: msgData.message_id ?? "",
46409
+ conversationId: convId,
46410
+ timestamp: msgData.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
46411
+ messageType: msgData.message_type ?? "text"
46412
+ };
46413
+ this.emit("room_message", {
46414
+ roomId: msgData.room_id,
46415
+ senderDeviceId: msgData.sender_device_id,
46416
+ plaintext,
46417
+ messageType: msgData.message_type ?? "text",
46418
+ timestamp: msgData.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
46419
+ });
46420
+ this.config.onMessage?.(plaintext, metadata);
46421
+ }
46422
+ /**
46423
+ * Find the pairwise conversation ID for a given sender in a room.
46424
+ */
46425
+ _findConversationForSender(senderDeviceId, roomId) {
46426
+ const room = this._persisted?.rooms?.[roomId];
46427
+ if (!room) return null;
46428
+ for (const convId of room.conversationIds) {
46429
+ const session = this._sessions.get(convId);
46430
+ if (session && session.ownerDeviceId === senderDeviceId) {
46431
+ return convId;
46432
+ }
46433
+ }
46434
+ return null;
46435
+ }
46133
46436
  /**
46134
46437
  * Sync missed messages across ALL sessions.
46135
46438
  * For each conversation, fetches messages since last sync and decrypts.