bloby-bot 0.53.9 → 0.53.10
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/package.json
CHANGED
|
@@ -197,7 +197,7 @@ function BlobyApp() {
|
|
|
197
197
|
const reg = await navigator.serviceWorker.ready;
|
|
198
198
|
const subscription = await reg.pushManager.getSubscription();
|
|
199
199
|
if (subscription) {
|
|
200
|
-
const res = await
|
|
200
|
+
const res = await authFetch(`/api/push/status?endpoint=${encodeURIComponent(subscription.endpoint)}`);
|
|
201
201
|
const data = await res.json();
|
|
202
202
|
setPushState(data.subscribed ? 'subscribed' : 'unsubscribed');
|
|
203
203
|
} else {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState, useRef } from 'react';
|
|
2
2
|
import type { WsClient } from '../lib/ws-client';
|
|
3
|
+
import { authFetch } from '../lib/auth';
|
|
3
4
|
|
|
4
5
|
export interface StoredAttachment {
|
|
5
6
|
type: string;
|
|
@@ -45,7 +46,7 @@ export function useChat(ws: WsClient | null) {
|
|
|
45
46
|
useEffect(() => {
|
|
46
47
|
if (loaded.current) return;
|
|
47
48
|
loaded.current = true;
|
|
48
|
-
|
|
49
|
+
authFetch('/api/context/current')
|
|
49
50
|
.then((r) => r.json())
|
|
50
51
|
.then((data) => {
|
|
51
52
|
if (data.conversationId) {
|
|
@@ -58,7 +59,7 @@ export function useChat(ws: WsClient | null) {
|
|
|
58
59
|
// Load messages when conversationId is set
|
|
59
60
|
useEffect(() => {
|
|
60
61
|
if (!conversationId) return;
|
|
61
|
-
|
|
62
|
+
authFetch(`/api/conversations/${conversationId}`)
|
|
62
63
|
.then((r) => {
|
|
63
64
|
if (!r.ok) throw new Error('not found');
|
|
64
65
|
return r.json();
|
|
@@ -105,7 +106,7 @@ export function useChat(ws: WsClient | null) {
|
|
|
105
106
|
.catch(() => {
|
|
106
107
|
// Conversation gone — clear
|
|
107
108
|
setConversationId(null);
|
|
108
|
-
|
|
109
|
+
authFetch('/api/context/clear', { method: 'POST' }).catch(() => {});
|
|
109
110
|
});
|
|
110
111
|
}, [conversationId]);
|
|
111
112
|
|
|
@@ -114,7 +115,7 @@ export function useChat(ws: WsClient | null) {
|
|
|
114
115
|
useEffect(() => {
|
|
115
116
|
if (conversationId && conversationId !== prevConvId.current) {
|
|
116
117
|
prevConvId.current = conversationId;
|
|
117
|
-
|
|
118
|
+
authFetch('/api/context/set', {
|
|
118
119
|
method: 'POST',
|
|
119
120
|
headers: { 'Content-Type': 'application/json' },
|
|
120
121
|
body: JSON.stringify({ conversationId }),
|
|
@@ -274,7 +275,7 @@ export function useChat(ws: WsClient | null) {
|
|
|
274
275
|
setTools([]);
|
|
275
276
|
prevConvId.current = null;
|
|
276
277
|
loaded.current = false;
|
|
277
|
-
|
|
278
|
+
authFetch('/api/context/clear', { method: 'POST' }).catch(() => {});
|
|
278
279
|
}, []);
|
|
279
280
|
|
|
280
281
|
return { messages, streaming, streamBuffer, conversationId, tools, sendMessage, stopStreaming, clearContext };
|
package/supervisor/index.ts
CHANGED
|
@@ -406,10 +406,21 @@ export async function startSupervisor() {
|
|
|
406
406
|
// The request handler is set up later via server.on('request')
|
|
407
407
|
const server = http.createServer();
|
|
408
408
|
|
|
409
|
-
// Start Vite dev server — pass supervisor server so Vite attaches HMR WebSocket directly
|
|
409
|
+
// Start Vite dev server — pass supervisor server so Vite attaches HMR WebSocket directly.
|
|
410
|
+
// A Vite boot failure must NOT take down the supervisor (G1): chat is served independently and
|
|
411
|
+
// is the lifeline the user needs to ask the agent to fix things. On failure we fall back to a
|
|
412
|
+
// sentinel port — the dashboard proxy then serves the branded "Reconnecting" page (which polls
|
|
413
|
+
// and carries the chat widget) while chat, the worker API, channels, and the tunnel all still
|
|
414
|
+
// come up. Previously this throw reached the top-level catch → process.exit before chat listened.
|
|
410
415
|
console.log('[supervisor] Starting Vite dev server...');
|
|
411
|
-
|
|
412
|
-
|
|
416
|
+
let vitePorts: { dashboard: number };
|
|
417
|
+
try {
|
|
418
|
+
vitePorts = await startViteDevServers(config.port, server);
|
|
419
|
+
console.log(`[supervisor] Vite ready — dashboard :${vitePorts.dashboard}`);
|
|
420
|
+
} catch (err) {
|
|
421
|
+
log.error(`Vite dev server failed to start — dashboard degraded, chat still available: ${err instanceof Error ? err.message : err}`);
|
|
422
|
+
vitePorts = { dashboard: -1 }; // sentinel → dashboard proxy serves RECOVERING_HTML
|
|
423
|
+
}
|
|
413
424
|
console.log(`[supervisor] Upgrade listeners on server: ${server.listenerCount('upgrade')}`);
|
|
414
425
|
|
|
415
426
|
// Ensure file storage dirs exist
|
|
@@ -504,36 +515,31 @@ export async function startSupervisor() {
|
|
|
504
515
|
}
|
|
505
516
|
}
|
|
506
517
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
518
|
+
// SECURITY MODEL — public allowlist (secure by default). When a portal password is set, EVERY
|
|
519
|
+
// /api/* route requires a valid Bearer token EXCEPT the ones listed here (the pre-login surface:
|
|
520
|
+
// login/onboarding/health/non-secret config). A new /api route is therefore GATED by default —
|
|
521
|
+
// it can only leak if someone explicitly adds it here. (Previously the gate skipped ALL GET/HEAD,
|
|
522
|
+
// so every data read — conversations, context, wallet, devices — was readable with no token.)
|
|
523
|
+
// Note: /api/agent/* (agent secret) and /api/channels/* are intercepted + returned BEFORE this
|
|
524
|
+
// gate, so they're unaffected; the channel entries below are belt-and-suspenders.
|
|
525
|
+
const PUBLIC_PRELOGIN_ROUTES = [
|
|
513
526
|
'GET /api/health',
|
|
514
|
-
|
|
515
|
-
//
|
|
516
|
-
// afterward. Re-onboard from the dashboard uses the internal x-internal WS path.
|
|
527
|
+
'GET /api/onboard/status',
|
|
528
|
+
'GET /api/settings', // secrets already stripped (worker denylist); widget + onboard read flags pre-login
|
|
517
529
|
'GET /api/push/vapid-public-key',
|
|
518
|
-
'GET /api/
|
|
519
|
-
'POST /api/
|
|
520
|
-
'
|
|
521
|
-
'
|
|
522
|
-
'POST /api/auth/codex/start',
|
|
523
|
-
'POST /api/auth/codex/cancel',
|
|
524
|
-
'GET /api/auth/codex/status',
|
|
525
|
-
'GET /api/auth/pi/providers',
|
|
526
|
-
'GET /api/auth/pi/status',
|
|
527
|
-
'POST /api/auth/pi/test',
|
|
528
|
-
'POST /api/auth/pi/save',
|
|
529
|
-
'DELETE /api/auth/pi',
|
|
530
|
-
'POST /api/auth/pi/completion',
|
|
531
|
-
'POST /api/portal/totp/setup',
|
|
532
|
-
'POST /api/portal/totp/verify-setup',
|
|
533
|
-
'POST /api/portal/totp/disable',
|
|
534
|
-
'GET /api/portal/totp/status',
|
|
530
|
+
'GET /api/portal/login',
|
|
531
|
+
'POST /api/portal/login',
|
|
532
|
+
'GET /api/portal/validate-token',
|
|
533
|
+
'POST /api/portal/validate-token',
|
|
535
534
|
'GET /api/portal/login/totp',
|
|
536
|
-
'
|
|
535
|
+
'GET /api/portal/totp/status',
|
|
536
|
+
'POST /api/portal/totp/setup', // self-protected in-handler (Bearer OR password OR first-run)
|
|
537
|
+
'POST /api/portal/totp/verify-setup', // self-protected in-handler
|
|
538
|
+
'POST /api/portal/totp/disable', // self-protected (requires password + valid code)
|
|
539
|
+
'POST /api/portal/verify-password', // verifies the password itself — cannot require a token
|
|
540
|
+
// NOTE: 'POST /api/onboard' is NOT public — gated below: open only on genuine first run
|
|
541
|
+
// (no portal_pass yet), token-required afterward. Dashboard re-onboard uses the x-internal WS path.
|
|
542
|
+
// Channel onboarding (also intercepted earlier; kept for completeness/safety):
|
|
537
543
|
'GET /api/channels/status',
|
|
538
544
|
'GET /api/channels/whatsapp/qr',
|
|
539
545
|
'GET /api/channels/whatsapp/qr-page',
|
|
@@ -546,12 +552,23 @@ export async function startSupervisor() {
|
|
|
546
552
|
'POST /api/channels/send',
|
|
547
553
|
'POST /api/channels/alexa/handle',
|
|
548
554
|
];
|
|
555
|
+
// Method-specific public PREFIXES — onboarding namespaces with sub-paths / params that carry no
|
|
556
|
+
// private chat data: provider OAuth setup/status (all of /api/auth/*), and handle availability
|
|
557
|
+
// (GET /api/handle/* only — handle register/change are POSTs and stay gated).
|
|
558
|
+
const PUBLIC_PRELOGIN_PREFIXES = [
|
|
559
|
+
'POST /api/auth/',
|
|
560
|
+
'GET /api/auth/',
|
|
561
|
+
'DELETE /api/auth/',
|
|
562
|
+
'GET /api/handle/',
|
|
563
|
+
];
|
|
549
564
|
|
|
550
|
-
function
|
|
565
|
+
function isPublicRoute(method: string, url: string): boolean {
|
|
566
|
+
const m = method === 'HEAD' ? 'GET' : method; // a HEAD to a public GET route is public
|
|
551
567
|
const path = url.split('?')[0];
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
568
|
+
if (PUBLIC_PRELOGIN_ROUTES.includes(`${m} ${path}`)) return true;
|
|
569
|
+
return PUBLIC_PRELOGIN_PREFIXES.some((r) => {
|
|
570
|
+
const sp = r.indexOf(' ');
|
|
571
|
+
return m === r.slice(0, sp) && path.startsWith(r.slice(sp + 1));
|
|
555
572
|
});
|
|
556
573
|
}
|
|
557
574
|
|
|
@@ -1728,9 +1745,11 @@ mint();
|
|
|
1728
1745
|
const isInternal = req.headers['x-internal'] === internalSecret;
|
|
1729
1746
|
|
|
1730
1747
|
if (!isInternal) {
|
|
1731
|
-
//
|
|
1748
|
+
// Require a token for EVERY /api route except the public pre-login allowlist. This now
|
|
1749
|
+
// covers GET data reads (conversations, context, wallet, devices, push status) that
|
|
1750
|
+
// previously skipped auth and leaked over the public relay.
|
|
1732
1751
|
const method = req.method || 'GET';
|
|
1733
|
-
if (
|
|
1752
|
+
if (!isPublicRoute(method, req.url || '')) {
|
|
1734
1753
|
// POST /api/onboard is open only on genuine first run (no portal_pass yet). Read the
|
|
1735
1754
|
// setting DIRECTLY rather than the 30s-cached isAuthRequired() so the gate closes the
|
|
1736
1755
|
// instant onboarding sets a password — no stale-cache window for a takeover. The
|
|
@@ -1848,6 +1867,14 @@ mint();
|
|
|
1848
1867
|
return;
|
|
1849
1868
|
}
|
|
1850
1869
|
|
|
1870
|
+
// Vite failed to boot (sentinel port) → serve the recovering page directly instead of
|
|
1871
|
+
// proxying to a dead port. Chat (/bloby/*) is served earlier, so the lifeline stays up.
|
|
1872
|
+
if (vitePorts.dashboard < 0) {
|
|
1873
|
+
res.writeHead(503, { 'Content-Type': 'text/html', 'Cache-Control': 'no-store, no-cache, must-revalidate' });
|
|
1874
|
+
res.end(RECOVERING_HTML);
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1851
1878
|
// Everything else → proxy to dashboard Vite dev server
|
|
1852
1879
|
console.log(`[supervisor] → dashboard Vite :${vitePorts.dashboard} | ${req.method} ${(req.url || '').split('?')[0]}`);
|
|
1853
1880
|
const GUARD_TAG = '<script defer src="/bloby/workspace-guard.js"></script>';
|
|
@@ -2788,14 +2815,36 @@ mint();
|
|
|
2788
2815
|
}, 1000);
|
|
2789
2816
|
}
|
|
2790
2817
|
|
|
2791
|
-
//
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2818
|
+
// Self-healing file watchers. Two failure modes the audit flagged, both of which would hurt G3
|
|
2819
|
+
// (auto-heal) or G1 (chat): (a) fs.watch throws synchronously if its target is missing — at boot
|
|
2820
|
+
// that reached the top-level catch → process.exit before chat listened; (b) a watcher 'error'
|
|
2821
|
+
// event (EMFILE under load, the watched inode removed during a workspace swap) has no listener,
|
|
2822
|
+
// so it crashes the supervisor AND leaves a silently-dead watcher (auto-heal stops with no
|
|
2823
|
+
// signal). Fix: ensure the dir exists, attach an 'error' listener, and re-arm with backoff.
|
|
2824
|
+
let backendWatcher: fs.FSWatcher | null = null;
|
|
2825
|
+
let workspaceWatcher: fs.FSWatcher | null = null;
|
|
2826
|
+
|
|
2827
|
+
function armBackendWatcher() {
|
|
2828
|
+
try {
|
|
2829
|
+
fs.mkdirSync(backendDir, { recursive: true }); // fs.watch throws if the target is missing
|
|
2830
|
+
const w = fs.watch(backendDir, { recursive: true }, (_event, filename) => {
|
|
2831
|
+
if (!filename || !filename.toString().match(/\.(ts|js|json)$/)) return;
|
|
2832
|
+
scheduleBackendRestart(`Backend file changed: ${filename}`);
|
|
2833
|
+
});
|
|
2834
|
+
w.on('error', (err: any) => {
|
|
2835
|
+
log.warn(`[watcher] backend watcher error: ${err?.message || err} — re-arming in 2s`);
|
|
2836
|
+
try { w.close(); } catch {}
|
|
2837
|
+
backendWatcher = null;
|
|
2838
|
+
setTimeout(armBackendWatcher, 2000);
|
|
2839
|
+
});
|
|
2840
|
+
backendWatcher = w;
|
|
2841
|
+
} catch (err: any) {
|
|
2842
|
+
log.warn(`[watcher] backend watcher failed to arm: ${err?.message || err} — retry in 5s`);
|
|
2843
|
+
setTimeout(armBackendWatcher, 5000);
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2796
2846
|
|
|
2797
|
-
|
|
2798
|
-
const workspaceWatcher = fs.watch(workspaceDir, (_event, filename) => {
|
|
2847
|
+
function onWorkspaceChange(_event: fs.WatchEventType, filename: string | Buffer | null) {
|
|
2799
2848
|
if (!filename) return;
|
|
2800
2849
|
if (filename === '.env') {
|
|
2801
2850
|
scheduleBackendRestart('.env changed');
|
|
@@ -2832,7 +2881,26 @@ mint();
|
|
|
2832
2881
|
runDeferredUpdate();
|
|
2833
2882
|
}
|
|
2834
2883
|
}
|
|
2835
|
-
}
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
function armWorkspaceWatcher() {
|
|
2887
|
+
try {
|
|
2888
|
+
const w = fs.watch(workspaceDir, onWorkspaceChange);
|
|
2889
|
+
w.on('error', (err: any) => {
|
|
2890
|
+
log.warn(`[watcher] workspace watcher error: ${err?.message || err} — re-arming in 2s`);
|
|
2891
|
+
try { w.close(); } catch {}
|
|
2892
|
+
workspaceWatcher = null;
|
|
2893
|
+
setTimeout(armWorkspaceWatcher, 2000);
|
|
2894
|
+
});
|
|
2895
|
+
workspaceWatcher = w;
|
|
2896
|
+
} catch (err: any) {
|
|
2897
|
+
log.warn(`[watcher] workspace watcher failed to arm: ${err?.message || err} — retry in 5s`);
|
|
2898
|
+
setTimeout(armWorkspaceWatcher, 5000);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
armBackendWatcher();
|
|
2903
|
+
armWorkspaceWatcher();
|
|
2836
2904
|
|
|
2837
2905
|
// Tunnel
|
|
2838
2906
|
let tunnelUrl: string | null = null;
|
|
@@ -2994,8 +3062,8 @@ mint();
|
|
|
2994
3062
|
log.info('Shutting down...');
|
|
2995
3063
|
await channelManager.disconnectAll();
|
|
2996
3064
|
stopScheduler();
|
|
2997
|
-
backendWatcher
|
|
2998
|
-
workspaceWatcher
|
|
3065
|
+
backendWatcher?.close();
|
|
3066
|
+
workspaceWatcher?.close();
|
|
2999
3067
|
if (backendRestartTimer) clearTimeout(backendRestartTimer);
|
|
3000
3068
|
if (watchdogInterval) clearInterval(watchdogInterval);
|
|
3001
3069
|
stopHeartbeat();
|