Multi-tenant reverse-tunnel service ("ngrok for Claude Code" with E2E): a
host-agent dials OUT to an operator-run relay; external devices reach the host
THROUGH the relay, routed by per-tenant subdomain, forwarding ciphertext only
(the relay never sees plaintext). Lets a customer reach their own self-hosted
web-terminal from anywhere with zero networking setup.
Packages — all tsc-strict + vitest green (656 tests), cross-package integration verified:
- relay-contracts: frozen shared contracts (mux frame codec, data model,
capability token, E2E envelope, pairing) — the src/types.ts analog
- term-relay: native WS mux + stateless data plane (subdomain routing, ciphertext forward)
- agent: host-agent (pairing, per-host Ed25519 + mTLS dial-out, forwards to 127.0.0.1:3000)
- control-plane: accounts/hosts registry, pairing-code flow, routing table, provisioning
- relay-e2e: browser<->agent E2E (X25519 ECDH through relay, AEAD, anti-replay, recoverable replay key)
- relay-auth: Passkey/WebAuthn, capability tokens, per-host certs, deny-by-default tenant isolation
- relay-web: browser login + Web Crypto E2E + client-side preview rendering
Security invariants INV1-15 enforced; cross-tenant isolation CI tripwire live
(.github/workflows/relay-tripwire.yml). Design + implementation-level plans in
docs/PLAN_RELAY_*.md and docs/EXPLORE_RELAY_SERVICE.md.
NOTE: generated autonomously per the reviewed plans. The security-critical
packages (relay-e2e, relay-auth) REQUIRE expert security audit before any real
deployment — passing tests prove self-consistency, not resistance to attackers.
Base app (src/, public/) unchanged; concurrent desktop work left uncommitted.
89 lines
3.0 KiB
TypeScript
89 lines
3.0 KiB
TypeScript
/**
|
|
* T3 · Stream state machine (§4.1) — a PURE reducer over the per-stream lifecycle.
|
|
*
|
|
* Lifecycle: `OPEN → (DATA | WINDOW_UPDATE)* → CLOSE`. An illegal transition returns
|
|
* `{ illegal: true }` so the caller RSTs that ONE stream (never the tunnel) — this is what
|
|
* lets P4's end-to-end `seq` monotonicity mean something (INV13). No I/O, no input mutation.
|
|
*/
|
|
import type { MuxFrameType } from 'relay-contracts'
|
|
|
|
export type StreamState =
|
|
| 'idle'
|
|
| 'open'
|
|
| 'halfClosedLocal'
|
|
| 'halfClosedRemote'
|
|
| 'closed'
|
|
|
|
export type StreamDirection = 'inbound' | 'outbound'
|
|
|
|
export type StreamTransition = { readonly next: StreamState } | { readonly illegal: true }
|
|
|
|
const ILLEGAL: StreamTransition = { illegal: true }
|
|
|
|
export function initialStreamState(): StreamState {
|
|
return 'idle'
|
|
}
|
|
|
|
export function isTerminal(s: StreamState): boolean {
|
|
return s === 'closed'
|
|
}
|
|
|
|
/** True when the state still accepts DATA/WINDOW_UPDATE flow in at least one direction. */
|
|
function isFlowing(s: StreamState): boolean {
|
|
return s === 'open' || s === 'halfClosedLocal' || s === 'halfClosedRemote'
|
|
}
|
|
|
|
/** The half-closed state produced when `dir` sends FIN while fully open. */
|
|
function halfCloseFor(dir: StreamDirection): StreamState {
|
|
return dir === 'outbound' ? 'halfClosedLocal' : 'halfClosedRemote'
|
|
}
|
|
|
|
/**
|
|
* Total transition function. Returns a NEW state (never mutates). `fin` marks a DATA/CLOSE
|
|
* frame that half/fully closes the sending direction; `dir` is which side sent the frame.
|
|
*/
|
|
export function nextStreamState(
|
|
current: StreamState,
|
|
type: MuxFrameType,
|
|
fin: boolean,
|
|
dir: StreamDirection,
|
|
): StreamTransition {
|
|
if (current === 'closed') return ILLEGAL
|
|
|
|
switch (type) {
|
|
case 'open':
|
|
// OPEN is only legal from idle; re-OPEN of a live stream is illegal.
|
|
return current === 'idle' ? { next: 'open' } : ILLEGAL
|
|
|
|
case 'data':
|
|
case 'windowUpdate': {
|
|
if (current === 'idle') return ILLEGAL // DATA/WU before OPEN
|
|
if (!isFlowing(current)) return ILLEGAL
|
|
// WINDOW_UPDATE never carries FIN; only DATA may half-close its direction.
|
|
if (type === 'windowUpdate') return { next: current }
|
|
if (!fin) return { next: current }
|
|
return finTransition(current, dir)
|
|
}
|
|
|
|
case 'close': {
|
|
if (current === 'idle') return ILLEGAL // CLOSE before OPEN
|
|
return { next: 'closed' }
|
|
}
|
|
|
|
// ping/pong/goaway are connection-level (streamId 0), never per-stream.
|
|
default:
|
|
return ILLEGAL
|
|
}
|
|
}
|
|
|
|
/** Apply a FIN from `dir` to a flowing state; both-side FIN ⇒ closed. */
|
|
function finTransition(current: StreamState, dir: StreamDirection): StreamTransition {
|
|
const half = halfCloseFor(dir)
|
|
if (current === 'open') return { next: half }
|
|
// Already half-closed on the OTHER side, and now this side FINs ⇒ fully closed.
|
|
if (current === 'halfClosedLocal' && dir === 'inbound') return { next: 'closed' }
|
|
if (current === 'halfClosedRemote' && dir === 'outbound') return { next: 'closed' }
|
|
// Re-FIN on the already-closed direction is illegal.
|
|
return ILLEGAL
|
|
}
|