botschat 0.1.4 → 0.1.7
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/README.md +64 -24
- package/migrations/0011_e2e_encryption.sql +35 -0
- package/package.json +7 -2
- package/packages/api/package.json +2 -1
- package/packages/api/src/do/connection-do.ts +162 -42
- package/packages/api/src/index.ts +132 -13
- package/packages/api/src/routes/auth.ts +127 -30
- package/packages/api/src/routes/pairing.ts +14 -1
- package/packages/api/src/routes/setup.ts +72 -24
- package/packages/api/src/routes/upload.ts +12 -8
- package/packages/api/src/utils/auth.ts +212 -43
- package/packages/api/src/utils/id.ts +30 -14
- package/packages/api/src/utils/rate-limit.ts +73 -0
- package/packages/plugin/dist/src/accounts.d.ts.map +1 -1
- package/packages/plugin/dist/src/accounts.js +1 -0
- package/packages/plugin/dist/src/accounts.js.map +1 -1
- package/packages/plugin/dist/src/channel.d.ts +1 -0
- package/packages/plugin/dist/src/channel.d.ts.map +1 -1
- package/packages/plugin/dist/src/channel.js +151 -9
- package/packages/plugin/dist/src/channel.js.map +1 -1
- package/packages/plugin/dist/src/types.d.ts +16 -0
- package/packages/plugin/dist/src/types.d.ts.map +1 -1
- package/packages/plugin/dist/src/ws-client.d.ts +2 -0
- package/packages/plugin/dist/src/ws-client.d.ts.map +1 -1
- package/packages/plugin/dist/src/ws-client.js +14 -3
- package/packages/plugin/dist/src/ws-client.js.map +1 -1
- package/packages/plugin/package.json +4 -3
- package/packages/web/dist/architecture.png +0 -0
- package/packages/web/dist/assets/index-BoNQoJjQ.js +1497 -0
- package/packages/web/dist/assets/{index-DuGeoFJT.css → index-ewBIratI.css} +1 -1
- package/packages/web/dist/botschat-icon.svg +4 -0
- package/packages/web/dist/index.html +23 -3
- package/packages/web/dist/manifest.json +24 -0
- package/packages/web/dist/sw.js +40 -0
- package/packages/web/index.html +21 -1
- package/packages/web/package.json +1 -0
- package/packages/web/src/App.tsx +286 -103
- package/packages/web/src/analytics.ts +57 -0
- package/packages/web/src/api.ts +67 -3
- package/packages/web/src/components/ChatWindow.tsx +11 -11
- package/packages/web/src/components/ConnectionSettings.tsx +477 -0
- package/packages/web/src/components/CronDetail.tsx +475 -235
- package/packages/web/src/components/CronSidebar.tsx +1 -1
- package/packages/web/src/components/DebugLogPanel.tsx +116 -3
- package/packages/web/src/components/E2ESettings.tsx +122 -0
- package/packages/web/src/components/IconRail.tsx +56 -27
- package/packages/web/src/components/JobList.tsx +2 -6
- package/packages/web/src/components/LoginPage.tsx +143 -104
- package/packages/web/src/components/MobileLayout.tsx +480 -0
- package/packages/web/src/components/OnboardingPage.tsx +159 -21
- package/packages/web/src/components/ResizeHandle.tsx +34 -0
- package/packages/web/src/components/Sidebar.tsx +1 -1
- package/packages/web/src/components/TaskBar.tsx +2 -2
- package/packages/web/src/components/ThreadPanel.tsx +2 -5
- package/packages/web/src/e2e.ts +133 -0
- package/packages/web/src/hooks/useIsMobile.ts +27 -0
- package/packages/web/src/index.css +59 -0
- package/packages/web/src/main.tsx +12 -0
- package/packages/web/src/store.ts +16 -8
- package/packages/web/src/ws.ts +78 -4
- package/scripts/dev.sh +16 -16
- package/scripts/test-e2e-live.ts +194 -0
- package/scripts/verify-e2e-db.ts +48 -0
- package/scripts/verify-e2e.ts +56 -0
- package/wrangler.toml +3 -1
- package/packages/web/dist/assets/index-DyzTR_Y4.js +0 -847
package/packages/web/src/ws.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** WebSocket client for connecting to the BotsChat ConnectionDO. */
|
|
2
2
|
|
|
3
3
|
import { dlog } from "./debug-log";
|
|
4
|
+
import { E2eService } from "./e2e";
|
|
4
5
|
|
|
5
6
|
export type WSMessage = {
|
|
6
7
|
type: string;
|
|
@@ -31,6 +32,8 @@ export class BotsChatWSClient {
|
|
|
31
32
|
connect(): void {
|
|
32
33
|
this.intentionalClose = false;
|
|
33
34
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
35
|
+
// Token is NOT included in the URL to avoid leaking it in logs/history.
|
|
36
|
+
// Authentication is handled via the "auth" message after connection.
|
|
34
37
|
const url = `${protocol}//${window.location.host}/api/ws/${this.opts.userId}/${this.opts.sessionId}`;
|
|
35
38
|
|
|
36
39
|
dlog.info("WS", `Connecting to ${url}`);
|
|
@@ -42,19 +45,75 @@ export class BotsChatWSClient {
|
|
|
42
45
|
this.ws!.send(JSON.stringify({ type: "auth", token: this.opts.token }));
|
|
43
46
|
};
|
|
44
47
|
|
|
45
|
-
this.ws.onmessage = (evt) => {
|
|
48
|
+
this.ws.onmessage = async (evt) => {
|
|
46
49
|
try {
|
|
47
50
|
const msg = JSON.parse(evt.data) as WSMessage;
|
|
51
|
+
|
|
52
|
+
// Handle E2E Decryption
|
|
53
|
+
if (msg.encrypted && E2eService.hasKey()) {
|
|
54
|
+
try {
|
|
55
|
+
if (msg.type === "agent.text" || msg.type === "agent.media") {
|
|
56
|
+
// Decrypt text/caption
|
|
57
|
+
const text = msg.text as string | undefined;
|
|
58
|
+
const caption = msg.caption as string | undefined;
|
|
59
|
+
const messageId = msg.messageId as string;
|
|
60
|
+
|
|
61
|
+
if (text && messageId) {
|
|
62
|
+
msg.text = await E2eService.decrypt(text, messageId);
|
|
63
|
+
msg.encrypted = false;
|
|
64
|
+
}
|
|
65
|
+
if (caption && messageId) {
|
|
66
|
+
msg.caption = await E2eService.decrypt(caption, messageId);
|
|
67
|
+
msg.encrypted = false;
|
|
68
|
+
}
|
|
69
|
+
} else if (msg.type === "job.update") {
|
|
70
|
+
const summary = msg.summary as string;
|
|
71
|
+
// Job ID is contextId
|
|
72
|
+
const jobId = msg.jobId as string;
|
|
73
|
+
if (summary && jobId) {
|
|
74
|
+
msg.summary = await E2eService.decrypt(summary, jobId);
|
|
75
|
+
msg.encrypted = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
dlog.warn("E2E", "Decryption failed", err);
|
|
80
|
+
msg.decryptionError = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Handle Task Scan Results (array items)
|
|
85
|
+
if (msg.type === "task.scan.result" && Array.isArray(msg.tasks) && E2eService.hasKey()) {
|
|
86
|
+
for (const t of msg.tasks) {
|
|
87
|
+
if (t.encrypted && t.iv) {
|
|
88
|
+
try {
|
|
89
|
+
if (t.schedule) t.schedule = await E2eService.decrypt(t.schedule, t.iv);
|
|
90
|
+
if (t.instructions) t.instructions = await E2eService.decrypt(t.instructions, t.iv);
|
|
91
|
+
t.encrypted = false;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
dlog.warn("E2E", `Task decryption failed for ${t.cronJobId}`, err);
|
|
94
|
+
t.decryptionError = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
48
100
|
if (msg.type === "auth.ok") {
|
|
49
101
|
dlog.info("WS", "Auth OK — connected");
|
|
102
|
+
|
|
103
|
+
// Try to load E2E password
|
|
104
|
+
const userId = msg.userId as string;
|
|
105
|
+
if (userId) {
|
|
106
|
+
await E2eService.loadSavedPassword(userId);
|
|
107
|
+
}
|
|
108
|
+
|
|
50
109
|
this.backoffMs = 1000;
|
|
51
110
|
this._connected = true;
|
|
52
111
|
this.opts.onStatusChange(true);
|
|
53
112
|
} else {
|
|
54
113
|
this.opts.onMessage(msg);
|
|
55
114
|
}
|
|
56
|
-
} catch {
|
|
57
|
-
dlog.warn("WS", "Failed to
|
|
115
|
+
} catch (err) {
|
|
116
|
+
dlog.warn("WS", "Failed to process incoming message", err);
|
|
58
117
|
}
|
|
59
118
|
};
|
|
60
119
|
|
|
@@ -77,8 +136,23 @@ export class BotsChatWSClient {
|
|
|
77
136
|
};
|
|
78
137
|
}
|
|
79
138
|
|
|
80
|
-
send(msg: WSMessage): void {
|
|
139
|
+
async send(msg: WSMessage): Promise<void> {
|
|
81
140
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
141
|
+
// E2E Encryption for user messages
|
|
142
|
+
if (msg.type === "user.message" && E2eService.hasKey() && typeof msg.text === "string") {
|
|
143
|
+
try {
|
|
144
|
+
const { ciphertext, messageId } = await E2eService.encrypt(msg.text);
|
|
145
|
+
msg.text = ciphertext;
|
|
146
|
+
msg.messageId = messageId;
|
|
147
|
+
msg.encrypted = true;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
dlog.error("E2E", "Encryption failed", err);
|
|
150
|
+
// Fail? or send as plaintext?
|
|
151
|
+
// Security first: if key exists but encrypt fails, abort.
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
82
156
|
this.ws.send(JSON.stringify(msg));
|
|
83
157
|
} else {
|
|
84
158
|
dlog.warn("WS", `Cannot send — socket not open (readyState=${this.ws?.readyState})`, msg);
|
package/scripts/dev.sh
CHANGED
|
@@ -54,41 +54,41 @@ do_build_web() {
|
|
|
54
54
|
do_start() {
|
|
55
55
|
kill_port 8787
|
|
56
56
|
info "Starting wrangler dev on 0.0.0.0:8787…"
|
|
57
|
-
exec npx wrangler dev --config wrangler.toml --ip 0.0.0.0
|
|
57
|
+
exec npx wrangler dev --config wrangler.toml --ip 0.0.0.0 --var ENVIRONMENT:development
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
do_sync_plugin() {
|
|
61
|
-
local
|
|
61
|
+
local REMOTE_USER="mini.local"
|
|
62
62
|
local REMOTE_DIR="~/Projects/botsChat/packages/plugin"
|
|
63
63
|
|
|
64
|
-
info "Syncing plugin to
|
|
64
|
+
info "Syncing plugin to mini.local…"
|
|
65
65
|
rsync -avz --exclude node_modules --exclude .git --exclude dist --exclude .wrangler \
|
|
66
|
-
packages/plugin/ "$
|
|
66
|
+
packages/plugin/ "$REMOTE_USER:$REMOTE_DIR/"
|
|
67
67
|
ok "Plugin files synced"
|
|
68
68
|
|
|
69
|
-
info "Building plugin
|
|
70
|
-
ssh "$
|
|
71
|
-
export PATH="/opt/homebrew/bin:$PATH"
|
|
69
|
+
info "Building plugin, deploying to extensions, restarting gateway on mini.local…"
|
|
70
|
+
ssh "$REMOTE_USER" 'export PATH="/opt/homebrew/bin:$PATH"
|
|
72
71
|
cd ~/Projects/botsChat/packages/plugin
|
|
73
72
|
npm run build
|
|
74
|
-
|
|
73
|
+
EXT_DIR=~/.openclaw/extensions/botschat
|
|
74
|
+
rsync -av --delete dist/ "$EXT_DIR/dist/"
|
|
75
|
+
rsync -av bin/ "$EXT_DIR/bin/" 2>/dev/null || true
|
|
76
|
+
cp -f package.json openclaw.plugin.json "$EXT_DIR/" 2>/dev/null || true
|
|
77
|
+
echo "--- Deployed to $EXT_DIR ---"
|
|
75
78
|
pkill -9 -f openclaw-gateway 2>/dev/null || true
|
|
76
79
|
sleep 3
|
|
77
|
-
nohup openclaw gateway run --bind loopback --port 18789 --force
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
REMOTE_SCRIPT
|
|
81
|
-
)"
|
|
82
|
-
ok "Plugin synced and gateway restarted"
|
|
80
|
+
nohup openclaw gateway run --bind loopback --port 18789 --force > /tmp/openclaw-gateway.log 2>&1 &
|
|
81
|
+
echo "Gateway restarted (PID=$!)"'
|
|
82
|
+
ok "Plugin synced, deployed to extensions, gateway restarted"
|
|
83
83
|
|
|
84
84
|
sleep 4
|
|
85
85
|
info "Checking connection…"
|
|
86
|
-
ssh "$
|
|
86
|
+
ssh "$REMOTE_USER" 'tail -5 /tmp/openclaw-gateway.log | grep -i "authenticated\|error\|Task scan"'
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
do_logs() {
|
|
90
90
|
info "Tailing gateway logs on mini.local…"
|
|
91
|
-
ssh
|
|
91
|
+
ssh mini.local 'tail -f /tmp/openclaw-gateway.log'
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
# ── Main ─────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live E2E encryption integration test.
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* 1. Web sends encrypted user.message via WS → plugin decrypts → agent responds → encrypted agent.text → Web decrypts
|
|
6
|
+
* 2. D1 stores ciphertext (encrypted=1), not plaintext
|
|
7
|
+
*
|
|
8
|
+
* Prerequisites:
|
|
9
|
+
* - wrangler dev running on localhost:8787 (ENVIRONMENT=development)
|
|
10
|
+
* - mini.local gateway running with e2ePassword set
|
|
11
|
+
* - Test user tong@mini.local exists
|
|
12
|
+
*
|
|
13
|
+
* Usage: npx tsx scripts/test-e2e-live.ts
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import WebSocket from "ws";
|
|
17
|
+
import { deriveKey, encryptText, decryptText, toBase64, fromBase64 } from "../packages/e2e-crypto/e2e-crypto.js";
|
|
18
|
+
import { execSync } from "node:child_process";
|
|
19
|
+
import assert from "node:assert";
|
|
20
|
+
|
|
21
|
+
const API_BASE = "http://localhost:8787";
|
|
22
|
+
const E2E_PASSWORD = "e2e-test-2026";
|
|
23
|
+
const TEST_EMAIL = "tong@mini.local";
|
|
24
|
+
const TEST_PASS = "botschat123";
|
|
25
|
+
const SECRET_TEXT = `E2E_TEST_SECRET_${Date.now()}`;
|
|
26
|
+
|
|
27
|
+
async function login(): Promise<{ token: string; userId: string }> {
|
|
28
|
+
const res = await fetch(`${API_BASE}/api/auth/login`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
body: JSON.stringify({ email: TEST_EMAIL, password: TEST_PASS }),
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) throw new Error(`Login failed: ${res.status}`);
|
|
34
|
+
const data = await res.json() as { token: string; id: string };
|
|
35
|
+
return { token: data.token, userId: data.id };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getFirstSessionKey(token: string): Promise<{ channelId: string; sessionKey: string }> {
|
|
39
|
+
const res = await fetch(`${API_BASE}/api/channels`, {
|
|
40
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
41
|
+
});
|
|
42
|
+
let { channels } = await res.json() as { channels: Array<{ id: string }> };
|
|
43
|
+
if (!channels.length) {
|
|
44
|
+
// Create a channel
|
|
45
|
+
const cr = await fetch(`${API_BASE}/api/channels`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
48
|
+
body: JSON.stringify({ name: "E2E Test", description: "E2E encryption test channel" }),
|
|
49
|
+
});
|
|
50
|
+
const ch = await cr.json() as { id: string };
|
|
51
|
+
channels = [ch];
|
|
52
|
+
}
|
|
53
|
+
const channelId = channels[0].id;
|
|
54
|
+
|
|
55
|
+
const res2 = await fetch(`${API_BASE}/api/channels/${channelId}/sessions`, {
|
|
56
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
57
|
+
});
|
|
58
|
+
let { sessions } = await res2.json() as { sessions: Array<{ sessionKey: string }> };
|
|
59
|
+
if (!sessions.length) {
|
|
60
|
+
// Create a session
|
|
61
|
+
const sr = await fetch(`${API_BASE}/api/channels/${channelId}/sessions`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
64
|
+
body: JSON.stringify({ name: "E2E Test Session" }),
|
|
65
|
+
});
|
|
66
|
+
const s = await sr.json() as { sessionKey: string };
|
|
67
|
+
sessions = [s];
|
|
68
|
+
}
|
|
69
|
+
return { channelId, sessionKey: sessions[0].sessionKey };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function connectWS(userId: string, token: string): Promise<WebSocket> {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const ws = new WebSocket(`ws://localhost:8787/api/ws/${userId}/live-test`);
|
|
75
|
+
ws.on("open", () => {
|
|
76
|
+
ws.send(JSON.stringify({ type: "auth", token }));
|
|
77
|
+
});
|
|
78
|
+
ws.on("message", (data) => {
|
|
79
|
+
const msg = JSON.parse(data.toString());
|
|
80
|
+
if (msg.type === "auth.ok") resolve(ws);
|
|
81
|
+
if (msg.type === "auth.fail") reject(new Error("WS auth failed"));
|
|
82
|
+
});
|
|
83
|
+
ws.on("error", reject);
|
|
84
|
+
setTimeout(() => reject(new Error("WS connect timeout")), 10000);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function waitForAgentReply(ws: WebSocket, timeoutMs = 60000): Promise<Record<string, unknown>> {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const timer = setTimeout(() => reject(new Error("Agent reply timeout")), timeoutMs);
|
|
91
|
+
const handler = (data: WebSocket.Data) => {
|
|
92
|
+
const msg = JSON.parse(data.toString()) as Record<string, unknown>;
|
|
93
|
+
if (msg.type === "agent.text") {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
ws.off("message", handler);
|
|
96
|
+
resolve(msg);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
ws.on("message", handler);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function d1Query(sql: string): string {
|
|
104
|
+
return execSync(
|
|
105
|
+
`npx wrangler d1 execute botschat-db --local --command "${sql.replace(/"/g, '\\"')}"`,
|
|
106
|
+
{ encoding: "utf8", cwd: process.cwd() },
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function run() {
|
|
111
|
+
console.log("=== E2E Live Integration Test ===\n");
|
|
112
|
+
|
|
113
|
+
// Step 1: Login
|
|
114
|
+
console.log("1. Logging in...");
|
|
115
|
+
const { token, userId } = await login();
|
|
116
|
+
console.log(` userId: ${userId}`);
|
|
117
|
+
|
|
118
|
+
// Step 2: Derive E2E key (same as plugin)
|
|
119
|
+
console.log("2. Deriving E2E key...");
|
|
120
|
+
const key = await deriveKey(E2E_PASSWORD, userId);
|
|
121
|
+
console.log(` Key derived (${key.length} bytes)`);
|
|
122
|
+
|
|
123
|
+
// Step 3: Get session
|
|
124
|
+
console.log("3. Getting session key...");
|
|
125
|
+
const { sessionKey } = await getFirstSessionKey(token);
|
|
126
|
+
console.log(` sessionKey: ${sessionKey}`);
|
|
127
|
+
|
|
128
|
+
// Step 4: Connect WS
|
|
129
|
+
console.log("4. Connecting WebSocket...");
|
|
130
|
+
const ws = connectWS(userId, token);
|
|
131
|
+
const wsConn = await ws;
|
|
132
|
+
console.log(" Connected and authenticated");
|
|
133
|
+
|
|
134
|
+
// Step 5: Send encrypted message
|
|
135
|
+
console.log(`5. Sending encrypted message: "${SECRET_TEXT}"`);
|
|
136
|
+
const messageId = `e2e-test-${Date.now()}`;
|
|
137
|
+
const ciphertext = await encryptText(key, SECRET_TEXT, messageId);
|
|
138
|
+
const ciphertextB64 = toBase64(ciphertext);
|
|
139
|
+
|
|
140
|
+
wsConn.send(JSON.stringify({
|
|
141
|
+
type: "user.message",
|
|
142
|
+
sessionKey,
|
|
143
|
+
text: ciphertextB64,
|
|
144
|
+
messageId,
|
|
145
|
+
encrypted: true,
|
|
146
|
+
}));
|
|
147
|
+
console.log(` Sent (ciphertext length=${ciphertextB64.length}, messageId=${messageId})`);
|
|
148
|
+
|
|
149
|
+
// Step 6: Wait for agent reply
|
|
150
|
+
console.log("6. Waiting for agent reply (may take up to 60s)...");
|
|
151
|
+
const agentMsg = await waitForAgentReply(wsConn);
|
|
152
|
+
console.log(` Got agent.text reply (encrypted=${agentMsg.encrypted})`);
|
|
153
|
+
|
|
154
|
+
if (agentMsg.encrypted && agentMsg.messageId) {
|
|
155
|
+
const agentCiphertext = fromBase64(agentMsg.text as string);
|
|
156
|
+
const agentPlain = await decryptText(key, agentCiphertext, agentMsg.messageId as string);
|
|
157
|
+
console.log(` ✅ Decrypted agent reply: "${agentPlain.slice(0, 100)}..."`);
|
|
158
|
+
} else {
|
|
159
|
+
console.log(` ⚠️ Agent reply was NOT encrypted (encrypted=${agentMsg.encrypted})`);
|
|
160
|
+
console.log(` Text: "${(agentMsg.text as string || "").slice(0, 100)}..."`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Step 7: Check D1 for encrypted storage
|
|
164
|
+
console.log("7. Checking D1 for encrypted messages...");
|
|
165
|
+
// Allow a small delay for persistence
|
|
166
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
167
|
+
|
|
168
|
+
const result = d1Query(`SELECT id, text, encrypted FROM messages WHERE user_id = '${userId}' ORDER BY created_at DESC LIMIT 5`);
|
|
169
|
+
console.log(" D1 query result:");
|
|
170
|
+
console.log(result);
|
|
171
|
+
|
|
172
|
+
// Verify: the secret text should NOT appear as plaintext in D1
|
|
173
|
+
if (result.includes(SECRET_TEXT)) {
|
|
174
|
+
console.error(" ❌ FAIL: Plaintext found in D1! E2E storage is broken.");
|
|
175
|
+
process.exit(1);
|
|
176
|
+
} else {
|
|
177
|
+
console.log(" ✅ Plaintext NOT found in D1 — ciphertext stored correctly");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check that at least one message has encrypted=1
|
|
181
|
+
if (result.includes("encrypted: 1") || result.includes('"encrypted":1') || result.includes("encrypted\n1") || result.includes("| 1")) {
|
|
182
|
+
console.log(" ✅ Found encrypted=1 rows in D1");
|
|
183
|
+
} else {
|
|
184
|
+
console.log(" ⚠️ Could not confirm encrypted=1 in output (check manually)");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
wsConn.close();
|
|
188
|
+
console.log("\n🎉 E2E Live Integration Test Complete!");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
run().catch((err) => {
|
|
192
|
+
console.error("❌ Test failed:", err);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify E2E DB schema: messages and jobs have BLOB columns and encrypted flag.
|
|
3
|
+
* Requires: npm run db:migrate (local D1) first.
|
|
4
|
+
* Run: npx tsx scripts/verify-e2e-db.ts
|
|
5
|
+
*
|
|
6
|
+
* For full DB content test (encrypted=1, content is BLOB not plaintext),
|
|
7
|
+
* run dev server, send an E2E-encrypted message via WS/API, then query D1.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
function wranglerD1Sql(sql: string): string {
|
|
13
|
+
return execSync(
|
|
14
|
+
`npx wrangler d1 execute botschat-db --local --command "${sql.replace(/"/g, '\\"')}"`,
|
|
15
|
+
{ encoding: "utf8" }
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function run() {
|
|
20
|
+
console.log("E2E DB schema verification (local D1)...\n");
|
|
21
|
+
|
|
22
|
+
// Check messages table: must have encrypted column and BLOB-capable columns
|
|
23
|
+
const msgSchema = wranglerD1Sql("PRAGMA table_info(messages);");
|
|
24
|
+
if (!msgSchema.includes("encrypted")) {
|
|
25
|
+
throw new Error("messages table missing 'encrypted' column. Run: npm run db:migrate");
|
|
26
|
+
}
|
|
27
|
+
if (!msgSchema.includes("text") || !msgSchema.includes("a2ui")) {
|
|
28
|
+
throw new Error("messages table missing text/a2ui columns");
|
|
29
|
+
}
|
|
30
|
+
console.log(" ✅ messages table has encrypted column");
|
|
31
|
+
|
|
32
|
+
// Check jobs table
|
|
33
|
+
const jobsSchema = wranglerD1Sql("PRAGMA table_info(jobs);");
|
|
34
|
+
if (!jobsSchema.includes("encrypted")) {
|
|
35
|
+
throw new Error("jobs table missing 'encrypted' column. Run: npm run db:migrate");
|
|
36
|
+
}
|
|
37
|
+
if (!jobsSchema.includes("summary")) {
|
|
38
|
+
throw new Error("jobs table missing summary column");
|
|
39
|
+
}
|
|
40
|
+
console.log(" ✅ jobs table has encrypted column");
|
|
41
|
+
|
|
42
|
+
console.log("\n🎉 E2E DB schema OK. For content verification, send an E2E message then inspect D1.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
run().catch((err) => {
|
|
46
|
+
console.error("❌", err.message);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { deriveKey, encryptText, decryptText, toBase64, fromBase64 } from "../packages/e2e-crypto/e2e-crypto";
|
|
2
|
+
import assert from "assert";
|
|
3
|
+
|
|
4
|
+
async function run() {
|
|
5
|
+
console.log("Starting E2E Encryption Verification...");
|
|
6
|
+
|
|
7
|
+
const password = "my-secret-password";
|
|
8
|
+
const userId = "user-123";
|
|
9
|
+
|
|
10
|
+
// 1. Key Derivation (Simulate Plugin & Web)
|
|
11
|
+
console.log("Testing Key Derivation...");
|
|
12
|
+
const key1 = await deriveKey(password, userId);
|
|
13
|
+
const key2 = await deriveKey(password, userId);
|
|
14
|
+
|
|
15
|
+
// Check keys byte-equal
|
|
16
|
+
assert.strictEqual(toBase64(new Uint8Array(key1)), toBase64(new Uint8Array(key2)), "Keys should be deterministic");
|
|
17
|
+
console.log("✅ Key Derivation Successful (consistent)");
|
|
18
|
+
|
|
19
|
+
// 2. Encrypt (Simulate Agent -> User)
|
|
20
|
+
console.log("Testing Encryption (Agent -> User)...");
|
|
21
|
+
const plaintext = "Hello Secret World";
|
|
22
|
+
const messageId = "msg-123-uuid"; // Context ID
|
|
23
|
+
|
|
24
|
+
const encryptedBytes = await encryptText(key1, plaintext, messageId);
|
|
25
|
+
const ciphertextBase64 = toBase64(encryptedBytes);
|
|
26
|
+
console.log("Ciphertext (Base64):", ciphertextBase64);
|
|
27
|
+
|
|
28
|
+
assert.notEqual(ciphertextBase64, plaintext, "Ciphertext should not match plaintext");
|
|
29
|
+
console.log("✅ Encryption Successful");
|
|
30
|
+
|
|
31
|
+
// 3. Decrypt (Simulate User -> Agent)
|
|
32
|
+
console.log("Testing Decryption (User -> Agent)...");
|
|
33
|
+
const decryptedText = await decryptText(key2, fromBase64(ciphertextBase64), messageId);
|
|
34
|
+
console.log("Decrypted:", decryptedText);
|
|
35
|
+
assert.strictEqual(decryptedText, plaintext, "Decrypted text MUST match original");
|
|
36
|
+
console.log("✅ Decryption Successful");
|
|
37
|
+
|
|
38
|
+
// 4. Test Task Encryption (Random IV flow)
|
|
39
|
+
console.log("Testing Task Encryption (Random IV)...");
|
|
40
|
+
const ivStr = "random-uuid-iv";
|
|
41
|
+
const schedule = "0 * * * *";
|
|
42
|
+
const encScheduleBytes = await encryptText(key1, schedule, ivStr);
|
|
43
|
+
const encScheduleBase64 = toBase64(encScheduleBytes);
|
|
44
|
+
|
|
45
|
+
const originalScheduleText = await decryptText(key2, fromBase64(encScheduleBase64), ivStr);
|
|
46
|
+
|
|
47
|
+
assert.strictEqual(originalScheduleText, schedule);
|
|
48
|
+
console.log("✅ Task Encryption/Decryption Successful");
|
|
49
|
+
|
|
50
|
+
console.log("🎉 All Checks Passed!");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
run().catch(err => {
|
|
54
|
+
console.error("❌ Verification Failed:", err);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
package/wrangler.toml
CHANGED
|
@@ -33,9 +33,11 @@ new_sqlite_classes = ["ConnectionDO"]
|
|
|
33
33
|
|
|
34
34
|
# --- Environment Variables ---
|
|
35
35
|
[vars]
|
|
36
|
-
ENVIRONMENT = "
|
|
36
|
+
ENVIRONMENT = "production"
|
|
37
37
|
# Firebase project ID for Google/GitHub Sign-In token verification.
|
|
38
38
|
FIREBASE_PROJECT_ID = "botschat-130ff"
|
|
39
|
+
# IMPORTANT: For production, set JWT_SECRET via `wrangler secret put JWT_SECRET`.
|
|
40
|
+
# The app will refuse to start in non-development mode without it.
|
|
39
41
|
|
|
40
42
|
# --- Dev settings ---
|
|
41
43
|
[dev]
|