botschat 0.1.12 → 0.1.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.
Files changed (37) hide show
  1. package/README.md +11 -15
  2. package/migrations/0012_push_tokens.sql +11 -0
  3. package/package.json +7 -1
  4. package/packages/api/src/do/connection-do.ts +90 -1
  5. package/packages/api/src/env.ts +2 -0
  6. package/packages/api/src/index.ts +4 -1
  7. package/packages/api/src/routes/auth.ts +39 -6
  8. package/packages/api/src/routes/push.ts +52 -0
  9. package/packages/api/src/utils/fcm.ts +167 -0
  10. package/packages/api/src/utils/firebase.ts +89 -1
  11. package/packages/plugin/package.json +1 -1
  12. package/packages/web/dist/assets/index-B9qN5gs6.js +1 -0
  13. package/packages/web/dist/assets/index-BQNMGVyU.js +2 -0
  14. package/packages/web/dist/assets/{index-CCBhODDo.css → index-Bd_RDcgO.css} +1 -1
  15. package/packages/web/dist/assets/{index-CCFgKLX_.js → index-Civeg2lm.js} +1 -1
  16. package/packages/web/dist/assets/index-Dk33VSnY.js +2 -0
  17. package/packages/web/dist/assets/index-Kr85Nj_-.js +1516 -0
  18. package/packages/web/dist/assets/{index-Dx64BDkP.js → index-lVB82JKU.js} +1 -1
  19. package/packages/web/dist/assets/index.esm-CtMkqqqb.js +599 -0
  20. package/packages/web/dist/assets/{web-DJQW-VLX.js → web-CUXjh_UA.js} +1 -1
  21. package/packages/web/dist/assets/web-vKLTVUul.js +1 -0
  22. package/packages/web/dist/index.html +6 -4
  23. package/packages/web/dist/sw.js +158 -1
  24. package/packages/web/index.html +4 -2
  25. package/packages/web/src/App.tsx +42 -2
  26. package/packages/web/src/api.ts +10 -0
  27. package/packages/web/src/components/AccountSettings.tsx +131 -0
  28. package/packages/web/src/components/DataConsentModal.tsx +249 -0
  29. package/packages/web/src/components/LoginPage.tsx +49 -9
  30. package/packages/web/src/firebase.ts +89 -2
  31. package/packages/web/src/foreground.ts +51 -0
  32. package/packages/web/src/main.tsx +2 -1
  33. package/packages/web/src/push.ts +205 -0
  34. package/scripts/dev.sh +139 -13
  35. package/scripts/mock-openclaw.mjs +382 -0
  36. package/packages/web/dist/assets/index-D8mBAwjS.js +0 -1516
  37. package/packages/web/dist/assets/index-E-nzPZl8.js +0 -2
@@ -11,7 +11,8 @@ initAnalytics();
11
11
  if (Capacitor.isNativePlatform()) {
12
12
  // Configure status bar and keyboard for native app
13
13
  import("@capacitor/status-bar").then(({ StatusBar, Style }) => {
14
- StatusBar.setStyle({ style: Style.Dark }).catch(() => {});
14
+ const isDark = document.documentElement.getAttribute("data-theme") !== "light";
15
+ StatusBar.setStyle({ style: isDark ? Style.Dark : Style.Light }).catch(() => {});
15
16
  StatusBar.setOverlaysWebView({ overlay: true }).catch(() => {});
16
17
  });
17
18
  import("@capacitor/keyboard").then(({ Keyboard }) => {
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Push notification initialization for Web (FCM) and Native (Capacitor).
3
+ *
4
+ * - Web: Firebase Cloud Messaging + Service Worker
5
+ * - iOS/Android: @capacitor/push-notifications
6
+ *
7
+ * For E2E encrypted messages, the E2E key is synced to IndexedDB so the
8
+ * Service Worker (web) or native handler can decrypt before showing.
9
+ */
10
+
11
+ import { Capacitor } from "@capacitor/core";
12
+ import { pushApi } from "./api";
13
+ import { dlog } from "./debug-log";
14
+ import { E2eService } from "./e2e";
15
+
16
+ let initialized = false;
17
+
18
+ // ---- IndexedDB helpers for SW E2E key sync ----
19
+
20
+ const IDB_NAME = "botschat-sw";
21
+ const IDB_STORE = "keys";
22
+ const IDB_KEY = "e2e_key";
23
+
24
+ function openDB(): Promise<IDBDatabase> {
25
+ return new Promise((resolve, reject) => {
26
+ const req = indexedDB.open(IDB_NAME, 1);
27
+ req.onupgradeneeded = () => {
28
+ req.result.createObjectStore(IDB_STORE);
29
+ };
30
+ req.onsuccess = () => resolve(req.result);
31
+ req.onerror = () => reject(req.error);
32
+ });
33
+ }
34
+
35
+ /** Sync the current E2E key to IndexedDB so the Service Worker can decrypt. */
36
+ export async function syncE2eKeyToSW(): Promise<void> {
37
+ try {
38
+ const db = await openDB();
39
+ const tx = db.transaction(IDB_STORE, "readwrite");
40
+ const store = tx.objectStore(IDB_STORE);
41
+
42
+ // Read the cached key from localStorage (base64-encoded Uint8Array)
43
+ const cachedKeyB64 = localStorage.getItem("botschat_e2e_key_cache");
44
+ if (cachedKeyB64) {
45
+ // Decode base64 to Uint8Array and store in IDB
46
+ const binary = atob(cachedKeyB64);
47
+ const key = new Uint8Array(binary.length);
48
+ for (let i = 0; i < binary.length; i++) {
49
+ key[i] = binary.charCodeAt(i);
50
+ }
51
+ store.put(key, IDB_KEY);
52
+ } else {
53
+ store.delete(IDB_KEY);
54
+ }
55
+
56
+ await new Promise<void>((resolve, reject) => {
57
+ tx.oncomplete = () => resolve();
58
+ tx.onerror = () => reject(tx.error);
59
+ });
60
+ } catch (err) {
61
+ dlog.warn("Push", "Failed to sync E2E key to SW IndexedDB", err);
62
+ }
63
+ }
64
+
65
+ /** Clear the E2E key from SW IndexedDB (call on logout or key clear). */
66
+ export async function clearE2eKeyFromSW(): Promise<void> {
67
+ try {
68
+ const db = await openDB();
69
+ const tx = db.transaction(IDB_STORE, "readwrite");
70
+ tx.objectStore(IDB_STORE).delete(IDB_KEY);
71
+ await new Promise<void>((resolve, reject) => {
72
+ tx.oncomplete = () => resolve();
73
+ tx.onerror = () => reject(tx.error);
74
+ });
75
+ } catch {
76
+ // Ignore — best effort
77
+ }
78
+ }
79
+
80
+ // ---- Push initialization ----
81
+
82
+ export async function initPushNotifications(): Promise<void> {
83
+ if (initialized) return;
84
+
85
+ // Sync E2E key so push notifications can be decrypted
86
+ await syncE2eKeyToSW();
87
+
88
+ // Subscribe to E2E key changes to keep SW in sync
89
+ E2eService.subscribe(() => {
90
+ syncE2eKeyToSW().catch(() => {});
91
+ });
92
+
93
+ if (Capacitor.isNativePlatform()) {
94
+ await initNativePush();
95
+ } else {
96
+ await initWebPush();
97
+ }
98
+
99
+ initialized = true;
100
+ }
101
+
102
+ // ---- Web Push (Firebase Cloud Messaging) ----
103
+
104
+ async function initWebPush(): Promise<void> {
105
+ try {
106
+ if (!("Notification" in self)) {
107
+ dlog.warn("Push", "Notifications not supported in this browser");
108
+ return;
109
+ }
110
+
111
+ const permission = await Notification.requestPermission();
112
+ if (permission !== "granted") {
113
+ dlog.warn("Push", "Notification permission denied");
114
+ return;
115
+ }
116
+
117
+ const { getMessaging, getToken, onMessage } = await import("firebase/messaging");
118
+ const { ensureFirebaseApp } = await import("./firebase");
119
+
120
+ const firebaseApp = ensureFirebaseApp();
121
+ if (!firebaseApp) {
122
+ dlog.warn("Push", "Firebase not configured (missing env vars)");
123
+ return;
124
+ }
125
+
126
+ const messaging = getMessaging(firebaseApp);
127
+
128
+ // Get service worker registration
129
+ const registration = await navigator.serviceWorker.getRegistration();
130
+ if (!registration) {
131
+ dlog.warn("Push", "No service worker registration found");
132
+ return;
133
+ }
134
+
135
+ const vapidKey = import.meta.env.VITE_FIREBASE_VAPID_KEY as string;
136
+ if (!vapidKey) {
137
+ dlog.warn("Push", "VITE_FIREBASE_VAPID_KEY not set — skipping web push");
138
+ return;
139
+ }
140
+
141
+ const fcmToken = await getToken(messaging, {
142
+ vapidKey,
143
+ serviceWorkerRegistration: registration,
144
+ });
145
+
146
+ if (fcmToken) {
147
+ dlog.info("Push", `FCM token obtained (${fcmToken.slice(0, 20)}...)`);
148
+ await pushApi.register(fcmToken, "web");
149
+ dlog.info("Push", "Token registered with backend");
150
+ }
151
+
152
+ // Suppress foreground notifications (WS already delivers the message)
153
+ onMessage(messaging, (_payload) => {
154
+ dlog.info("Push", "Foreground FCM message received (suppressed)");
155
+ });
156
+ } catch (err) {
157
+ dlog.error("Push", "Web push init failed", err);
158
+ }
159
+ }
160
+
161
+ // ---- Native Push (Capacitor) ----
162
+
163
+ async function initNativePush(): Promise<void> {
164
+ try {
165
+ const { PushNotifications } = await import("@capacitor/push-notifications");
166
+
167
+ const permResult = await PushNotifications.requestPermissions();
168
+ if (permResult.receive !== "granted") {
169
+ dlog.warn("Push", "Native push permission denied");
170
+ return;
171
+ }
172
+
173
+ await PushNotifications.register();
174
+
175
+ PushNotifications.addListener("registration", async (token) => {
176
+ dlog.info("Push", `Native push token: ${token.value.slice(0, 20)}...`);
177
+ const platform = Capacitor.getPlatform() as "ios" | "android";
178
+ await pushApi.register(token.value, platform);
179
+ dlog.info("Push", "Native token registered with backend");
180
+ });
181
+
182
+ PushNotifications.addListener("registrationError", (error) => {
183
+ dlog.error("Push", "Native push registration failed", error);
184
+ });
185
+
186
+ // Data-only messages arrive here in foreground — suppress (WS handles it)
187
+ PushNotifications.addListener("pushNotificationReceived", (_notification) => {
188
+ dlog.info("Push", "Foreground native notification (suppressed)");
189
+ });
190
+
191
+ // User tapped a notification (app was in background)
192
+ PushNotifications.addListener("pushNotificationActionPerformed", (action) => {
193
+ dlog.info("Push", "Notification tapped", action);
194
+ // TODO: navigate to specific session from action.notification.data
195
+ });
196
+ } catch (err) {
197
+ dlog.error("Push", "Native push init failed", err);
198
+ }
199
+ }
200
+
201
+ /** Unregister push token (call on logout). */
202
+ export async function unregisterPush(): Promise<void> {
203
+ initialized = false;
204
+ await clearE2eKeyFromSW();
205
+ }
package/scripts/dev.sh CHANGED
@@ -1,24 +1,44 @@
1
1
  #!/usr/bin/env bash
2
2
  # BotsChat local dev startup script
3
3
  # Usage:
4
- # ./scripts/dev.sh — build web + migrate + start server
5
- # ./scripts/dev.sh reset — nuke local DB, re-migrate, then start
4
+ # ./scripts/dev.sh — full dev env: build + migrate + server + mock AI + open browser
5
+ # ./scripts/dev.sh reset — nuke local DB, re-migrate, then start full dev env
6
+ # ./scripts/dev.sh server — only start wrangler dev server (no mock, no browser)
6
7
  # ./scripts/dev.sh migrate — only run D1 migrations (no server)
7
8
  # ./scripts/dev.sh build — only build web frontend (no server)
8
9
  # ./scripts/dev.sh sync — sync plugin to mini.local + rebuild + restart gateway
9
10
  # ./scripts/dev.sh logs — tail gateway logs on mini.local
11
+ # ./scripts/dev.sh mock — start mock OpenClaw standalone (foreground)
10
12
  set -euo pipefail
11
13
 
12
14
  cd "$(dirname "$0")/.."
13
15
  ROOT="$(pwd)"
14
16
 
17
+ # ── Auto-set DEV_AUTH_SECRET ──────────────────────────────────────────
18
+ # For local dev, any string works. Use a fixed default so new developers
19
+ # can run `./scripts/dev.sh` without setting env vars first.
20
+ if [[ -z "${DEV_AUTH_SECRET:-}" ]]; then
21
+ DEV_AUTH_SECRET="botschat-local-dev-secret"
22
+ export DEV_AUTH_SECRET
23
+ fi
24
+
15
25
  # ── Colours ──────────────────────────────────────────────────────────
16
- RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; CYAN='\033[0;36m'; NC='\033[0m'
26
+ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; CYAN='\033[0;36m'; DIM='\033[2m'; NC='\033[0m'
17
27
  info() { echo -e "${CYAN}▸${NC} $*"; }
18
28
  ok() { echo -e "${GREEN}✔${NC} $*"; }
19
29
  warn() { echo -e "${YELLOW}▲${NC} $*"; }
20
30
  fail() { echo -e "${RED}✖${NC} $*"; exit 1; }
21
31
 
32
+ # ── Process tracking ─────────────────────────────────────────────────
33
+ WRANGLER_PID=""
34
+ MOCK_PID=""
35
+
36
+ cleanup() {
37
+ [[ -n "$MOCK_PID" ]] && kill "$MOCK_PID" 2>/dev/null || true
38
+ [[ -n "$WRANGLER_PID" ]] && kill "$WRANGLER_PID" 2>/dev/null || true
39
+ wait 2>/dev/null || true
40
+ }
41
+
22
42
  # ── Helpers ──────────────────────────────────────────────────────────
23
43
 
24
44
  kill_port() {
@@ -32,6 +52,55 @@ kill_port() {
32
52
  fi
33
53
  }
34
54
 
55
+ wait_for_server() {
56
+ info "Waiting for server…"
57
+ local max=60 i=0
58
+ while ! curl -sf --max-time 1 -o /dev/null http://localhost:8787/ 2>/dev/null; do
59
+ sleep 1
60
+ i=$((i + 1))
61
+ if [[ $i -ge $max ]]; then
62
+ fail "Server didn't start within ${max}s"
63
+ fi
64
+ done
65
+ }
66
+
67
+ get_mock_token() {
68
+ local BASE_URL="http://localhost:8787"
69
+ local TOKEN_JSON AUTH_TOKEN PAT_JSON PAT
70
+
71
+ TOKEN_JSON=$(curl -sf -X POST "$BASE_URL/api/dev-auth/login" \
72
+ -H 'Content-Type: application/json' \
73
+ -d "{\"secret\":\"$DEV_AUTH_SECRET\",\"userId\":\"dev-test-user\"}" 2>&1) || {
74
+ fail "Cannot reach $BASE_URL — is the server running?"
75
+ }
76
+ AUTH_TOKEN=$(echo "$TOKEN_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])" 2>/dev/null) || {
77
+ fail "Failed to parse auth token: $TOKEN_JSON"
78
+ }
79
+
80
+ PAT_JSON=$(curl -sf -X POST "$BASE_URL/api/pairing-tokens" \
81
+ -H "Authorization: Bearer $AUTH_TOKEN" \
82
+ -H 'Content-Type: application/json' \
83
+ -d '{"label":"mock-openclaw"}' 2>&1) || {
84
+ fail "Failed to create pairing token"
85
+ }
86
+ PAT=$(echo "$PAT_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])" 2>/dev/null) || {
87
+ fail "Failed to parse pairing token: $PAT_JSON"
88
+ }
89
+
90
+ echo "$PAT"
91
+ }
92
+
93
+ open_browser() {
94
+ local url="http://localhost:8787/?dev_token=${DEV_AUTH_SECRET}"
95
+ if command -v open &>/dev/null; then
96
+ open "$url"
97
+ elif command -v xdg-open &>/dev/null; then
98
+ xdg-open "$url"
99
+ else
100
+ warn "Open in browser: $url"
101
+ fi
102
+ }
103
+
35
104
  do_migrate() {
36
105
  info "Applying D1 migrations (local)…"
37
106
  npx wrangler d1 migrations apply botschat-db --local
@@ -51,20 +120,68 @@ do_build_web() {
51
120
  ok "Web build complete (packages/web/dist)"
52
121
  }
53
122
 
123
+ # ── Server-only start (foreground, no mock/browser) ──────────────────
124
+
54
125
  do_start() {
55
126
  kill_port 8787
56
127
  info "Starting wrangler dev on 0.0.0.0:8787…"
57
- exec npx wrangler dev --config wrangler.toml --ip 0.0.0.0 --var ENVIRONMENT:development --var DEV_AUTH_SECRET:"${DEV_AUTH_SECRET:?Set DEV_AUTH_SECRET env var}"
128
+ exec npx wrangler dev --config wrangler.toml --ip 0.0.0.0 --var ENVIRONMENT:development --var DEV_AUTH_SECRET:"$DEV_AUTH_SECRET"
129
+ }
130
+
131
+ # ── Full dev environment (server + mock + browser) ───────────────────
132
+
133
+ do_start_full() {
134
+ kill_port 8787
135
+ trap cleanup EXIT INT TERM
136
+
137
+ info "Starting wrangler dev on 0.0.0.0:8787…"
138
+ npx wrangler dev --config wrangler.toml --ip 0.0.0.0 \
139
+ --var ENVIRONMENT:development \
140
+ --var DEV_AUTH_SECRET:"$DEV_AUTH_SECRET" &
141
+ WRANGLER_PID=$!
142
+
143
+ wait_for_server
144
+ ok "Server ready on http://localhost:8787"
145
+
146
+ info "Starting Mock OpenClaw…"
147
+ local PAT
148
+ PAT=$(get_mock_token)
149
+ mkdir -p "$ROOT/.wrangler"
150
+ node "$ROOT/scripts/mock-openclaw.mjs" --token "$PAT" > "$ROOT/.wrangler/mock-openclaw.log" 2>&1 &
151
+ MOCK_PID=$!
152
+ ok "Mock OpenClaw connected (pid=$MOCK_PID)"
153
+
154
+ open_browser
155
+
156
+ echo ""
157
+ echo -e "${CYAN}╭──────────────────────────────────────────────────╮${NC}"
158
+ echo -e "${CYAN}│${NC} ${GREEN}BotsChat Dev Environment Ready${NC} ${CYAN}│${NC}"
159
+ echo -e "${CYAN}│${NC} ${CYAN}│${NC}"
160
+ echo -e "${CYAN}│${NC} Server: http://localhost:8787 ${CYAN}│${NC}"
161
+ echo -e "${CYAN}│${NC} Mock AI: running ${DIM}(log: .wrangler/mock-openclaw.log)${NC}"
162
+ echo -e "${CYAN}│${NC} Auth: auto-login enabled ${CYAN}│${NC}"
163
+ echo -e "${CYAN}│${NC} ${CYAN}│${NC}"
164
+ echo -e "${CYAN}│${NC} Press ${YELLOW}Ctrl+C${NC} to stop all services ${CYAN}│${NC}"
165
+ echo -e "${CYAN}╰──────────────────────────────────────────────────╯${NC}"
166
+ echo ""
167
+
168
+ wait $WRANGLER_PID 2>/dev/null || true
169
+ }
170
+
171
+ # ── Standalone mock (foreground) ─────────────────────────────────────
172
+
173
+ do_mock() {
174
+ info "Getting auth token via dev-auth…"
175
+ local PAT
176
+ PAT=$(get_mock_token)
177
+ ok "Pairing token: ${PAT:0:16}***"
178
+ info "Starting Mock OpenClaw…"
179
+ exec node "$ROOT/scripts/mock-openclaw.mjs" --token "$PAT" "$@"
58
180
  }
59
181
 
182
+ # ── Sync plugin to mini.local ────────────────────────────────────────
183
+
60
184
  do_sync_plugin() {
61
- # ── IMPORTANT ──────────────────────────────────────────────────
62
- # Development repo and production plugin MUST be kept separate:
63
- # Dev repo: mini:~/Projects/botschat-app/botsChat/packages/plugin/
64
- # Production: mini:~/.openclaw/extensions/botschat/
65
- # NEVER edit files directly in ~/.openclaw/extensions/botschat/.
66
- # Always: edit dev repo → build → deploy artifacts to extensions.
67
- # ────────────────────────────────────────────────────────────────
68
185
  local REMOTE="mini.local"
69
186
  local DEV_DIR="~/Projects/botschat-app/botsChat/packages/plugin"
70
187
  local EXT_DIR="~/.openclaw/extensions/botschat"
@@ -105,6 +222,11 @@ case "$cmd" in
105
222
  reset)
106
223
  do_reset
107
224
  do_build_web
225
+ do_start_full
226
+ ;;
227
+ server)
228
+ do_build_web
229
+ do_migrate
108
230
  do_start
109
231
  ;;
110
232
  migrate)
@@ -119,10 +241,14 @@ case "$cmd" in
119
241
  logs)
120
242
  do_logs
121
243
  ;;
244
+ mock)
245
+ shift
246
+ do_mock "$@"
247
+ ;;
122
248
  *)
123
- # Default: build + migrate + start
249
+ # Default: full dev experience — build + migrate + server + mock + browser
124
250
  do_build_web
125
251
  do_migrate
126
- do_start
252
+ do_start_full
127
253
  ;;
128
254
  esac