@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/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { SecureChannel } from "./channel.js";
2
2
  export type { SecureChannelConfig, ChannelState, MessageMetadata, PersistedState, LegacyPersistedState, DeviceSession, HistoryEntry, } from "./types.js";
3
3
  export { agentVaultPlugin, setOcRuntime, getActiveChannel } from "./openclaw-plugin.js";
4
- export declare const VERSION = "0.5.1";
4
+ export declare const VERSION = "0.6.12";
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,oBAAoB,EACpB,aAAa,EACb,YAAY,GACb,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExF,eAAO,MAAM,OAAO,UAAU,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,oBAAoB,EACpB,aAAa,EACb,YAAY,GACb,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
@@ -1,6 +1,10 @@
1
1
  // src/channel.ts
2
2
  import { EventEmitter } from "node:events";
3
+ import { createServer } from "node:http";
3
4
  import { randomUUID } from "node:crypto";
5
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
6
+ import { join as join2 } from "node:path";
7
+ import { readFile as readFile2 } from "node:fs/promises";
4
8
 
5
9
  // ../../node_modules/libsodium-sumo/dist/modules-sumo-esm/libsodium-sumo.mjs
6
10
  var __filename;
@@ -44917,6 +44921,39 @@ var DoubleRatchet = class _DoubleRatchet {
44917
44921
  }
44918
44922
  };
44919
44923
 
44924
+ // ../crypto/dist/file-crypto.js
44925
+ function encryptFile(plainData) {
44926
+ const fileKey = libsodium_wrappers_default.randombytes_buf(libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_KEYBYTES);
44927
+ const fileNonce = libsodium_wrappers_default.randombytes_buf(libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
44928
+ const encryptedData = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_encrypt(
44929
+ plainData,
44930
+ null,
44931
+ // no additional data
44932
+ null,
44933
+ // secret nonce (unused)
44934
+ fileNonce,
44935
+ fileKey
44936
+ );
44937
+ const digestBytes = libsodium_wrappers_default.crypto_generichash(32, encryptedData);
44938
+ const digest = libsodium_wrappers_default.to_hex(digestBytes);
44939
+ return { encryptedData, fileKey, fileNonce, digest };
44940
+ }
44941
+ function decryptFile(encryptedData, fileKey, fileNonce) {
44942
+ return libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(
44943
+ null,
44944
+ // secret nonce (unused)
44945
+ encryptedData,
44946
+ null,
44947
+ // no additional data
44948
+ fileNonce,
44949
+ fileKey
44950
+ );
44951
+ }
44952
+ function computeFileDigest(data) {
44953
+ const digestBytes = libsodium_wrappers_default.crypto_generichash(32, data);
44954
+ return libsodium_wrappers_default.to_hex(digestBytes);
44955
+ }
44956
+
44920
44957
  // src/crypto-helpers.ts
44921
44958
  function hexToBytes(hex) {
44922
44959
  return libsodium_wrappers_default.from_hex(hex);
@@ -45081,6 +45118,7 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45081
45118
  _ackTimer = null;
45082
45119
  _stopped = false;
45083
45120
  _persisted = null;
45121
+ _httpServer = null;
45084
45122
  static PING_INTERVAL_MS = 3e4;
45085
45123
  // Send ping every 30s
45086
45124
  static PING_TIMEOUT_MS = 1e4;
@@ -45193,6 +45231,7 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45193
45231
  this._stopped = true;
45194
45232
  this._flushAcks();
45195
45233
  this._stopPing();
45234
+ this._stopHttpServer();
45196
45235
  if (this._ackTimer) {
45197
45236
  clearTimeout(this._ackTimer);
45198
45237
  this._ackTimer = null;
@@ -45212,6 +45251,65 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45212
45251
  }
45213
45252
  this._setState("disconnected");
45214
45253
  }
45254
+ // --- Local HTTP server for proactive sends ---
45255
+ startHttpServer(port) {
45256
+ if (this._httpServer) return;
45257
+ this._httpServer = createServer(async (req, res) => {
45258
+ const remote = req.socket.remoteAddress;
45259
+ if (remote !== "127.0.0.1" && remote !== "::1" && remote !== "::ffff:127.0.0.1") {
45260
+ res.writeHead(403, { "Content-Type": "application/json" });
45261
+ res.end(JSON.stringify({ ok: false, error: "Forbidden" }));
45262
+ return;
45263
+ }
45264
+ if (req.method === "POST" && req.url === "/send") {
45265
+ let body = "";
45266
+ req.on("data", (chunk) => {
45267
+ body += chunk.toString();
45268
+ });
45269
+ req.on("end", async () => {
45270
+ try {
45271
+ const parsed = JSON.parse(body);
45272
+ const text = parsed.text;
45273
+ if (!text || typeof text !== "string") {
45274
+ res.writeHead(400, { "Content-Type": "application/json" });
45275
+ res.end(JSON.stringify({ ok: false, error: "Missing 'text' field" }));
45276
+ return;
45277
+ }
45278
+ if (parsed.file_path && typeof parsed.file_path === "string") {
45279
+ await this.sendWithAttachment(text, parsed.file_path, { topicId: parsed.topicId });
45280
+ } else {
45281
+ await this.send(text, { topicId: parsed.topicId });
45282
+ }
45283
+ res.writeHead(200, { "Content-Type": "application/json" });
45284
+ res.end(JSON.stringify({ ok: true }));
45285
+ } catch (err) {
45286
+ res.writeHead(500, { "Content-Type": "application/json" });
45287
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
45288
+ }
45289
+ });
45290
+ } else if (req.method === "GET" && req.url === "/status") {
45291
+ res.writeHead(200, { "Content-Type": "application/json" });
45292
+ res.end(JSON.stringify({
45293
+ ok: true,
45294
+ state: this._state,
45295
+ deviceId: this._deviceId,
45296
+ sessions: this._sessions.size
45297
+ }));
45298
+ } else {
45299
+ res.writeHead(404, { "Content-Type": "application/json" });
45300
+ res.end(JSON.stringify({ ok: false, error: "Not found. Use POST /send or GET /status" }));
45301
+ }
45302
+ });
45303
+ this._httpServer.listen(port, "127.0.0.1", () => {
45304
+ this.emit("http-ready", port);
45305
+ });
45306
+ }
45307
+ _stopHttpServer() {
45308
+ if (this._httpServer) {
45309
+ this._httpServer.close();
45310
+ this._httpServer = null;
45311
+ }
45312
+ }
45215
45313
  // --- Topic management ---
45216
45314
  /**
45217
45315
  * Create a new topic within the conversation group.
@@ -45534,10 +45632,14 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45534
45632
  }
45535
45633
  let messageText;
45536
45634
  let messageType;
45635
+ let attachmentInfo = null;
45537
45636
  try {
45538
45637
  const parsed = JSON.parse(plaintext);
45539
45638
  messageType = parsed.type || "message";
45540
45639
  messageText = parsed.text || plaintext;
45640
+ if (parsed.attachment) {
45641
+ attachmentInfo = parsed.attachment;
45642
+ }
45541
45643
  } catch {
45542
45644
  messageType = "message";
45543
45645
  messageText = plaintext;
@@ -45550,15 +45652,29 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45550
45652
  }
45551
45653
  if (messageType === "message") {
45552
45654
  const topicId = msgData.topic_id;
45655
+ let attachmentPath;
45656
+ if (attachmentInfo) {
45657
+ try {
45658
+ attachmentPath = await this._downloadAndDecryptAttachment(attachmentInfo);
45659
+ } catch (err) {
45660
+ console.error(`[SecureChannel] Failed to download attachment:`, err);
45661
+ }
45662
+ }
45553
45663
  this._appendHistory("owner", messageText, topicId);
45664
+ let emitText = messageText;
45665
+ if (attachmentPath) {
45666
+ emitText = `[Attachment: ${attachmentInfo.filename} saved to ${attachmentPath}]
45667
+
45668
+ ${messageText}`;
45669
+ }
45554
45670
  const metadata = {
45555
45671
  messageId: msgData.message_id,
45556
45672
  conversationId: convId,
45557
45673
  timestamp: msgData.created_at,
45558
45674
  topicId
45559
45675
  };
45560
- this.emit("message", messageText, metadata);
45561
- this.config.onMessage?.(messageText, metadata);
45676
+ this.emit("message", emitText, metadata);
45677
+ this.config.onMessage?.(emitText, metadata);
45562
45678
  await this._relaySyncToSiblings(convId, session.ownerDeviceId, messageText, topicId);
45563
45679
  }
45564
45680
  if (this._persisted) {
@@ -45566,6 +45682,111 @@ var SecureChannel = class _SecureChannel extends EventEmitter {
45566
45682
  }
45567
45683
  await this._persistState();
45568
45684
  }
45685
+ /**
45686
+ * Download an encrypted attachment blob, decrypt it, verify integrity,
45687
+ * and save the plaintext file to disk.
45688
+ */
45689
+ async _downloadAndDecryptAttachment(info) {
45690
+ const attachDir = join2(this.config.dataDir, "attachments");
45691
+ await mkdir2(attachDir, { recursive: true });
45692
+ const url = `${this.config.apiUrl}${info.blobUrl}`;
45693
+ const res = await fetch(url, {
45694
+ headers: { Authorization: `Bearer ${this._deviceJwt}` }
45695
+ });
45696
+ if (!res.ok) {
45697
+ throw new Error(`Attachment download failed: ${res.status}`);
45698
+ }
45699
+ const buffer = await res.arrayBuffer();
45700
+ const encryptedData = new Uint8Array(buffer);
45701
+ const digest = computeFileDigest(encryptedData);
45702
+ if (digest !== info.digest) {
45703
+ throw new Error("Attachment digest mismatch \u2014 possible tampering");
45704
+ }
45705
+ const fileKey = base64ToBytes(info.fileKey);
45706
+ const fileNonce = base64ToBytes(info.fileNonce);
45707
+ const decrypted = decryptFile(encryptedData, fileKey, fileNonce);
45708
+ const filePath = join2(attachDir, info.filename);
45709
+ await writeFile2(filePath, decrypted);
45710
+ console.log(`[SecureChannel] Attachment saved: ${filePath} (${decrypted.length} bytes)`);
45711
+ return filePath;
45712
+ }
45713
+ /**
45714
+ * Upload an attachment file: encrypt, upload to server, return metadata
45715
+ * for inclusion in the message envelope.
45716
+ */
45717
+ async _uploadAttachment(filePath, conversationId) {
45718
+ const data = await readFile2(filePath);
45719
+ const plainData = new Uint8Array(data);
45720
+ const result = encryptFile(plainData);
45721
+ const { Blob: NodeBlob, FormData: NodeFormData } = await import("node:buffer").then(
45722
+ () => globalThis
45723
+ );
45724
+ const formData = new FormData();
45725
+ formData.append("conversation_id", conversationId);
45726
+ formData.append(
45727
+ "file",
45728
+ new Blob([result.encryptedData.buffer], { type: "application/octet-stream" }),
45729
+ "attachment.bin"
45730
+ );
45731
+ const res = await fetch(`${this.config.apiUrl}/api/v1/attachments/upload`, {
45732
+ method: "POST",
45733
+ headers: { Authorization: `Bearer ${this._deviceJwt}` },
45734
+ body: formData
45735
+ });
45736
+ if (!res.ok) {
45737
+ const detail = await res.text();
45738
+ throw new Error(`Attachment upload failed (${res.status}): ${detail}`);
45739
+ }
45740
+ const resp = await res.json();
45741
+ const filename = filePath.split("/").pop() || "file";
45742
+ return {
45743
+ blobId: resp.blob_id,
45744
+ blobUrl: resp.blob_url,
45745
+ fileKey: bytesToBase64(result.fileKey),
45746
+ fileNonce: bytesToBase64(result.fileNonce),
45747
+ digest: result.digest,
45748
+ filename,
45749
+ mime: "application/octet-stream",
45750
+ size: plainData.length
45751
+ };
45752
+ }
45753
+ /**
45754
+ * Send a message with an attached file. Encrypts the file, uploads it,
45755
+ * then sends the envelope with attachment metadata via Double Ratchet.
45756
+ */
45757
+ async sendWithAttachment(plaintext, filePath, options) {
45758
+ if (this._state !== "ready" || this._sessions.size === 0 || !this._ws) {
45759
+ throw new Error("Channel is not ready");
45760
+ }
45761
+ const topicId = options?.topicId ?? this._persisted?.defaultTopicId;
45762
+ const attachMeta = await this._uploadAttachment(filePath, this._primaryConversationId);
45763
+ const envelope = JSON.stringify({
45764
+ type: "message",
45765
+ text: plaintext,
45766
+ topicId,
45767
+ attachment: attachMeta
45768
+ });
45769
+ this._appendHistory("agent", plaintext, topicId);
45770
+ const messageGroupId = randomUUID();
45771
+ for (const [convId, session] of this._sessions) {
45772
+ if (!session.activated) continue;
45773
+ const encrypted = session.ratchet.encrypt(envelope);
45774
+ const transport = encryptedMessageToTransport(encrypted);
45775
+ this._ws.send(
45776
+ JSON.stringify({
45777
+ event: "message",
45778
+ data: {
45779
+ conversation_id: convId,
45780
+ header_blob: transport.header_blob,
45781
+ ciphertext: transport.ciphertext,
45782
+ message_group_id: messageGroupId,
45783
+ topic_id: topicId
45784
+ }
45785
+ })
45786
+ );
45787
+ }
45788
+ await this._persistState();
45789
+ }
45569
45790
  /**
45570
45791
  * Relay an owner's message to all sibling sessions as encrypted sync messages.
45571
45792
  * This allows all owner devices to see messages from any single device.
@@ -45862,6 +46083,7 @@ var agentVaultPlugin = {
45862
46083
  dataDir: av.dataDir ?? "~/.openclaw/agentvault",
45863
46084
  apiUrl: av.apiUrl ?? "https://api.agentvault.chat",
45864
46085
  agentName: av.agentName ?? "OpenClaw Agent",
46086
+ httpPort: av.httpPort ?? 18790,
45865
46087
  configured: Boolean(av.dataDir)
45866
46088
  };
45867
46089
  }
@@ -45898,6 +46120,11 @@ var agentVaultPlugin = {
45898
46120
  }
45899
46121
  });
45900
46122
  _channels.set(account.accountId, channel);
46123
+ const httpPort = account.httpPort;
46124
+ channel.on("ready", () => {
46125
+ channel.startHttpServer(httpPort);
46126
+ log?.(`[AgentVault] HTTP send server listening on http://127.0.0.1:${httpPort}`);
46127
+ });
45901
46128
  abortSignal?.addEventListener("abort", () => {
45902
46129
  _channels.delete(account.accountId);
45903
46130
  });
@@ -45912,11 +46139,25 @@ var agentVaultPlugin = {
45912
46139
  },
45913
46140
  outbound: {
45914
46141
  deliveryMode: "direct",
46142
+ targets: [
46143
+ {
46144
+ id: "owner",
46145
+ label: "AgentVault Owner",
46146
+ accountId: "default"
46147
+ },
46148
+ {
46149
+ id: "default",
46150
+ label: "AgentVault Owner (default)",
46151
+ accountId: "default"
46152
+ }
46153
+ ],
45915
46154
  sendText: async ({
45916
46155
  text,
45917
- accountId
46156
+ accountId,
46157
+ targetId
45918
46158
  }) => {
45919
- const channel = _channels.get(accountId ?? "default");
46159
+ const resolvedId = accountId ?? (targetId === "owner" ? "default" : targetId ?? "default");
46160
+ const channel = _channels.get(resolvedId);
45920
46161
  if (!channel) {
45921
46162
  return { ok: false, error: "AgentVault channel is not connected" };
45922
46163
  }
@@ -46006,7 +46247,7 @@ async function _handleInbound(params) {
46006
46247
  }
46007
46248
 
46008
46249
  // src/index.ts
46009
- var VERSION = "0.5.1";
46250
+ var VERSION = "0.6.12";
46010
46251
  export {
46011
46252
  SecureChannel,
46012
46253
  VERSION,