feat(observer): M-Obs 4-7 — widgets, dashboard, docs, integration example
Some checks failed
Test / test (push) Has been cancelled

M-Obs 4: @shade/widgets React library
- ShadeProvider context with observer URL + token + theme
- useShadeState (polling) + useShadeEvents (SSE) hooks
- 7 widgets: IdentityCard, SessionList, PrekeyStock, RecentActivity,
  ServerStatus, FingerprintCompare, WidgetCatalog (meta-widget for
  user-selectable layout with localStorage persistence)
- Self-contained CSS via inline styles, no external CSS conflicts
- Light/dark/auto theme via tokens

M-Obs 5: @shade/dashboard standalone SPA
- Vite + React app composing all widgets into a full debugger layout
- Login screen with token persistence to localStorage
- Build script copies dist/ to @shade/observer/dist/ for embedded serving
- 211 KB JS bundle (66 KB gzipped)

M-Obs 6: Documentation + integration example
- READMEs for @shade/observer and @shade/widgets
- examples/06-observer-dashboard runnable demo: spins up prekey server +
  observer, runs Alice ↔ Bob conversation loop, dashboard at :3901
- Updated root README and docker-compose.yml with observer integration

M-Obs 7: End-to-end verification
- StateAggregator now drains buffered events on subscription, so
  identity.initialized fires before observer construction are still seen
- Verified live: snapshot endpoint returns full state, dashboard serves,
  401 without auth, sessions/messages/ratchet steps all tracked

220 tests passing, 0 failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 19:00:21 +02:00
parent b014f9b44c
commit 9ceab037ca
32 changed files with 2016 additions and 2 deletions

View File

@@ -0,0 +1,21 @@
# Example 06: Observer Dashboard
Spins up a Shade prekey server with the observer attached, runs Alice ↔ Bob conversations in a loop, and serves the dashboard at `http://localhost:3901/dashboard/`.
## Run
```bash
cd packages/shade-dashboard && bun run build # build the SPA once
cd ../../examples/06-observer-dashboard
bun run main.ts
```
Then open `http://localhost:3901/dashboard/` and enter the bearer token printed in the console.
## What you'll see
- Identity card with the demo's fingerprint
- Live session between Alice and Bob with message counters incrementing
- Recent activity feed showing every X3DH handshake, encryption, and ratchet step
- Prekey stock decreasing as Alice consumes them
- Server stats updating in real time

View File

@@ -0,0 +1,110 @@
import { Hono } from 'hono';
import {
ShadeSessionManager,
ShadeEventEmitter,
} from '../../packages/shade-core/src/index.js';
import {
SubtleCryptoProvider,
MemoryStorage,
} from '../../packages/shade-crypto-web/src/index.js';
import {
createPrekeyServer,
MemoryPrekeyStore,
PrekeyServerEvents,
} from '../../packages/shade-server/src/index.js';
import { createObserver } from '../../packages/shade-observer/src/index.js';
const crypto = new SubtleCryptoProvider();
const TOKEN = 'demo-token-must-be-at-least-16-chars';
async function main() {
console.log('━━━ Shade Observer Demo ━━━\n');
// ─── Wire up event emitters ──────────────────────────
const clientEvents = new ShadeEventEmitter();
const serverEvents = new PrekeyServerEvents();
// ─── Two demo session managers ───────────────────────
const alice = new ShadeSessionManager(crypto, new MemoryStorage(), { events: clientEvents });
const bob = new ShadeSessionManager(crypto, new MemoryStorage(), { events: clientEvents });
await alice.initialize();
await bob.initialize();
// ─── Prekey server with events ───────────────────────
const prekeyApp = createPrekeyServer({
crypto,
store: new MemoryPrekeyStore(),
disableRateLimit: true,
events: serverEvents,
});
// ─── Observer ────────────────────────────────────────
const observer = createObserver({
token: TOKEN,
clientEvents,
serverEvents,
});
// ─── Mount everything in one Hono app ────────────────
const app = new Hono();
app.route('/prekey', prekeyApp);
app.route('/', observer);
const PORT = 3901;
Bun.serve({ port: PORT, fetch: app.fetch });
console.log(`✓ Server listening on http://localhost:${PORT}`);
console.log(`✓ Dashboard: http://localhost:${PORT}/dashboard/`);
console.log(`✓ Token: ${TOKEN}\n`);
console.log('Open the dashboard, paste the token, and watch the live activity below…\n');
// ─── Establish Alice ↔ Bob session ───────────────────
await bob.generateOneTimePreKeys(20);
const bundle = await bob.createPreKeyBundle();
// Inline a one-time prekey since we're not going through the prekey server
const otpks = await bob.generateOneTimePreKeys(1);
bundle.oneTimePreKey = { keyId: otpks[0].keyId, publicKey: otpks[0].keyPair.publicKey };
await alice.initSessionFromBundle('bob', bundle);
// Initial exchange to establish bidirectional ratchet
const initEnv = await alice.encrypt('bob', 'init');
await bob.decrypt('alice', initEnv);
const initReply = await bob.encrypt('alice', 'init reply');
await alice.decrypt('bob', initReply);
// ─── Run a loop of encrypted messages ────────────────
const messages = [
"Hey Bob, can you see this?",
"Yes Alice, loud and clear.",
"Cool — every message has a fresh key.",
"And the dashboard shows it live.",
"Ratchet steps every time we switch direction.",
"Forward secrecy in action.",
];
let i = 0;
setInterval(async () => {
try {
const fromAlice = i % 2 === 0;
const sender = fromAlice ? alice : bob;
const receiver = fromAlice ? bob : alice;
const senderAddr = fromAlice ? 'bob' : 'alice';
const receiverAddr = fromAlice ? 'alice' : 'bob';
const text = messages[i % messages.length];
const env = await sender.encrypt(senderAddr, text);
await receiver.decrypt(receiverAddr, env);
console.log(` [${i + 1}] ${fromAlice ? 'Alice' : 'Bob '}: "${text}"`);
i++;
// Periodically replenish prekeys to show that activity in the dashboard
if (i % 8 === 0) {
await bob.ensurePreKeyStock(5, 20);
}
} catch (err) {
console.error('Loop error:', err);
}
}, 2000);
}
main().catch(console.error);