@agentvault/secure-channel 0.6.12 → 0.6.14

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
@@ -74,6 +74,23 @@ export declare class SecureChannel extends EventEmitter {
74
74
  * and relays as sync messages to sibling sessions.
75
75
  */
76
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>;
77
94
  /**
78
95
  * Relay an owner's message to all sibling sessions as encrypted sync messages.
79
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;AAc3C,OAAO,KAAK,EACV,mBAAmB,EACnB,YAAY,EAKb,MAAM,YAAY,CAAC;AAiDpB,qBAAa,aAAc,SAAQ,YAAY;IAyBjC,OAAO,CAAC,MAAM;IAxB1B,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;IACjD,OAAO,CAAC,WAAW,CAAuB;IAE1C,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;IA2B3B,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAmDnC,OAAO,CAAC,eAAe;IASvB;;;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
@@ -4,6 +4,9 @@
4
4
  import { EventEmitter } from "node:events";
5
5
  import { createServer } from "node:http";
6
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";
7
10
 
8
11
  // ../../node_modules/libsodium-sumo/dist/modules-sumo-esm/libsodium-sumo.mjs
9
12
  var __filename;
@@ -44920,6 +44923,39 @@ var DoubleRatchet = class _DoubleRatchet {
44920
44923
  }
44921
44924
  };
44922
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
+
44923
44959
  // src/crypto-helpers.ts
44924
44960
  function hexToBytes(hex) {
44925
44961
  return libsodium_wrappers_default.from_hex(hex);
@@ -45241,7 +45277,11 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45241
45277
  res.end(JSON.stringify({ ok: false, error: "Missing 'text' field" }));
45242
45278
  return;
45243
45279
  }
45244
- await this.send(text, { topicId: parsed.topicId });
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
+ }
45245
45285
  res.writeHead(200, { "Content-Type": "application/json" });
45246
45286
  res.end(JSON.stringify({ ok: true }));
45247
45287
  } catch (err) {
@@ -45594,10 +45634,14 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45594
45634
  }
45595
45635
  let messageText;
45596
45636
  let messageType;
45637
+ let attachmentInfo = null;
45597
45638
  try {
45598
45639
  const parsed = JSON.parse(plaintext);
45599
45640
  messageType = parsed.type || "message";
45600
45641
  messageText = parsed.text || plaintext;
45642
+ if (parsed.attachment) {
45643
+ attachmentInfo = parsed.attachment;
45644
+ }
45601
45645
  } catch {
45602
45646
  messageType = "message";
45603
45647
  messageText = plaintext;
@@ -45610,15 +45654,56 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45610
45654
  }
45611
45655
  if (messageType === "message") {
45612
45656
  const topicId = msgData.topic_id;
45657
+ let attachData;
45658
+ if (attachmentInfo) {
45659
+ try {
45660
+ const { filePath, decrypted } = await this._downloadAndDecryptAttachment(attachmentInfo);
45661
+ attachData = {
45662
+ filename: attachmentInfo.filename,
45663
+ mime: attachmentInfo.mime,
45664
+ size: decrypted.length,
45665
+ filePath
45666
+ };
45667
+ if (attachmentInfo.mime.startsWith("image/")) {
45668
+ attachData.base64 = `data:${attachmentInfo.mime};base64,${bytesToBase64(decrypted)}`;
45669
+ }
45670
+ const textMimes = ["text/", "application/json", "application/xml", "application/csv"];
45671
+ if (textMimes.some((m2) => attachmentInfo.mime.startsWith(m2))) {
45672
+ attachData.textContent = new TextDecoder().decode(decrypted);
45673
+ }
45674
+ } catch (err) {
45675
+ console.error(`[SecureChannel] Failed to download attachment:`, err);
45676
+ }
45677
+ }
45613
45678
  this._appendHistory("owner", messageText, topicId);
45679
+ let emitText = messageText;
45680
+ if (attachData) {
45681
+ if (attachData.textContent) {
45682
+ emitText = `[Attachment: ${attachData.filename} (${attachData.mime})]
45683
+ ---
45684
+ ${attachData.textContent}
45685
+ ---
45686
+
45687
+ ${messageText}`;
45688
+ } else if (attachData.base64) {
45689
+ emitText = `[Image attachment: ${attachData.filename}]
45690
+
45691
+ ${messageText}`;
45692
+ } else {
45693
+ emitText = `[Attachment: ${attachData.filename} saved to ${attachData.filePath}]
45694
+
45695
+ ${messageText}`;
45696
+ }
45697
+ }
45614
45698
  const metadata = {
45615
45699
  messageId: msgData.message_id,
45616
45700
  conversationId: convId,
45617
45701
  timestamp: msgData.created_at,
45618
- topicId
45702
+ topicId,
45703
+ attachment: attachData
45619
45704
  };
45620
- this.emit("message", messageText, metadata);
45621
- this.config.onMessage?.(messageText, metadata);
45705
+ this.emit("message", emitText, metadata);
45706
+ this.config.onMessage?.(emitText, metadata);
45622
45707
  await this._relaySyncToSiblings(convId, session.ownerDeviceId, messageText, topicId);
45623
45708
  }
45624
45709
  if (this._persisted) {
@@ -45626,6 +45711,111 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45626
45711
  }
45627
45712
  await this._persistState();
45628
45713
  }
45714
+ /**
45715
+ * Download an encrypted attachment blob, decrypt it, verify integrity,
45716
+ * and save the plaintext file to disk.
45717
+ */
45718
+ async _downloadAndDecryptAttachment(info) {
45719
+ const attachDir = join2(this.config.dataDir, "attachments");
45720
+ await mkdir2(attachDir, { recursive: true });
45721
+ const url = `${this.config.apiUrl}${info.blobUrl}`;
45722
+ const res = await fetch(url, {
45723
+ headers: { Authorization: `Bearer ${this._deviceJwt}` }
45724
+ });
45725
+ if (!res.ok) {
45726
+ throw new Error(`Attachment download failed: ${res.status}`);
45727
+ }
45728
+ const buffer = await res.arrayBuffer();
45729
+ const encryptedData = new Uint8Array(buffer);
45730
+ const digest = computeFileDigest(encryptedData);
45731
+ if (digest !== info.digest) {
45732
+ throw new Error("Attachment digest mismatch \u2014 possible tampering");
45733
+ }
45734
+ const fileKey = base64ToBytes(info.fileKey);
45735
+ const fileNonce = base64ToBytes(info.fileNonce);
45736
+ const decrypted = decryptFile(encryptedData, fileKey, fileNonce);
45737
+ const filePath = join2(attachDir, info.filename);
45738
+ await writeFile2(filePath, decrypted);
45739
+ console.log(`[SecureChannel] Attachment saved: ${filePath} (${decrypted.length} bytes)`);
45740
+ return { filePath, decrypted };
45741
+ }
45742
+ /**
45743
+ * Upload an attachment file: encrypt, upload to server, return metadata
45744
+ * for inclusion in the message envelope.
45745
+ */
45746
+ async _uploadAttachment(filePath, conversationId) {
45747
+ const data = await readFile2(filePath);
45748
+ const plainData = new Uint8Array(data);
45749
+ const result = encryptFile(plainData);
45750
+ const { Blob: NodeBlob, FormData: NodeFormData } = await import("node:buffer").then(
45751
+ () => globalThis
45752
+ );
45753
+ const formData = new FormData();
45754
+ formData.append("conversation_id", conversationId);
45755
+ formData.append(
45756
+ "file",
45757
+ new Blob([result.encryptedData.buffer], { type: "application/octet-stream" }),
45758
+ "attachment.bin"
45759
+ );
45760
+ const res = await fetch(`${this.config.apiUrl}/api/v1/attachments/upload`, {
45761
+ method: "POST",
45762
+ headers: { Authorization: `Bearer ${this._deviceJwt}` },
45763
+ body: formData
45764
+ });
45765
+ if (!res.ok) {
45766
+ const detail = await res.text();
45767
+ throw new Error(`Attachment upload failed (${res.status}): ${detail}`);
45768
+ }
45769
+ const resp = await res.json();
45770
+ const filename = filePath.split("/").pop() || "file";
45771
+ return {
45772
+ blobId: resp.blob_id,
45773
+ blobUrl: resp.blob_url,
45774
+ fileKey: bytesToBase64(result.fileKey),
45775
+ fileNonce: bytesToBase64(result.fileNonce),
45776
+ digest: result.digest,
45777
+ filename,
45778
+ mime: "application/octet-stream",
45779
+ size: plainData.length
45780
+ };
45781
+ }
45782
+ /**
45783
+ * Send a message with an attached file. Encrypts the file, uploads it,
45784
+ * then sends the envelope with attachment metadata via Double Ratchet.
45785
+ */
45786
+ async sendWithAttachment(plaintext, filePath, options) {
45787
+ if (this._state !== "ready" || this._sessions.size === 0 || !this._ws) {
45788
+ throw new Error("Channel is not ready");
45789
+ }
45790
+ const topicId = options?.topicId ?? this._persisted?.defaultTopicId;
45791
+ const attachMeta = await this._uploadAttachment(filePath, this._primaryConversationId);
45792
+ const envelope = JSON.stringify({
45793
+ type: "message",
45794
+ text: plaintext,
45795
+ topicId,
45796
+ attachment: attachMeta
45797
+ });
45798
+ this._appendHistory("agent", plaintext, topicId);
45799
+ const messageGroupId = randomUUID();
45800
+ for (const [convId, session] of this._sessions) {
45801
+ if (!session.activated) continue;
45802
+ const encrypted = session.ratchet.encrypt(envelope);
45803
+ const transport = encryptedMessageToTransport(encrypted);
45804
+ this._ws.send(
45805
+ JSON.stringify({
45806
+ event: "message",
45807
+ data: {
45808
+ conversation_id: convId,
45809
+ header_blob: transport.header_blob,
45810
+ ciphertext: transport.ciphertext,
45811
+ message_group_id: messageGroupId,
45812
+ topic_id: topicId
45813
+ }
45814
+ })
45815
+ );
45816
+ }
45817
+ await this._persistState();
45818
+ }
45629
45819
  /**
45630
45820
  * Relay an owner's message to all sibling sessions as encrypted sync messages.
45631
45821
  * This allows all owner devices to see messages from any single device.