@agentvault/secure-channel 0.6.11 → 0.6.13

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
@@ -18,6 +18,7 @@ export declare class SecureChannel extends EventEmitter {
18
18
  private _ackTimer;
19
19
  private _stopped;
20
20
  private _persisted;
21
+ private _httpServer;
21
22
  private static readonly PING_INTERVAL_MS;
22
23
  private static readonly PING_TIMEOUT_MS;
23
24
  constructor(config: SecureChannelConfig);
@@ -43,6 +44,8 @@ export declare class SecureChannel extends EventEmitter {
43
44
  topicId?: string;
44
45
  }): Promise<void>;
45
46
  stop(): Promise<void>;
47
+ startHttpServer(port: number): void;
48
+ private _stopHttpServer;
46
49
  /**
47
50
  * Create a new topic within the conversation group.
48
51
  * Requires the channel to be initialized with a groupId (from activation).
@@ -71,6 +74,23 @@ export declare class SecureChannel extends EventEmitter {
71
74
  * and relays as sync messages to sibling sessions.
72
75
  */
73
76
  private _handleIncomingMessage;
77
+ /**
78
+ * Download an encrypted attachment blob, decrypt it, verify integrity,
79
+ * and save the plaintext file to disk.
80
+ */
81
+ private _downloadAndDecryptAttachment;
82
+ /**
83
+ * Upload an attachment file: encrypt, upload to server, return metadata
84
+ * for inclusion in the message envelope.
85
+ */
86
+ private _uploadAttachment;
87
+ /**
88
+ * Send a message with an attached file. Encrypts the file, uploads it,
89
+ * then sends the envelope with attachment metadata via Double Ratchet.
90
+ */
91
+ sendWithAttachment(plaintext: string, filePath: string, options?: {
92
+ topicId?: string;
93
+ }): Promise<void>;
74
94
  /**
75
95
  * Relay an owner's message to all sibling sessions as encrypted sync messages.
76
96
  * This allows all owner devices to see messages from any single device.
@@ -1 +1 @@
1
- {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAa3C,OAAO,KAAK,EACV,mBAAmB,EACnB,YAAY,EAKb,MAAM,YAAY,CAAC;AAiDpB,qBAAa,aAAc,SAAQ,YAAY;IAwBjC,OAAO,CAAC,MAAM;IAvB1B,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,sBAAsB,CAAc;IAC5C,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,SAAS,CAGH;IACd,OAAO,CAAC,GAAG,CAA0B;IACrC,OAAO,CAAC,UAAU,CAA8C;IAChE,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,eAAe,CAA8C;IACrE,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,SAAS,CAA8C;IAC/D,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAA+B;IAEjD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAU;IAClD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAW;gBAE9B,MAAM,EAAE,mBAAmB;IAI/C,IAAI,KAAK,IAAI,YAAY,CAExB;IAED,IAAI,QAAQ,IAAI,MAAM,GAAG,IAAI,CAE5B;IAED,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IAED,iEAAiE;IACjE,IAAI,cAAc,IAAI,MAAM,GAAG,IAAI,CAElC;IAED,2CAA2C;IAC3C,IAAI,eAAe,IAAI,MAAM,EAAE,CAE9B;IAED,6CAA6C;IAC7C,IAAI,YAAY,IAAI,MAAM,CAEzB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAsC5B;;OAEG;IACH,OAAO,CAAC,cAAc;IAuBtB;;;OAGG;IACG,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAyCtE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B3B;;;OAGG;IACG,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IAsC1F;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;YAiCtE,OAAO;IAgDrB,OAAO,CAAC,KAAK;YAsCC,SAAS;IAyIvB,OAAO,CAAC,QAAQ;IAwEhB;;;;OAIG;YACW,sBAAsB;IA2FpC;;;OAGG;YACW,oBAAoB;IAqClC;;;OAGG;YACW,uBAAuB;IAkCrC;;;;OAIG;YACW,mBAAmB;IAkEjC;;;OAGG;YACW,mBAAmB;IA+FjC,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,UAAU;IAoBlB,OAAO,CAAC,SAAS;IAWjB,OAAO,CAAC,kBAAkB;IAiB1B,OAAO,CAAC,SAAS;IAOjB,OAAO,CAAC,YAAY;IAKpB;;;OAGG;YACW,aAAa;CAc5B"}
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  // src/channel.ts
4
4
  import { EventEmitter } from "node:events";
5
+ import { createServer } from "node:http";
5
6
  import { randomUUID } from "node:crypto";
7
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
8
+ import { join as join2 } from "node:path";
9
+ import { readFile as readFile2 } from "node:fs/promises";
6
10
 
7
11
  // ../../node_modules/libsodium-sumo/dist/modules-sumo-esm/libsodium-sumo.mjs
8
12
  var __filename;
@@ -44919,6 +44923,39 @@ var DoubleRatchet = class _DoubleRatchet {
44919
44923
  }
44920
44924
  };
44921
44925
 
44926
+ // ../crypto/dist/file-crypto.js
44927
+ function encryptFile(plainData) {
44928
+ const fileKey = libsodium_wrappers_default.randombytes_buf(libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_KEYBYTES);
44929
+ const fileNonce = libsodium_wrappers_default.randombytes_buf(libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
44930
+ const encryptedData = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_encrypt(
44931
+ plainData,
44932
+ null,
44933
+ // no additional data
44934
+ null,
44935
+ // secret nonce (unused)
44936
+ fileNonce,
44937
+ fileKey
44938
+ );
44939
+ const digestBytes = libsodium_wrappers_default.crypto_generichash(32, encryptedData);
44940
+ const digest = libsodium_wrappers_default.to_hex(digestBytes);
44941
+ return { encryptedData, fileKey, fileNonce, digest };
44942
+ }
44943
+ function decryptFile(encryptedData, fileKey, fileNonce) {
44944
+ return libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(
44945
+ null,
44946
+ // secret nonce (unused)
44947
+ encryptedData,
44948
+ null,
44949
+ // no additional data
44950
+ fileNonce,
44951
+ fileKey
44952
+ );
44953
+ }
44954
+ function computeFileDigest(data) {
44955
+ const digestBytes = libsodium_wrappers_default.crypto_generichash(32, data);
44956
+ return libsodium_wrappers_default.to_hex(digestBytes);
44957
+ }
44958
+
44922
44959
  // src/crypto-helpers.ts
44923
44960
  function hexToBytes(hex) {
44924
44961
  return libsodium_wrappers_default.from_hex(hex);
@@ -45083,6 +45120,7 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45083
45120
  _ackTimer = null;
45084
45121
  _stopped = false;
45085
45122
  _persisted = null;
45123
+ _httpServer = null;
45086
45124
  static PING_INTERVAL_MS = 3e4;
45087
45125
  // Send ping every 30s
45088
45126
  static PING_TIMEOUT_MS = 1e4;
@@ -45195,6 +45233,7 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45195
45233
  this._stopped = true;
45196
45234
  this._flushAcks();
45197
45235
  this._stopPing();
45236
+ this._stopHttpServer();
45198
45237
  if (this._ackTimer) {
45199
45238
  clearTimeout(this._ackTimer);
45200
45239
  this._ackTimer = null;
@@ -45214,6 +45253,65 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45214
45253
  }
45215
45254
  this._setState("disconnected");
45216
45255
  }
45256
+ // --- Local HTTP server for proactive sends ---
45257
+ startHttpServer(port) {
45258
+ if (this._httpServer) return;
45259
+ this._httpServer = createServer(async (req, res) => {
45260
+ const remote = req.socket.remoteAddress;
45261
+ if (remote !== "127.0.0.1" && remote !== "::1" && remote !== "::ffff:127.0.0.1") {
45262
+ res.writeHead(403, { "Content-Type": "application/json" });
45263
+ res.end(JSON.stringify({ ok: false, error: "Forbidden" }));
45264
+ return;
45265
+ }
45266
+ if (req.method === "POST" && req.url === "/send") {
45267
+ let body = "";
45268
+ req.on("data", (chunk) => {
45269
+ body += chunk.toString();
45270
+ });
45271
+ req.on("end", async () => {
45272
+ try {
45273
+ const parsed = JSON.parse(body);
45274
+ const text = parsed.text;
45275
+ if (!text || typeof text !== "string") {
45276
+ res.writeHead(400, { "Content-Type": "application/json" });
45277
+ res.end(JSON.stringify({ ok: false, error: "Missing 'text' field" }));
45278
+ return;
45279
+ }
45280
+ if (parsed.file_path && typeof parsed.file_path === "string") {
45281
+ await this.sendWithAttachment(text, parsed.file_path, { topicId: parsed.topicId });
45282
+ } else {
45283
+ await this.send(text, { topicId: parsed.topicId });
45284
+ }
45285
+ res.writeHead(200, { "Content-Type": "application/json" });
45286
+ res.end(JSON.stringify({ ok: true }));
45287
+ } catch (err) {
45288
+ res.writeHead(500, { "Content-Type": "application/json" });
45289
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
45290
+ }
45291
+ });
45292
+ } else if (req.method === "GET" && req.url === "/status") {
45293
+ res.writeHead(200, { "Content-Type": "application/json" });
45294
+ res.end(JSON.stringify({
45295
+ ok: true,
45296
+ state: this._state,
45297
+ deviceId: this._deviceId,
45298
+ sessions: this._sessions.size
45299
+ }));
45300
+ } else {
45301
+ res.writeHead(404, { "Content-Type": "application/json" });
45302
+ res.end(JSON.stringify({ ok: false, error: "Not found. Use POST /send or GET /status" }));
45303
+ }
45304
+ });
45305
+ this._httpServer.listen(port, "127.0.0.1", () => {
45306
+ this.emit("http-ready", port);
45307
+ });
45308
+ }
45309
+ _stopHttpServer() {
45310
+ if (this._httpServer) {
45311
+ this._httpServer.close();
45312
+ this._httpServer = null;
45313
+ }
45314
+ }
45217
45315
  // --- Topic management ---
45218
45316
  /**
45219
45317
  * Create a new topic within the conversation group.
@@ -45536,10 +45634,14 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45536
45634
  }
45537
45635
  let messageText;
45538
45636
  let messageType;
45637
+ let attachmentInfo = null;
45539
45638
  try {
45540
45639
  const parsed = JSON.parse(plaintext);
45541
45640
  messageType = parsed.type || "message";
45542
45641
  messageText = parsed.text || plaintext;
45642
+ if (parsed.attachment) {
45643
+ attachmentInfo = parsed.attachment;
45644
+ }
45543
45645
  } catch {
45544
45646
  messageType = "message";
45545
45647
  messageText = plaintext;
@@ -45552,15 +45654,29 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45552
45654
  }
45553
45655
  if (messageType === "message") {
45554
45656
  const topicId = msgData.topic_id;
45657
+ let attachmentPath;
45658
+ if (attachmentInfo) {
45659
+ try {
45660
+ attachmentPath = await this._downloadAndDecryptAttachment(attachmentInfo);
45661
+ } catch (err) {
45662
+ console.error(`[SecureChannel] Failed to download attachment:`, err);
45663
+ }
45664
+ }
45555
45665
  this._appendHistory("owner", messageText, topicId);
45666
+ let emitText = messageText;
45667
+ if (attachmentPath) {
45668
+ emitText = `[Attachment: ${attachmentInfo.filename} saved to ${attachmentPath}]
45669
+
45670
+ ${messageText}`;
45671
+ }
45556
45672
  const metadata = {
45557
45673
  messageId: msgData.message_id,
45558
45674
  conversationId: convId,
45559
45675
  timestamp: msgData.created_at,
45560
45676
  topicId
45561
45677
  };
45562
- this.emit("message", messageText, metadata);
45563
- this.config.onMessage?.(messageText, metadata);
45678
+ this.emit("message", emitText, metadata);
45679
+ this.config.onMessage?.(emitText, metadata);
45564
45680
  await this._relaySyncToSiblings(convId, session.ownerDeviceId, messageText, topicId);
45565
45681
  }
45566
45682
  if (this._persisted) {
@@ -45568,6 +45684,111 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45568
45684
  }
45569
45685
  await this._persistState();
45570
45686
  }
45687
+ /**
45688
+ * Download an encrypted attachment blob, decrypt it, verify integrity,
45689
+ * and save the plaintext file to disk.
45690
+ */
45691
+ async _downloadAndDecryptAttachment(info) {
45692
+ const attachDir = join2(this.config.dataDir, "attachments");
45693
+ await mkdir2(attachDir, { recursive: true });
45694
+ const url = `${this.config.apiUrl}${info.blobUrl}`;
45695
+ const res = await fetch(url, {
45696
+ headers: { Authorization: `Bearer ${this._deviceJwt}` }
45697
+ });
45698
+ if (!res.ok) {
45699
+ throw new Error(`Attachment download failed: ${res.status}`);
45700
+ }
45701
+ const buffer = await res.arrayBuffer();
45702
+ const encryptedData = new Uint8Array(buffer);
45703
+ const digest = computeFileDigest(encryptedData);
45704
+ if (digest !== info.digest) {
45705
+ throw new Error("Attachment digest mismatch \u2014 possible tampering");
45706
+ }
45707
+ const fileKey = base64ToBytes(info.fileKey);
45708
+ const fileNonce = base64ToBytes(info.fileNonce);
45709
+ const decrypted = decryptFile(encryptedData, fileKey, fileNonce);
45710
+ const filePath = join2(attachDir, info.filename);
45711
+ await writeFile2(filePath, decrypted);
45712
+ console.log(`[SecureChannel] Attachment saved: ${filePath} (${decrypted.length} bytes)`);
45713
+ return filePath;
45714
+ }
45715
+ /**
45716
+ * Upload an attachment file: encrypt, upload to server, return metadata
45717
+ * for inclusion in the message envelope.
45718
+ */
45719
+ async _uploadAttachment(filePath, conversationId) {
45720
+ const data = await readFile2(filePath);
45721
+ const plainData = new Uint8Array(data);
45722
+ const result = encryptFile(plainData);
45723
+ const { Blob: NodeBlob, FormData: NodeFormData } = await import("node:buffer").then(
45724
+ () => globalThis
45725
+ );
45726
+ const formData = new FormData();
45727
+ formData.append("conversation_id", conversationId);
45728
+ formData.append(
45729
+ "file",
45730
+ new Blob([result.encryptedData.buffer], { type: "application/octet-stream" }),
45731
+ "attachment.bin"
45732
+ );
45733
+ const res = await fetch(`${this.config.apiUrl}/api/v1/attachments/upload`, {
45734
+ method: "POST",
45735
+ headers: { Authorization: `Bearer ${this._deviceJwt}` },
45736
+ body: formData
45737
+ });
45738
+ if (!res.ok) {
45739
+ const detail = await res.text();
45740
+ throw new Error(`Attachment upload failed (${res.status}): ${detail}`);
45741
+ }
45742
+ const resp = await res.json();
45743
+ const filename = filePath.split("/").pop() || "file";
45744
+ return {
45745
+ blobId: resp.blob_id,
45746
+ blobUrl: resp.blob_url,
45747
+ fileKey: bytesToBase64(result.fileKey),
45748
+ fileNonce: bytesToBase64(result.fileNonce),
45749
+ digest: result.digest,
45750
+ filename,
45751
+ mime: "application/octet-stream",
45752
+ size: plainData.length
45753
+ };
45754
+ }
45755
+ /**
45756
+ * Send a message with an attached file. Encrypts the file, uploads it,
45757
+ * then sends the envelope with attachment metadata via Double Ratchet.
45758
+ */
45759
+ async sendWithAttachment(plaintext, filePath, options) {
45760
+ if (this._state !== "ready" || this._sessions.size === 0 || !this._ws) {
45761
+ throw new Error("Channel is not ready");
45762
+ }
45763
+ const topicId = options?.topicId ?? this._persisted?.defaultTopicId;
45764
+ const attachMeta = await this._uploadAttachment(filePath, this._primaryConversationId);
45765
+ const envelope = JSON.stringify({
45766
+ type: "message",
45767
+ text: plaintext,
45768
+ topicId,
45769
+ attachment: attachMeta
45770
+ });
45771
+ this._appendHistory("agent", plaintext, topicId);
45772
+ const messageGroupId = randomUUID();
45773
+ for (const [convId, session] of this._sessions) {
45774
+ if (!session.activated) continue;
45775
+ const encrypted = session.ratchet.encrypt(envelope);
45776
+ const transport = encryptedMessageToTransport(encrypted);
45777
+ this._ws.send(
45778
+ JSON.stringify({
45779
+ event: "message",
45780
+ data: {
45781
+ conversation_id: convId,
45782
+ header_blob: transport.header_blob,
45783
+ ciphertext: transport.ciphertext,
45784
+ message_group_id: messageGroupId,
45785
+ topic_id: topicId
45786
+ }
45787
+ })
45788
+ );
45789
+ }
45790
+ await this._persistState();
45791
+ }
45571
45792
  /**
45572
45793
  * Relay an owner's message to all sibling sessions as encrypted sync messages.
45573
45794
  * This allows all owner devices to see messages from any single device.