@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 +20 -0
- package/dist/channel.d.ts.map +1 -1
- package/dist/cli.js +223 -2
- package/dist/cli.js.map +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +246 -5
- package/dist/index.js.map +4 -4
- package/dist/openclaw-entry.d.ts.map +1 -1
- package/dist/openclaw-entry.js +18 -15
- package/dist/openclaw-entry.js.map +2 -2
- package/dist/openclaw-plugin.d.ts +8 -1
- package/dist/openclaw-plugin.d.ts.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/openclaw.plugin.json +16 -1
- package/package.json +1 -1
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.
|
|
4
|
+
export declare const VERSION = "0.6.12";
|
|
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,
|
|
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",
|
|
45561
|
-
this.config.onMessage?.(
|
|
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
|
|
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.
|
|
46250
|
+
var VERSION = "0.6.12";
|
|
46010
46251
|
export {
|
|
46011
46252
|
SecureChannel,
|
|
46012
46253
|
VERSION,
|