release(v4.0.0): Shade GA — V3.x consolidation + audit prep
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers byte-for-byte. The version bump is semantic: audit-cycle complete, opt-in surface fully exposed, threat model refreshed for every new surface. Highlights: - All 24 @shade/* packages bumped to 4.0.0 in lockstep. - CHANGELOG 4.0.0 section is the canonical manifest of what landed. - THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12 Web-Worker boundary) + residual-risks table refreshed. - OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox, bridge, observer, /metrics, /healthz, /ready. - MIGRATION 0.3.x → 4.0 documented + smoke-tested against shade migrate-storage on a real SQLite DB. - docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer. - scripts/soak.ts harness for the GA-stable 2-week soak window. - All V*.md plans archived under docs/archive/ with Status: Done. - Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen non-realtime stack. Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green. Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports version 4.0.0 on /health. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
126
packages/shade-transfer/tests/observability.test.ts
Normal file
126
packages/shade-transfer/tests/observability.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { SubtleCryptoProvider } from '@shade/crypto-web';
|
||||
import { createRecorder } from '@shade/observability';
|
||||
import {
|
||||
TransferEngine,
|
||||
MemoryControlChannel,
|
||||
MemoryTransferTransport,
|
||||
type IncomingTransfer,
|
||||
type TransferHandle,
|
||||
type TransferResult,
|
||||
} from '../src/index.js';
|
||||
|
||||
const crypto = new SubtleCryptoProvider();
|
||||
|
||||
const ALICE = 'alice@trace-test.local';
|
||||
const BOB = 'bob@trace-test.local';
|
||||
const SECRET_PAYLOAD = new TextEncoder().encode(
|
||||
'classified-shadow-token-DO-NOT-LOG',
|
||||
);
|
||||
|
||||
function makePair(observability: ReturnType<typeof createRecorder>) {
|
||||
const { a: ctrlA, b: ctrlB } = MemoryControlChannel.linked(ALICE, BOB);
|
||||
const { a: txA, b: txB } = MemoryTransferTransport.linked(ALICE, BOB);
|
||||
const sender = new TransferEngine({
|
||||
crypto,
|
||||
controlChannel: ctrlA,
|
||||
transport: txA,
|
||||
myAddress: ALICE,
|
||||
observability,
|
||||
});
|
||||
const receiver = new TransferEngine({
|
||||
crypto,
|
||||
controlChannel: ctrlB,
|
||||
transport: txB,
|
||||
myAddress: BOB,
|
||||
observability,
|
||||
});
|
||||
txB.setChunkHandler(async (from, sid, lane, seq, bytes) =>
|
||||
receiver.receiveChunk(from, sid, lane, seq, bytes),
|
||||
);
|
||||
return { sender, receiver };
|
||||
}
|
||||
|
||||
describe('TransferEngine observability', () => {
|
||||
test('emits upload+download spans with PII-safe attributes', async () => {
|
||||
const rec = createRecorder();
|
||||
const { sender, receiver } = makePair(rec);
|
||||
|
||||
let resolveRecv!: (h: TransferHandle) => void;
|
||||
const recvP = new Promise<TransferHandle>((r) => { resolveRecv = r; });
|
||||
const unsub = receiver.onIncomingTransfer(async (incoming: IncomingTransfer) => {
|
||||
const h = await incoming.accept({ output: { kind: 'buffer' } });
|
||||
resolveRecv(h);
|
||||
});
|
||||
|
||||
const handle = await sender.upload({
|
||||
to: BOB,
|
||||
input: SECRET_PAYLOAD,
|
||||
lanes: 1,
|
||||
chunkSize: 4096,
|
||||
});
|
||||
const recvH = await recvP;
|
||||
await Promise.all([handle.done(), recvH.done()]);
|
||||
unsub();
|
||||
|
||||
const upload = rec.spans.find((s) => s.name === 'shade.transfer.upload');
|
||||
const download = rec.spans.find((s) => s.name === 'shade.transfer.download');
|
||||
expect(upload).toBeDefined();
|
||||
expect(download).toBeDefined();
|
||||
|
||||
expect(upload!.attributes['shade.direction']).toBe('upload');
|
||||
expect(upload!.attributes['shade.peer.hash']).toMatch(/^[0-9a-f]{8}$/);
|
||||
expect(upload!.attributes['shade.bytes.bin']).toBe('≤4KB');
|
||||
expect(upload!.attributes['shade.lane.count']).toBe(1);
|
||||
expect(upload!.attributes['shade.result']).toBe('ok');
|
||||
expect(upload!.status).toBe('ok');
|
||||
expect(upload!.ended).toBe(true);
|
||||
|
||||
expect(download!.attributes['shade.direction']).toBe('download');
|
||||
expect(download!.attributes['shade.peer.hash']).toMatch(/^[0-9a-f]{8}$/);
|
||||
expect(download!.attributes['shade.result']).toBe('ok');
|
||||
expect(download!.ended).toBe(true);
|
||||
|
||||
// PII guard: no plaintext addresses, no payload, no exact byte counts.
|
||||
const hits = rec.scanForPII([
|
||||
ALICE,
|
||||
BOB,
|
||||
'trace-test',
|
||||
'classified-shadow-token',
|
||||
String(SECRET_PAYLOAD.length),
|
||||
]);
|
||||
if (hits.length > 0) {
|
||||
throw new Error(`PII leak: ${JSON.stringify(hits, null, 2)}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('failed upload marks span as error', async () => {
|
||||
const rec = createRecorder();
|
||||
const { sender } = makePair(rec);
|
||||
// No receiver wired, so probe will succeed but accept() never fires.
|
||||
// Force failure by aborting before the upload completes.
|
||||
const ac = new AbortController();
|
||||
const handle = await sender.upload({
|
||||
to: BOB,
|
||||
input: new Uint8Array(8),
|
||||
lanes: 1,
|
||||
chunkSize: 4,
|
||||
signal: ac.signal,
|
||||
});
|
||||
ac.abort();
|
||||
await handle.done().catch(() => undefined);
|
||||
const upload = rec.spans.find((s) => s.name === 'shade.transfer.upload');
|
||||
expect(upload).toBeDefined();
|
||||
expect(upload!.ended).toBe(true);
|
||||
// Either resulted in error or completed cleanly before abort took effect.
|
||||
// We only assert that attributes never echoed peer addresses.
|
||||
const hits = rec.scanForPII([ALICE, BOB, 'trace-test']);
|
||||
if (hits.length > 0) {
|
||||
throw new Error(`PII leak (failure path): ${JSON.stringify(hits, null, 2)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Type-level guard so TransferResult import isn't unused.
|
||||
const _t: TransferResult | null = null;
|
||||
void _t;
|
||||
Reference in New Issue
Block a user