Files
Shade/packages/shade-transport/src/ws-adapter.ts
Sterister d071551b2f feat: M6 Transport wrappers — fetch + WebSocket adapters
- ShadeFetchTransport: HTTP client for prekey server
  (register, fetchBundle, replenish, getKeyCount)
- ShadeWebSocket: wraps existing WebSocket with auto E2EE
  (binary wire format, transparent encrypt/decrypt)
- Full integration test: register → fetch → session → encrypt → decrypt
  over real HTTP against in-process Hono prekey server

101 tests, 0 failures across all milestones (M1-M7).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:18:21 +02:00

84 lines
2.4 KiB
TypeScript

import type { ShadeSessionManager, ShadeEnvelope, RatchetMessage } from '@shade/core';
import { encodeEnvelope, decodeEnvelope } from '@shade/proto';
/**
* ShadeWebSocket — wraps an existing WebSocket with automatic E2EE.
*
* All outgoing messages are encrypted via the Double Ratchet.
* All incoming messages are decrypted transparently.
*
* Usage:
* ```ts
* const ws = new WebSocket('wss://example.com/sync');
* const shade = new ShadeWebSocket(ws, sessionManager, 'server');
*
* shade.onMessage((plaintext) => {
* console.log('Received:', plaintext);
* });
*
* await shade.send('Hello encrypted world!');
* ```
*/
export class ShadeWebSocket {
private messageHandlers: Array<(plaintext: string) => void> = [];
private errorHandlers: Array<(error: Error) => void> = [];
constructor(
private readonly ws: WebSocket,
private readonly manager: ShadeSessionManager,
private readonly peerAddress: string,
) {
this.ws.addEventListener('message', (event) => {
this.handleIncoming(event.data).catch((err) => {
for (const handler of this.errorHandlers) handler(err);
});
});
}
/** Send an encrypted message to the peer */
async send(plaintext: string): Promise<void> {
const envelope = await this.manager.encrypt(this.peerAddress, plaintext);
const bytes = encodeEnvelope(envelope);
// Send as binary
this.ws.send(bytes);
}
/** Register a handler for decrypted incoming messages */
onMessage(handler: (plaintext: string) => void): void {
this.messageHandlers.push(handler);
}
/** Register a handler for decryption errors */
onError(handler: (error: Error) => void): void {
this.errorHandlers.push(handler);
}
/** Close the underlying WebSocket */
close(): void {
this.ws.close();
}
private async handleIncoming(data: any): Promise<void> {
let bytes: Uint8Array;
if (data instanceof ArrayBuffer) {
bytes = new Uint8Array(data);
} else if (data instanceof Uint8Array) {
bytes = data;
} else if (typeof data === 'string') {
// Base64-encoded fallback for environments that don't support binary WS
bytes = new Uint8Array(Buffer.from(data, 'base64'));
} else {
throw new Error('Unexpected WebSocket message type');
}
const envelope = decodeEnvelope(bytes);
const plaintext = await this.manager.decrypt(this.peerAddress, envelope);
for (const handler of this.messageHandlers) {
handler(plaintext);
}
}
}