Docs: - README.md: index with doc table, ADR table, lifecycle definitions - overview.md: purpose, exports, dependencies, constraints - transport.md: Transport trait, TCP/TLS/iroh implementations, stream join - client.md: SOCKS5 server, port forwarding, channel manager, reconnection - server.md: auth, channel handling, stealth mode, outbound proxy - tun-shim.md: separate privileged process, virtual DNS, --unshare mode - napi-and-pubsub.md: NAPI wrapper, pubsub event target adapter ADRs: - 001: Pluggable transport via AsyncRead+AsyncWrite trait - 002: TUN shim as separate process - 003: iroh stream via tokio::io::join - 004: SSH runs over transport, not alongside - 005: SOCKS5 as primary interface, TUN as add-on - 006(007): NAPI exposes single duplex stream Open questions: 11 items covering TLS certs, iroh relay defaults, Windows TUN, auth expansion, NAPI surface, TCP reconstruction
5.1 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-06-01 |
NAPI Wrapper & PubSub Event Target
What
Two integration layers that enable TypeScript/JavaScript consumers to use wraith as a transport:
- NAPI wrapper (
@alkdev/wraith) — A minimal Node.js native addon exposingconnect()andserve()that return duplex streams - PubSub event target (
@alkdev/pubsubadapter) — An implementation of theTypedEventTargetinterface that routes events over wraith's SSH channel
Why
The wraith Rust binary serves CLI users. But the broader ecosystem (pubsub, operations, agent spokes) is TypeScript-first. These integration layers let TypeScript code use wraith's transport without reimplementing SSH.
The NAPI surface is intentionally tiny — it exposes the transport connection, not the full SSH protocol. The pubsub adapter is also minimal — it implements TypedEventTarget and serializes EventEnvelope JSON over the stream.
Architecture
NAPI Wrapper
The wrapper exposes a single function that establishes a wraith connection and returns a Node.js Duplex stream:
// @alkdev/wraith (TypeScript side)
interface WraithConnectOptions {
// TCP/TLS mode
server?: string; // e.g., "example.com:443"
// iroh mode
peer?: string; // iroh EndpointId (hex)
// Transport
transport: 'tcp' | 'tls' | 'iroh';
// Auth
identity?: string; // path to SSH key
// TLS
tlsServerName?: string; // SNI hostname
insecure?: boolean; // accept self-signed certs
// iroh
irohRelay?: string; // relay URL (default: n0)
}
function connect(options: WraithConnectOptions): Duplex;
The Duplex stream carries raw SSH channel data. On the Rust side, the NAPI function:
- Creates a transport (TCP/TLS/iroh) based on options
- Establishes an SSH session via
client::connect_stream() - Opens a single
direct_tcpipchannel to a well-known destination (or uses a control protocol) - Returns the channel as a NAPI
Bufferstream
Key design decision: The NAPI wrapper does NOT expose the full SSH channel multiplexing API. It returns one duplex stream. If the TypeScript consumer needs multiple logical channels, it multiplexes them itself (e.g., via pubsub's event routing).
PubSub Event Target Adapter
This implements TypedEventTarget from @alkdev/pubsub:
// @alkdev/pubsub (new adapter: event-target-wraith.ts)
export interface WraithEventTargetOptions {
stream: Duplex; // from @alkdev/wraith.connect()
}
export interface WraithEventTarget<TEvent extends TypedEvent>
extends TypedEventTarget<TEvent> {
close(): void;
}
export function createWraithEventTarget<TEvent extends TypedEvent>(
options: WraithEventTargetOptions
): WraithEventTarget<TEvent>;
Wire protocol (same as other pubsub adapters):
- Framing: 4-byte big-endian length prefix + JSON payload
- Payload:
EventEnvelopeJSON ({ type, id, payload }) - Control:
__subscribe/__unsubscribemessages for topic-based routing - Direction: Bidirectional —
dispatchEventsends,addEventListenersubscribes and receives
On the Server Side
The wraith server exposes a "control channel" — a special direct_tcpip destination (e.g., wraith-control:0) that routes to the pubsub event bus instead of a TCP target. When a client connects to this destination:
- Server's
channel_open_direct_tcpiphandler detects the special target - Instead of opening a TCP connection, it bridges the channel to its local pubsub event bus
EventEnvelopeJSON flows bidirectionally over the SSH channel
Alternatively, the server can listen on a specific port (e.g., 9736) for the hub's WebSocket server, and wraith simply port-forwards that port.
Direction Agnostic
Because wraith supports both local and remote port forwarding, the event target works in either direction:
- Worker connects to hub:
wraith connect --forward 9736:hub:9736then create WebSocket event target pointing atws://localhost:9736 - Hub connects to worker:
wraith connect --remote-forward 9736:worker:9736— same result, opposite initiator
The pubsub adapter doesn't care which side initiated the SSH session. It just needs a byte stream.
Constraints
- The NAPI wrapper exposes a single duplex stream, not the full SSH channel API. Multiplexing is done at the pubsub layer.
- The pubsub wire protocol is length-prefixed JSON, matching the existing adapter pattern. Binary payloads should be base64-encoded in the
EventEnvelope.payload. - The NAPI binary size will be ~5-10MB (includes russh + tokio + cryptography). The
irohfeature adds significant size; it should be an optional feature.
Open Questions
- OQ-10: Whether the NAPI wrapper should expose raw channel access or a higher-level "send JSON, receive JSON" API
- OQ-11: Whether to use napi-rs or uniffi for the FFI bridge (napi-rs is more established for Node.js, uniffi supports more targets)
Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| 007 | NAPI exposes single duplex stream | No SSH multiplexing in JS, pubsub handles it |