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.
Files changed (66) hide show
  1. package/README.md +64 -24
  2. package/migrations/0011_e2e_encryption.sql +35 -0
  3. package/package.json +7 -2
  4. package/packages/api/package.json +2 -1
  5. package/packages/api/src/do/connection-do.ts +162 -42
  6. package/packages/api/src/index.ts +132 -13
  7. package/packages/api/src/routes/auth.ts +127 -30
  8. package/packages/api/src/routes/pairing.ts +14 -1
  9. package/packages/api/src/routes/setup.ts +72 -24
  10. package/packages/api/src/routes/upload.ts +12 -8
  11. package/packages/api/src/utils/auth.ts +212 -43
  12. package/packages/api/src/utils/id.ts +30 -14
  13. package/packages/api/src/utils/rate-limit.ts +73 -0
  14. package/packages/plugin/dist/src/accounts.d.ts.map +1 -1
  15. package/packages/plugin/dist/src/accounts.js +1 -0
  16. package/packages/plugin/dist/src/accounts.js.map +1 -1
  17. package/packages/plugin/dist/src/channel.d.ts +1 -0
  18. package/packages/plugin/dist/src/channel.d.ts.map +1 -1
  19. package/packages/plugin/dist/src/channel.js +151 -9
  20. package/packages/plugin/dist/src/channel.js.map +1 -1
  21. package/packages/plugin/dist/src/types.d.ts +16 -0
  22. package/packages/plugin/dist/src/types.d.ts.map +1 -1
  23. package/packages/plugin/dist/src/ws-client.d.ts +2 -0
  24. package/packages/plugin/dist/src/ws-client.d.ts.map +1 -1
  25. package/packages/plugin/dist/src/ws-client.js +14 -3
  26. package/packages/plugin/dist/src/ws-client.js.map +1 -1
  27. package/packages/plugin/package.json +4 -3
  28. package/packages/web/dist/architecture.png +0 -0
  29. package/packages/web/dist/assets/index-BoNQoJjQ.js +1497 -0
  30. package/packages/web/dist/assets/{index-DuGeoFJT.css → index-ewBIratI.css} +1 -1
  31. package/packages/web/dist/botschat-icon.svg +4 -0
  32. package/packages/web/dist/index.html +23 -3
  33. package/packages/web/dist/manifest.json +24 -0
  34. package/packages/web/dist/sw.js +40 -0
  35. package/packages/web/index.html +21 -1
  36. package/packages/web/package.json +1 -0
  37. package/packages/web/src/App.tsx +286 -103
  38. package/packages/web/src/analytics.ts +57 -0
  39. package/packages/web/src/api.ts +67 -3
  40. package/packages/web/src/components/ChatWindow.tsx +11 -11
  41. package/packages/web/src/components/ConnectionSettings.tsx +477 -0
  42. package/packages/web/src/components/CronDetail.tsx +475 -235
  43. package/packages/web/src/components/CronSidebar.tsx +1 -1
  44. package/packages/web/src/components/DebugLogPanel.tsx +116 -3
  45. package/packages/web/src/components/E2ESettings.tsx +122 -0
  46. package/packages/web/src/components/IconRail.tsx +56 -27
  47. package/packages/web/src/components/JobList.tsx +2 -6
  48. package/packages/web/src/components/LoginPage.tsx +143 -104
  49. package/packages/web/src/components/MobileLayout.tsx +480 -0
  50. package/packages/web/src/components/OnboardingPage.tsx +159 -21
  51. package/packages/web/src/components/ResizeHandle.tsx +34 -0
  52. package/packages/web/src/components/Sidebar.tsx +1 -1
  53. package/packages/web/src/components/TaskBar.tsx +2 -2
  54. package/packages/web/src/components/ThreadPanel.tsx +2 -5
  55. package/packages/web/src/e2e.ts +133 -0
  56. package/packages/web/src/hooks/useIsMobile.ts +27 -0
  57. package/packages/web/src/index.css +59 -0
  58. package/packages/web/src/main.tsx +12 -0
  59. package/packages/web/src/store.ts +16 -8
  60. package/packages/web/src/ws.ts +78 -4
  61. package/scripts/dev.sh +16 -16
  62. package/scripts/test-e2e-live.ts +194 -0
  63. package/scripts/verify-e2e-db.ts +48 -0
  64. package/scripts/verify-e2e.ts +56 -0
  65. package/wrangler.toml +3 -1
  66. package/packages/web/dist/assets/index-DyzTR_Y4.js +0 -847
@@ -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 parse incoming message", evt.data);
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 REMOTE="YinTong@mini.local"
61
+ local REMOTE_USER="mini.local"
62
62
  local REMOTE_DIR="~/Projects/botsChat/packages/plugin"
63
63
 
64
- info "Syncing plugin to $REMOTE…"
64
+ info "Syncing plugin to mini.local…"
65
65
  rsync -avz --exclude node_modules --exclude .git --exclude dist --exclude .wrangler \
66
- packages/plugin/ "$REMOTE:$REMOTE_DIR/"
66
+ packages/plugin/ "$REMOTE_USER:$REMOTE_DIR/"
67
67
  ok "Plugin files synced"
68
68
 
69
- info "Building plugin + restarting gateway on mini.local…"
70
- ssh "$REMOTE" "$(cat <<'REMOTE_SCRIPT'
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
- echo "--- Plugin build OK ---"
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
- > /tmp/openclaw-gateway.log 2>&1 &
79
- echo "Gateway restarted (PID=$!)"
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 "$REMOTE" 'tail -5 /tmp/openclaw-gateway.log | grep -i "authenticated\|error\|Task scan"'
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 YinTong@mini.local 'tail -f /tmp/openclaw-gateway.log'
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 = "development"
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]