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