/** * End-to-end PII guard test. * * Exercises real Shade entry points (session encrypt/decrypt, transfer * upload + receive, prekey HTTP routes, files RPC) with a recorder hook * and asserts that NONE of the recorded span attributes echo the * sensitive plaintext we deliberately fed in (peer address, message * content, exact byte counts). */ import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; import { ShadeSessionManager, type StorageProvider } from '@shade/core'; import { MemoryStorage, SubtleCryptoProvider } from '@shade/crypto-web'; import { createRecorder } from '../src/index.ts'; import { createPrekeyRoutes, MemoryPrekeyStore } from '@shade/server'; const DANGER_FRAGMENTS = [ // Peer addresses we feed into APIs: 'alice@danger.test', 'bob@danger.test', 'device:hot-secret-12345', // Plaintext message bodies: 'sekret-payload-XYZ', 'CLASSIFIED-7777', // Exact byte counts that we'd never want leaked: '1048577', ]; describe('observability — PII guard for ShadeSessionManager', () => { test('encrypt/decrypt spans never echo address or plaintext', async () => { const rec = createRecorder(); const crypto = new SubtleCryptoProvider(); const aliceStorage: StorageProvider = new MemoryStorage(); const bobStorage: StorageProvider = new MemoryStorage(); const alice = new ShadeSessionManager(crypto, aliceStorage, { observability: rec }); const bob = new ShadeSessionManager(crypto, bobStorage, { observability: rec }); await alice.initialize(); await bob.initialize(); // Alice -> Bob handshake (X3DH) const bobBundle = await bob.createPreKeyBundle(); await alice.initSessionFromBundle('bob@danger.test', bobBundle); const env1 = await alice.encrypt('bob@danger.test', 'sekret-payload-XYZ'); await bob.decrypt('alice@danger.test', env1); // Round-trip a second time so a steady-state ratchet step also runs. const env2 = await bob.encrypt('alice@danger.test', 'CLASSIFIED-7777'); await alice.decrypt('bob@danger.test', env2); expect(rec.spans.length).toBeGreaterThan(0); const hits = rec.scanForPII(DANGER_FRAGMENTS); if (hits.length > 0) { throw new Error(`PII leak in spans: ${JSON.stringify(hits, null, 2)}`); } }); }); describe('observability — PII guard for prekey routes', () => { let port: number; let server: ReturnType; let rec: ReturnType; beforeAll(async () => { rec = createRecorder(); const crypto = new SubtleCryptoProvider(); const store = new MemoryPrekeyStore(); const app = createPrekeyRoutes(store, crypto, { observability: rec, disableRateLimit: true, }); server = Bun.serve({ fetch: app.fetch, port: 0, }); port = (server as unknown as { port: number }).port; }); afterAll(async () => { await server.stop(); }); test('GET /v1/keys/bundle/
never logs the address verbatim', async () => { const addr = 'device:hot-secret-12345'; // Anonymous fetch — bundle endpoint will 404 since we never registered, // but the route still emits a span with the route template (not the // raw address path). await fetch(`http://localhost:${port}/v1/keys/bundle/${encodeURIComponent(addr)}`); expect(rec.spans.length).toBeGreaterThan(0); const hits = rec.scanForPII([addr, 'hot-secret']); if (hits.length > 0) { throw new Error(`PII leak in prekey-route spans: ${JSON.stringify(hits, null, 2)}`); } // The span name should reference the route TEMPLATE, not the raw path. const seenRoutes = rec.spans.flatMap((s) => { const r = s.attributes['shade.route']; return typeof r === 'string' ? [r] : []; }); // Route should be `/v1/keys/bundle/:address` (or empty if Hono didn't // resolve it; what we MUST NOT see is the literal device:... value). for (const r of seenRoutes) { expect(r.includes('hot-secret')).toBe(false); } }); });