feat(observer): M-Obs 4-7 — widgets, dashboard, docs, integration example
Some checks failed
Test / test (push) Has been cancelled
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:
110
examples/06-observer-dashboard/main.ts
Normal file
110
examples/06-observer-dashboard/main.ts
Normal 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);
|
||||
Reference in New Issue
Block a user