Files
Shade/packages/shade-proto/tests/stream-wire.test.ts
Sterister 037f994572
Some checks failed
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Test / test (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
release(v4.11.0): streaming Double-Ratchet sub-sessions (ShadeStream)
Answers Vyvern FR shade-ws-streaming-ratchet.md with a first-class
streaming-session API rather than the documented-contract fallback.
The Double-Ratchet crypto was already safe for high-frequency
one-directional use; the send/receive wrapper was not (per-frame
saveSession keystore write; shared per-peer mutex + single stored
session row coupling reuse to the HTTP path).

- @shade/core: stream.ts — identity-bound 3-DH seeding (X3DH-minus-
  prekeys, no prekey-server round trip, mutually authenticated against
  the parent session's pinned identities), bootstrapStreamSession
  reusing init{Sender,Receiver}Session verbatim, in-memory-only
  StreamRatchet (own op-mutex, never persisted, zeroized on close).
  beginStream/acceptStream on ShadeSessionManager; Stream{Closed,
  Handshake}Error; stream.opened/closed events.
- @shade/proto: STREAM_OPEN/OPEN_ACK/FRAME wire (0x31/0x32/0x33),
  additive; inspectEnvelopeType extended.
- @shade/sdk: Shade.openStream/acceptStream → ShadeStream
  (handshakeFrame/handleHandshake/seal/open/close), transport-
  agnostic, independent of encrypt/decrypt queues + parent session,
  identical server (sqlite:) and browser (IndexedDB) — touches no
  storage.
- Tests: 5000-frame one-directional burst (bounded skipped keys + FS
  zeroize), parent-session independence, replay/rewind rejection,
  mutual-auth, proto wire round-trips. Full suite green (1159 pass).
- docs/streaming-sessions.md (R1–R7 contract); SECURITY.md matrix rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:29:09 +02:00

72 lines
2.5 KiB
TypeScript

import { describe, test, expect } from 'bun:test';
import {
encodeStreamOpen,
encodeStreamOpenAck,
decodeStreamHandshake,
encodeStreamFrame,
decodeStreamFrame,
inspectEnvelopeType,
} from '../src/index.js';
import type { RatchetMessage } from '@shade/core';
function randBytes(n: number): Uint8Array {
const buf = new Uint8Array(n);
crypto.getRandomValues(buf);
return buf;
}
function makeRatchetMessage(): RatchetMessage {
return {
dhPublicKey: randBytes(32),
previousCounter: 3,
counter: 9001,
ciphertext: randBytes(128),
nonce: randBytes(12),
};
}
describe('Stream sub-session wire (V4.11)', () => {
test('STREAM_OPEN round-trips and inspects', () => {
const sid = randBytes(16);
const eph = randBytes(32);
const bytes = encodeStreamOpen(sid, eph);
expect(inspectEnvelopeType(bytes)).toBe('stream-open');
const hs = decodeStreamHandshake(bytes);
expect(hs.kind).toBe('open');
expect(hs.streamId).toEqual(sid);
expect(hs.ephemeralPub).toEqual(eph);
});
test('STREAM_OPEN_ACK round-trips and inspects', () => {
const sid = randBytes(16);
const eph = randBytes(32);
const bytes = encodeStreamOpenAck(sid, eph);
expect(inspectEnvelopeType(bytes)).toBe('stream-open-ack');
const hs = decodeStreamHandshake(bytes);
expect(hs.kind).toBe('open-ack');
expect(hs.streamId).toEqual(sid);
expect(hs.ephemeralPub).toEqual(eph);
});
test('STREAM_FRAME carries a full ratchet message verbatim', () => {
const sid = randBytes(16);
const msg = makeRatchetMessage();
const bytes = encodeStreamFrame(sid, msg);
expect(inspectEnvelopeType(bytes)).toBe('stream-frame');
const decoded = decodeStreamFrame(bytes);
expect(decoded.streamId).toEqual(sid);
expect(decoded.message.dhPublicKey).toEqual(msg.dhPublicKey);
expect(decoded.message.previousCounter).toBe(msg.previousCounter);
expect(decoded.message.counter).toBe(msg.counter);
expect(decoded.message.ciphertext).toEqual(msg.ciphertext);
expect(decoded.message.nonce).toEqual(msg.nonce);
});
test('rejects wrong sizes and wrong type tags', () => {
expect(() => encodeStreamOpen(randBytes(15), randBytes(32))).toThrow();
expect(() => encodeStreamOpen(randBytes(16), randBytes(31))).toThrow();
expect(() => decodeStreamHandshake(encodeStreamFrame(randBytes(16), makeRatchetMessage()))).toThrow();
expect(() => decodeStreamFrame(encodeStreamOpen(randBytes(16), randBytes(32)))).toThrow();
});
});