Address 5 critical and 7 warning issues from architecture review: - Fix duplicate sentence in napi-and-pubsub.md server side section - Add wraith- namespace reservation to server.md constraints (ADR-018) - Document stealth mode TLS-only requirement in server.md - Create ADR-019 for --proxy dual semantics (client vs server) - Clarify NAPI connect() vs CLI wraith connect distinction - Add SOCKS5h default as privacy design decision in client.md - Expand reconnection section (always-on, re-register port forwards) - Add graceful shutdown sections to client.md and server.md - Specify OpenSSH key format for path-or-buffer inputs across all docs - Resolve pubsub alternative approach ambiguity (ADR-018 is primary) - Replace server.md handler impl block with behavioral description - Standardize iroh endpoint ID terminology (base58-encoded) - Remove iroh API implementation details from transport.md/server.md - Add error handling pattern as cross-cutting concern in overview.md - Update all document statuses from draft to reviewed
7.7 KiB
status, last_updated
| status | last_updated |
|---|---|
| reviewed | 2026-06-02 |
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 Node.js native addon (via napi-rs) 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 minimal — it exposes transport connections as duplex streams, not the full SSH protocol. The pubsub adapter wraps those streams with EventEnvelope serialization.
Architecture
NAPI Wrapper (napi-rs)
The wrapper uses napi-rs (ADR-015) and exposes two functions (ADR-016):
// @alkdev/wraith (TypeScript side)
interface WraithConnectOptions {
// TCP/TLS mode
server?: string; // e.g., "example.com:443"
// iroh mode
peer?: string; // iroh endpoint ID (base58-encoded)
// Transport
transport: 'tcp' | 'tls' | 'iroh';
// Auth
identity?: string; // path to SSH key, or Buffer with key data
// TLS
tlsServerName?: string; // SNI hostname
insecure?: boolean; // accept self-signed certs
// iroh
irohRelay?: string; // relay URL (default: n0)
// Proxy
proxy?: string; // upstream SOCKS5/HTTP proxy URL
}
interface WraithServeOptions {
// Transport
transport: 'tcp' | 'tls' | 'iroh';
// Auth
hostKey?: string; // path to SSH host key, or Buffer with key data
authorizedKeys?: string; // path to authorized_keys, or Buffer with key data
certAuthority?: string; // path to CA public key for cert-authority auth
// TLS
tlsCert?: string; // path to TLS cert
tlsKey?: string; // path to TLS key
acmeDomain?: string; // ACME domain for auto-cert (ADR-008)
// Listen
listen?: string; // listen address (default: 0.0.0.0:22)
// iroh
irohRelay?: string; // relay URL (default: n0)
}
// Returns a Duplex stream for the SSH channel
function connect(options: WraithConnectOptions): Promise<Duplex>;
// Returns a server object with close() and connection events
function serve(options: WraithServeOptions): Promise<WraithServer>;
interface WraithServer {
close(): Promise<void>;
onConnection(callback: (stream: Duplex, info: ConnectionInfo) => void): void;
}
The NAPI layer is transport-agnostic — it doesn't know about pubsub's EventEnvelope. The pubsub adapter wraps the Duplex stream to implement TypedEventTarget. This separation ensures the NAPI wrapper is reusable for any stream-based protocol, not tied specifically to pubsub.
NAPI connect() vs CLI wraith connect
The NAPI connect() function and the CLI wraith connect command are fundamentally different operations despite sharing the same name:
- CLI
wraith connect: Starts a full SSH client session with a local SOCKS5 server and optional port forwards. It manages multiple SSH channels over a single session — the user routes traffic through it via SOCKS5 or forwarded ports. - NAPI
connect(): Opens a single SSH channel and returns it as aDuplexstream. No SOCKS5 server, no port forwarding. The caller reads and writes bytes directly. This is designed for the pubsub/programmatic use case where a single bidirectional byte stream is needed.
For SOCKS5 proxy functionality, use the CLI binary (wraith connect). The NAPI wrapper is for programmatic consumers that need a raw stream.
Programmatic Configuration (ADR-011)
Both connect() and serve() accept options as plain objects. No file paths are mandatory — keys can be provided as Buffer data directly, making programmatic usage straightforward. Environment variables (WRAITH_SERVER, WRAITH_IDENTITY) provide convenience defaults.
Key material provided as Buffer must be in OpenSSH key format (the format used by ssh-keygen). Private keys: OpenSSH format (-----BEGIN OPENSSH PRIVATE KEY-----). Public keys: OpenSSH format (ssh-ed25519 AAAA...). PEM-encoded keys (PKCS#1, PKCS#8) are not supported.
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() or serve()
}
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 uses a reserved direct_tcpip destination (wraith-control:0) for the pubsub control channel (ADR-018). When a client connects to this destination:
- The server's
channel_open_direct_iphandler detects the reservedwraith-controltarget - Instead of opening a TCP connection, it bridges the channel to its local pubsub event bus
EventEnvelopeJSON flows bidirectionally over the SSH channel
Users who prefer not to use the control channel can alternatively run a pubsub hub on a specific port and use standard port forwarding: wraith connect --forward 9736:hub:9736. This is a deployment choice, not a separate implementation — wraith's port forwarding works normally for any TCP service.
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 duplex streams, 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. - Keys can be provided as file paths or
Bufferdata, supporting both CLI and programmatic usage patterns (ADR-011).
Open Questions
None — all resolved.
Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| 007 | NAPI exposes single duplex stream | No SSH multiplexing in JS, pubsub handles it |
| 011 | Programmatic-first API | No file-based config; options are structs or env vars |
| 015 | napi-rs for FFI | Standard Node.js native addon tooling |
| 016 | Both connect() and serve() | NAPI exposes client and server sides from the start |
| 018 | Control channel for pubsub | Reserved wraith-control destination for event bus |