@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/channel.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from "node:events";
2
- import type { SecureChannelConfig, ChannelState, SendOptions, DecisionRequest, DecisionResponse, HeartbeatStatus, StatusAlert } 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;
@@ -67,9 +67,46 @@ export declare class SecureChannel extends EventEmitter {
67
67
  * Optional timeout rejects with an Error.
68
68
  */
69
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[];
70
95
  startHeartbeat(intervalSeconds: number, statusCallback: () => HeartbeatStatus): void;
71
96
  stopHeartbeat(): Promise<void>;
72
97
  sendStatusAlert(alert: StatusAlert): Promise<void>;
98
+ sendArtifact(artifact: {
99
+ filePath: string;
100
+ filename: string;
101
+ mimeType: string;
102
+ description?: string;
103
+ }): Promise<void>;
104
+ sendActionConfirmation(confirmation: {
105
+ action: string;
106
+ status: "completed" | "failed" | "partial";
107
+ decisionId?: string;
108
+ detail?: string;
109
+ }): Promise<void>;
73
110
  private _sendHeartbeat;
74
111
  stop(): Promise<void>;
75
112
  startHttpServer(port: number): void;
@@ -135,6 +172,15 @@ export declare class SecureChannel extends EventEmitter {
135
172
  * a new ratchet session.
136
173
  */
137
174
  private _handleDeviceLinked;
175
+ /**
176
+ * Handle an incoming room message. Finds the pairwise conversation
177
+ * for the sender, decrypts, and emits a room_message event.
178
+ */
179
+ private _handleRoomMessage;
180
+ /**
181
+ * Find the pairwise conversation ID for a given sender in a room.
182
+ */
183
+ private _findConversationForSender;
138
184
  /**
139
185
  * Sync missed messages across ALL sessions.
140
186
  * For each conversation, fetches messages since last sync and decrypts.
@@ -1 +1 @@
1
- {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -45413,6 +45413,168 @@ var init_channel = __esm({
45413
45413
  }
45414
45414
  });
45415
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 ---
45416
45578
  startHeartbeat(intervalSeconds, statusCallback) {
45417
45579
  this.stopHeartbeat();
45418
45580
  this._heartbeatCallback = statusCallback;
@@ -45447,13 +45609,17 @@ var init_channel = __esm({
45447
45609
  }
45448
45610
  async sendStatusAlert(alert) {
45449
45611
  const priority = alert.severity === "error" || alert.severity === "critical" ? "high" : "normal";
45612
+ const envelope = {
45613
+ title: alert.title,
45614
+ message: alert.message,
45615
+ severity: alert.severity,
45616
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
45617
+ };
45618
+ if (alert.detail !== void 0) envelope.detail = alert.detail;
45619
+ if (alert.detailFormat !== void 0) envelope.detail_format = alert.detailFormat;
45620
+ if (alert.category !== void 0) envelope.category = alert.category;
45450
45621
  await this.send(
45451
- JSON.stringify({
45452
- title: alert.title,
45453
- message: alert.message,
45454
- severity: alert.severity,
45455
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
45456
- }),
45622
+ JSON.stringify(envelope),
45457
45623
  {
45458
45624
  messageType: "status_alert",
45459
45625
  priority,
@@ -45461,6 +45627,57 @@ var init_channel = __esm({
45461
45627
  }
45462
45628
  );
45463
45629
  }
45630
+ async sendArtifact(artifact) {
45631
+ if (this._state !== "ready" || this._sessions.size === 0 || !this._ws) {
45632
+ throw new Error("Channel is not ready");
45633
+ }
45634
+ const attachMeta = await this._uploadAttachment(artifact.filePath, this._primaryConversationId);
45635
+ const envelope = JSON.stringify({
45636
+ type: "artifact",
45637
+ blob_id: attachMeta.blobId,
45638
+ blob_url: attachMeta.blobUrl,
45639
+ filename: artifact.filename,
45640
+ mime_type: artifact.mimeType,
45641
+ size_bytes: attachMeta.size,
45642
+ description: artifact.description,
45643
+ attachment: attachMeta
45644
+ });
45645
+ const messageGroupId = randomUUID();
45646
+ for (const [convId, session] of this._sessions) {
45647
+ if (!session.activated) continue;
45648
+ const encrypted = session.ratchet.encrypt(envelope);
45649
+ const transport = encryptedMessageToTransport(encrypted);
45650
+ this._ws.send(
45651
+ JSON.stringify({
45652
+ event: "message",
45653
+ data: {
45654
+ conversation_id: convId,
45655
+ header_blob: transport.header_blob,
45656
+ ciphertext: transport.ciphertext,
45657
+ message_group_id: messageGroupId,
45658
+ message_type: "artifact_share"
45659
+ }
45660
+ })
45661
+ );
45662
+ }
45663
+ await this._persistState();
45664
+ }
45665
+ async sendActionConfirmation(confirmation) {
45666
+ const envelope = {
45667
+ type: "action_confirmation",
45668
+ action: confirmation.action,
45669
+ status: confirmation.status
45670
+ };
45671
+ if (confirmation.decisionId !== void 0) envelope.decision_id = confirmation.decisionId;
45672
+ if (confirmation.detail !== void 0) envelope.detail = confirmation.detail;
45673
+ await this.send(
45674
+ JSON.stringify(envelope),
45675
+ {
45676
+ messageType: "action_confirmation",
45677
+ metadata: { status: confirmation.status }
45678
+ }
45679
+ );
45680
+ }
45464
45681
  _sendHeartbeat() {
45465
45682
  if (this._state !== "ready" || !this._heartbeatCallback) return;
45466
45683
  const status = this._heartbeatCallback();
@@ -45850,6 +46067,28 @@ var init_channel = __esm({
45850
46067
  if (data.event === "message") {
45851
46068
  await this._handleIncomingMessage(data.data);
45852
46069
  }
46070
+ if (data.event === "room_joined") {
46071
+ const d2 = data.data;
46072
+ this.joinRoom({
46073
+ roomId: d2.room_id,
46074
+ name: d2.name,
46075
+ members: (d2.members || []).map((m2) => ({
46076
+ deviceId: m2.device_id,
46077
+ entityType: m2.entity_type,
46078
+ displayName: m2.display_name,
46079
+ identityPublicKey: m2.identity_public_key,
46080
+ ephemeralPublicKey: m2.ephemeral_public_key
46081
+ })),
46082
+ conversations: (d2.conversations || []).map((c2) => ({
46083
+ id: c2.id,
46084
+ participantA: c2.participant_a,
46085
+ participantB: c2.participant_b
46086
+ }))
46087
+ }).catch((err) => this.emit("error", err));
46088
+ }
46089
+ if (data.event === "room_message") {
46090
+ await this._handleRoomMessage(data.data);
46091
+ }
45853
46092
  } catch (err) {
45854
46093
  this.emit("error", err);
45855
46094
  }
@@ -46199,6 +46438,70 @@ ${messageText}`;
46199
46438
  this.emit("error", err);
46200
46439
  }
46201
46440
  }
46441
+ /**
46442
+ * Handle an incoming room message. Finds the pairwise conversation
46443
+ * for the sender, decrypts, and emits a room_message event.
46444
+ */
46445
+ async _handleRoomMessage(msgData) {
46446
+ if (msgData.sender_device_id === this._deviceId) return;
46447
+ const convId = msgData.conversation_id ?? this._findConversationForSender(msgData.sender_device_id, msgData.room_id);
46448
+ if (!convId) {
46449
+ console.warn(
46450
+ `[SecureChannel] No conversation found for sender ${msgData.sender_device_id.slice(0, 8)}... in room ${msgData.room_id}`
46451
+ );
46452
+ return;
46453
+ }
46454
+ const session = this._sessions.get(convId);
46455
+ if (!session) {
46456
+ console.warn(
46457
+ `[SecureChannel] No session for room conv ${convId.slice(0, 8)}..., skipping`
46458
+ );
46459
+ return;
46460
+ }
46461
+ const encrypted = transportToEncryptedMessage({
46462
+ header_blob: msgData.header_blob,
46463
+ ciphertext: msgData.ciphertext
46464
+ });
46465
+ const plaintext = session.ratchet.decrypt(encrypted);
46466
+ if (!session.activated) {
46467
+ session.activated = true;
46468
+ console.log(
46469
+ `[SecureChannel] Room session ${convId.slice(0, 8)}... activated by first message`
46470
+ );
46471
+ }
46472
+ if (msgData.message_id) {
46473
+ this._sendAck(msgData.message_id);
46474
+ }
46475
+ await this._persistState();
46476
+ const metadata = {
46477
+ messageId: msgData.message_id ?? "",
46478
+ conversationId: convId,
46479
+ timestamp: msgData.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
46480
+ messageType: msgData.message_type ?? "text"
46481
+ };
46482
+ this.emit("room_message", {
46483
+ roomId: msgData.room_id,
46484
+ senderDeviceId: msgData.sender_device_id,
46485
+ plaintext,
46486
+ messageType: msgData.message_type ?? "text",
46487
+ timestamp: msgData.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
46488
+ });
46489
+ this.config.onMessage?.(plaintext, metadata);
46490
+ }
46491
+ /**
46492
+ * Find the pairwise conversation ID for a given sender in a room.
46493
+ */
46494
+ _findConversationForSender(senderDeviceId, roomId) {
46495
+ const room = this._persisted?.rooms?.[roomId];
46496
+ if (!room) return null;
46497
+ for (const convId of room.conversationIds) {
46498
+ const session = this._sessions.get(convId);
46499
+ if (session && session.ownerDeviceId === senderDeviceId) {
46500
+ return convId;
46501
+ }
46502
+ }
46503
+ return null;
46504
+ }
46202
46505
  /**
46203
46506
  * Sync missed messages across ALL sessions.
46204
46507
  * For each conversation, fetches messages since last sync and decrypts.