M-Obs 1: Event bus in @shade/core - ShadeEventEmitter with typed event union, ring buffer for replay - 12 event types covering session lifecycle, ratchet operations, prekey changes, identity rotation, trust changes - Wired into ShadeSessionManager (zero overhead when not enabled) - shortHash helper for safe display of public keys - Security test: regex-checks event payloads contain no key material M-Obs 2: Prekey server event hooks - PrekeyServerEvents emitter mirroring core's pattern - 5 server event types: registered, fetched, replenished, deleted, rate_limited - Wired into all routes including the rate-limit error handler - shortHash helper using crypto.subtle directly (no provider dep) M-Obs 3: @shade/observer package - StateAggregator subscribes to client + server events, builds rolling snapshot - Hono routes: GET /api/state (snapshot), GET /api/events (SSE stream) - Bearer token auth via SHADE_OBSERVER_TOKEN, query string for SSE - Refuses to start with token < 16 chars (ConfigurationError) - Static file serving for bundled dashboard at /dashboard/ - Placeholder dashboard renders when no built SPA present 220 tests passing, 0 failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
91 lines
2.9 KiB
TypeScript
91 lines
2.9 KiB
TypeScript
import { Hono } from 'hono';
|
|
import { join } from 'path';
|
|
import { existsSync, readFileSync, statSync } from 'fs';
|
|
|
|
/**
|
|
* Serve the bundled dashboard SPA from /dashboard/.
|
|
*
|
|
* Looks for dist/ in the @shade/observer package directory.
|
|
* Falls back to a placeholder page if no build is present.
|
|
*/
|
|
export function createStaticRoutes(distDir: string): Hono {
|
|
const app = new Hono();
|
|
|
|
app.get('/dashboard', (c) => c.redirect('/dashboard/'));
|
|
|
|
app.get('/dashboard/*', async (c) => {
|
|
const url = new URL(c.req.url);
|
|
let path = url.pathname.replace(/^\/dashboard\/?/, '') || 'index.html';
|
|
|
|
// Prevent path traversal
|
|
if (path.includes('..')) {
|
|
return c.text('Forbidden', 403);
|
|
}
|
|
|
|
const fullPath = join(distDir, path);
|
|
|
|
if (!existsSync(fullPath) || !statSync(fullPath).isFile()) {
|
|
// Fall back to index.html for SPA routing
|
|
const indexPath = join(distDir, 'index.html');
|
|
if (!existsSync(indexPath)) {
|
|
return c.html(placeholderHtml());
|
|
}
|
|
const content = readFileSync(indexPath);
|
|
c.header('Content-Type', 'text/html; charset=utf-8');
|
|
return c.body(content as any);
|
|
}
|
|
|
|
const content = readFileSync(fullPath);
|
|
const ct = contentTypeFor(path);
|
|
c.header('Content-Type', ct);
|
|
if (path.endsWith('.html')) {
|
|
c.header('Cache-Control', 'no-cache');
|
|
} else {
|
|
c.header('Cache-Control', 'public, max-age=3600');
|
|
}
|
|
return c.body(content as any);
|
|
});
|
|
|
|
return app;
|
|
}
|
|
|
|
function contentTypeFor(path: string): string {
|
|
if (path.endsWith('.html')) return 'text/html; charset=utf-8';
|
|
if (path.endsWith('.js')) return 'application/javascript; charset=utf-8';
|
|
if (path.endsWith('.css')) return 'text/css; charset=utf-8';
|
|
if (path.endsWith('.json')) return 'application/json; charset=utf-8';
|
|
if (path.endsWith('.svg')) return 'image/svg+xml';
|
|
if (path.endsWith('.png')) return 'image/png';
|
|
if (path.endsWith('.woff2')) return 'font/woff2';
|
|
return 'application/octet-stream';
|
|
}
|
|
|
|
function placeholderHtml(): string {
|
|
return `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Shade Observer</title>
|
|
<style>
|
|
body { font-family: system-ui; max-width: 600px; margin: 80px auto; padding: 0 20px; color: #d4d4d4; background: #0a0a0a; }
|
|
h1 { color: #f7c948; }
|
|
code { background: #1a1a1a; padding: 2px 6px; border-radius: 4px; }
|
|
a { color: #f7c948; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Shade Observer</h1>
|
|
<p>The dashboard SPA hasn't been built yet. The observer API is running, but there's no UI bundled.</p>
|
|
<p>To build the dashboard:</p>
|
|
<pre><code>cd packages/shade-dashboard && bun run build</code></pre>
|
|
<p>Then re-run the observer.</p>
|
|
<h2>API endpoints</h2>
|
|
<ul>
|
|
<li><code>GET /api/state</code> — current snapshot (requires <code>Authorization: Bearer ...</code>)</li>
|
|
<li><code>GET /api/events</code> — SSE stream of live events</li>
|
|
<li><code>GET /health</code> — health check (no auth)</li>
|
|
</ul>
|
|
</body>
|
|
</html>`;
|
|
}
|