feat(relay): rendezvous-relay service — 7 packages + plans (contracts/transport/agent/control-plane/e2e/auth/web)

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.
This commit is contained in:
Yaojia Wang
2026-07-02 06:10:16 +02:00
parent e4c327e25e
commit 2af57e6686
326 changed files with 40877 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
import { describe, test, expect, vi } from 'vitest'
import { createAgentListener, type MtlsVerifier, type RouteRegistrar, type TlsServerOptions } from '../data-plane/agent-listener.js'
import { loadDataPlaneConfig, type DataPlaneConfig } from '../data-plane/config.js'
import type { WebSocketLike } from '../data-plane/ws-like.js'
import type { ScheduleHandle } from '../mux/heartbeat.js'
function cfg(): DataPlaneConfig {
return loadDataPlaneConfig({
BASE_DOMAIN: 'term.example.com',
TLS_CERT_PATH: '/c',
TLS_KEY_PATH: '/k',
AGENT_CA_CERT_PATH: '/ca',
RELAY_NODE_ID: 'node-1',
HEARTBEAT_INTERVAL_MS: '1000',
ROUTE_TTL_MS: '5000',
})
}
function fakeWs() {
const closeCbs: (() => void)[] = []
return {
send: vi.fn(),
close: vi.fn(),
onMessage: vi.fn(),
onClose: (h: () => void) => closeCbs.push(h),
emitClose: () => closeCbs.forEach((f) => f()),
} satisfies WebSocketLike & { emitClose: () => void }
}
function goodMtls(): MtlsVerifier {
return { verifyPeer: (cert) => (cert[0] === 1 ? { hostId: 'host-1', accountId: 'acct-1' } : null) }
}
function registrar(): RouteRegistrar {
return { register: vi.fn(async () => {}), heartbeat: vi.fn(async () => {}), deregister: vi.fn(async () => {}) }
}
function fakeScheduler() {
const fns: (() => void)[] = []
const schedule = (fn: () => void): ScheduleHandle => {
fns.push(fn)
return { cancel: () => {} }
}
return { schedule, tick: () => fns.forEach((f) => f()) }
}
const flush = () => new Promise((r) => setTimeout(r, 0))
describe('agent tunnel listener (T9, INV4/INV7/INV14)', () => {
test('valid peer cert → MuxSession created, route registered, tunnel indexed by hostId', async () => {
const reg = registrar()
const l = createAgentListener({ config: cfg(), mtls: goodMtls(), registrar: reg, caBundle: [], onTunnel: () => {}, schedule: fakeScheduler().schedule })
l.attach(fakeWs(), new Uint8Array([1]))
await flush()
expect(reg.register).toHaveBeenCalledWith('host-1', 'node-1', 5000)
expect(l.tunnels().get('host-1')?.hostId).toBe('host-1')
})
test('TLS-layer enforcement (Finding-4): server built with requestCert+rejectUnauthorized+ca', () => {
let opts: TlsServerOptions | undefined
const ca = [new Uint8Array([9, 9])]
createAgentListener({
config: cfg(),
mtls: goodMtls(),
registrar: registrar(),
caBundle: ca,
onTunnel: () => {},
tlsServerFactory: (o) => {
opts = o
return { close: () => {} }
},
})
expect(opts).toEqual({ requestCert: true, rejectUnauthorized: true, ca })
})
test('invalid cert (verifyPeer null) → ws closed, NO route registered (INV14)', async () => {
const reg = registrar()
const ws = fakeWs()
const l = createAgentListener({ config: cfg(), mtls: goodMtls(), registrar: reg, caBundle: [], onTunnel: () => {}, schedule: fakeScheduler().schedule })
l.attach(ws, new Uint8Array([0])) // cert[0] !== 1 → verifyPeer null
await flush()
expect(ws.close).toHaveBeenCalled()
expect(reg.register).not.toHaveBeenCalled()
expect(l.tunnels().size).toBe(0)
})
test('heartbeat: registrar.heartbeat runs on the interval; deregister on ws close', async () => {
const reg = registrar()
const sched = fakeScheduler()
const ws = fakeWs()
const l = createAgentListener({ config: cfg(), mtls: goodMtls(), registrar: reg, caBundle: [], onTunnel: () => {}, schedule: sched.schedule })
l.attach(ws, new Uint8Array([1]))
sched.tick()
await flush()
expect(reg.heartbeat).toHaveBeenCalledWith('host-1')
ws.emitClose()
await flush()
expect(reg.deregister).toHaveBeenCalledWith('host-1')
expect(l.tunnels().size).toBe(0)
})
test('closeTunnel tears down + deregisters (revocation lever, INV12)', async () => {
const reg = registrar()
const l = createAgentListener({ config: cfg(), mtls: goodMtls(), registrar: reg, caBundle: [], onTunnel: () => {}, schedule: fakeScheduler().schedule })
l.attach(fakeWs(), new Uint8Array([1]))
l.tunnels().get('host-1')!.closeTunnel()
await flush()
expect(reg.deregister).toHaveBeenCalledWith('host-1')
expect(l.tunnels().size).toBe(0)
})
test('reconnection: a second attach for the same hostId replaces the prior tunnel', async () => {
const reg = registrar()
const l = createAgentListener({ config: cfg(), mtls: goodMtls(), registrar: reg, caBundle: [], onTunnel: () => {}, schedule: fakeScheduler().schedule })
l.attach(fakeWs(), new Uint8Array([1]))
const first = l.tunnels().get('host-1')
l.attach(fakeWs(), new Uint8Array([1]))
const second = l.tunnels().get('host-1')
await flush()
expect(second).not.toBe(first)
expect(reg.deregister).toHaveBeenCalledWith('host-1') // old one deregistered
expect(reg.register).toHaveBeenCalledTimes(2)
expect(l.tunnels().size).toBe(1)
})
})

View File

@@ -0,0 +1,57 @@
import { describe, test, expect } from 'vitest'
import { loadDataPlaneConfig } from '../data-plane/config.js'
const base = {
BASE_DOMAIN: 'term.example.com',
TLS_CERT_PATH: '/etc/relay/cert.pem',
TLS_KEY_PATH: '/etc/relay/key.pem',
AGENT_CA_CERT_PATH: '/etc/relay/ca.pem',
RELAY_NODE_ID: 'node-1',
} satisfies NodeJS.ProcessEnv
describe('loadDataPlaneConfig (T1, INV9 fail-fast)', () => {
test('happy path returns a frozen typed config with defaults applied', () => {
const cfg = loadDataPlaneConfig(base)
expect(cfg.baseDomain).toBe('term.example.com')
expect(cfg.bindHost).toBe('0.0.0.0')
expect(cfg.maxFrameBytes).toBe(1024 * 1024)
expect(cfg.initialWindowBytes).toBe(256 * 1024)
expect(cfg.heartbeatIntervalMs).toBe(15_000)
expect(cfg.routeTtlMs).toBe(45_000)
// agentCaChainPath falls back to agentCaCertPath when unset.
expect(cfg.agentCaChainPath).toBe('/etc/relay/ca.pem')
expect(Object.isFrozen(cfg)).toBe(true)
})
test('throws when BASE_DOMAIN is missing', () => {
expect(() => loadDataPlaneConfig({ ...base, BASE_DOMAIN: undefined })).toThrow(/baseDomain/)
})
test('throws when TLS_CERT_PATH / TLS_KEY_PATH missing (fail-fast)', () => {
expect(() => loadDataPlaneConfig({ ...base, TLS_CERT_PATH: undefined })).toThrow(/tlsCertPath/)
expect(() => loadDataPlaneConfig({ ...base, TLS_KEY_PATH: undefined })).toThrow(/tlsKeyPath/)
})
test('throws when AGENT_CA_CERT_PATH (mTLS trust-anchor) missing — Finding-4 footgun', () => {
expect(() => loadDataPlaneConfig({ ...base, AGENT_CA_CERT_PATH: undefined })).toThrow(
/agentCaCertPath/,
)
})
test('throws on non-numeric MAX_FRAME_BYTES', () => {
expect(() => loadDataPlaneConfig({ ...base, MAX_FRAME_BYTES: 'not-a-number' })).toThrow(
/maxFrameBytes/,
)
})
test('error message never echoes secret values (INV9)', () => {
try {
loadDataPlaneConfig({ ...base, TLS_KEY_PATH: undefined, MAX_FRAME_BYTES: 'xyz' })
throw new Error('should have thrown')
} catch (e) {
const msg = (e as Error).message
expect(msg).not.toContain('/etc/relay/key.pem')
expect(msg).not.toContain('xyz')
}
})
})

View File

@@ -0,0 +1,74 @@
import { describe, test, expect, vi } from 'vitest'
import { drainNode, drainHost, DRAIN_REASON } from '../data-plane/drain.js'
import type { AgentTunnel, RouteRegistrar } from '../data-plane/agent-listener.js'
import type { MuxSession } from '../mux/mux-session.js'
function fakeTunnel(hostId: string): AgentTunnel & { drainSpy: ReturnType<typeof vi.fn> } {
const drainSpy = vi.fn()
const session = { openStream: vi.fn(), onWire: vi.fn(), drain: drainSpy, close: vi.fn() } satisfies MuxSession
return { hostId, accountId: 'a', session, closeTunnel: vi.fn(), drainSpy }
}
function registrar(): RouteRegistrar {
return { register: vi.fn(async () => {}), heartbeat: vi.fn(async () => {}), deregister: vi.fn(async () => {}) }
}
describe('graceful drain + GOAWAY (T11, INV7/INV12)', () => {
test('operatorDrain: GOAWAY sent, teardown deferred to the grace window', async () => {
const t = fakeTunnel('h1')
const reg = registrar()
const scheduled: (() => void)[] = []
await drainNode({
tunnels: () => new Map([['h1', t]]),
registrar: reg,
reason: DRAIN_REASON.operatorDrain,
inFlightGraceMs: 5000,
setTimeoutFn: (fn) => scheduled.push(fn),
})
expect(t.drainSpy).toHaveBeenCalledWith(0, DRAIN_REASON.operatorDrain) // GOAWAY
expect(t.closeTunnel).not.toHaveBeenCalled() // still within grace
scheduled.forEach((f) => f()) // grace elapses
await new Promise((r) => setTimeout(r, 0))
expect(t.closeTunnel).toHaveBeenCalled()
expect(reg.deregister).toHaveBeenCalledWith('h1')
})
test('revoked: IMMEDIATE teardown regardless of a large grace (Finding-2/INV12)', async () => {
const t = fakeTunnel('h1')
const reg = registrar()
const setTimeoutFn = vi.fn()
await drainHost('h1', {
tunnels: () => new Map([['h1', t]]),
registrar: reg,
reason: DRAIN_REASON.revoked,
inFlightGraceMs: 30_000, // must be ignored
setTimeoutFn,
})
expect(t.drainSpy).toHaveBeenCalledWith(0, DRAIN_REASON.revoked)
expect(t.closeTunnel).toHaveBeenCalled() // immediate
expect(reg.deregister).toHaveBeenCalledWith('h1')
expect(setTimeoutFn).not.toHaveBeenCalled() // grace never scheduled
})
test('drainHost targets exactly one host; siblings keep running', async () => {
const h1 = fakeTunnel('h1')
const h2 = fakeTunnel('h2')
const reg = registrar()
await drainHost('h1', {
tunnels: () => new Map([['h1', h1], ['h2', h2]]),
registrar: reg,
reason: DRAIN_REASON.revoked,
inFlightGraceMs: 0,
})
expect(h1.closeTunnel).toHaveBeenCalled()
expect(h2.closeTunnel).not.toHaveBeenCalled()
expect(reg.deregister).toHaveBeenCalledWith('h1')
expect(reg.deregister).not.toHaveBeenCalledWith('h2')
})
test('drainHost for an absent host is a no-op (deny-by-default, never a broader kill)', async () => {
const reg = registrar()
await drainHost('ghost', { tunnels: () => new Map(), registrar: reg, reason: DRAIN_REASON.revoked, inFlightGraceMs: 0 })
expect(reg.deregister).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,68 @@
import { describe, test, expect } from 'vitest'
import {
createFlowController,
DEFAULT_INITIAL_WINDOW,
WINDOW_REPLENISH_THRESHOLD,
} from '../mux/flow-control.js'
describe('credit flow control (T4, §4.1)', () => {
test('canSend true until credit exhausted, then false; never negative', () => {
const fc = createFlowController()
fc.registerStream(1, 100)
expect(fc.canSend(1, 100)).toBe(true)
fc.consumeSendCredit(1, 100)
expect(fc.creditFor(1)).toBe(0)
expect(fc.canSend(1, 1)).toBe(false)
// Over-consume never drives credit negative.
fc.consumeSendCredit(1, 50)
expect(fc.creditFor(1)).toBe(0)
})
test('grantCredit unblocks a backpressured stream', () => {
const fc = createFlowController()
fc.registerStream(1, 10)
fc.consumeSendCredit(1, 10)
expect(fc.canSend(1, 5)).toBe(false)
fc.grantCredit(1, 20)
expect(fc.canSend(1, 5)).toBe(true)
expect(fc.creditFor(1)).toBe(20)
})
test('onDelivered returns a replenish amount only once the threshold is crossed', () => {
const fc = createFlowController()
fc.registerStream(1, 100) // threshold = 50 bytes
expect(fc.onDelivered(1, 40)).toBe(0)
const replenish = fc.onDelivered(1, 20) // cumulative 60 ≥ 50
expect(replenish).toBe(60)
// Counter reset after replenish.
expect(fc.onDelivered(1, 10)).toBe(0)
})
test('unknown streamId is deny-by-default: canSend false, creditFor 0', () => {
const fc = createFlowController()
expect(fc.canSend(999, 1)).toBe(false)
expect(fc.creditFor(999)).toBe(0)
expect(fc.onDelivered(999, 100)).toBe(0)
})
test('released streams carry no credit (send-after-close is denied)', () => {
const fc = createFlowController()
fc.registerStream(1, 100)
fc.releaseStream(1)
expect(fc.canSend(1, 1)).toBe(false)
})
test('per-stream isolation: exhausting A does not affect B (no cross-stream starvation)', () => {
const fc = createFlowController()
fc.registerStream(1, 50)
fc.registerStream(2, 50)
fc.consumeSendCredit(1, 50)
expect(fc.canSend(1, 1)).toBe(false)
expect(fc.canSend(2, 50)).toBe(true) // B untouched
})
test('exposes the documented constants', () => {
expect(DEFAULT_INITIAL_WINDOW).toBe(256 * 1024)
expect(WINDOW_REPLENISH_THRESHOLD).toBe(0.5)
})
})

View File

@@ -0,0 +1,112 @@
import { describe, test, expect } from 'vitest'
import {
MUX_HEADER_BYTES,
encodeMuxFrame,
decodeHeader,
decodeMuxFrame,
encodeOpen,
decodeOpen,
encodeWindowUpdate,
decodeWindowUpdate,
encodeGoaway,
decodeGoaway,
TYPE_TO_BYTE,
BYTE_TO_TYPE,
type MuxFrameHeader,
type MuxFrameType,
type MuxOpen,
} from '../mux/frame-codec.js'
const ALL_TYPES: readonly MuxFrameType[] = [
'open',
'data',
'close',
'ping',
'pong',
'windowUpdate',
'goaway',
]
function header(over: Partial<MuxFrameHeader>): MuxFrameHeader {
return { version: 1, type: 'data', fin: false, rst: false, streamId: 7, payloadLen: 0, ...over }
}
describe('frame codec (T2, §4.1) — re-exported from the frozen relay-contracts codec', () => {
test('round-trips every frame type incl. fin/rst bit packing', () => {
for (const type of ALL_TYPES) {
const payload = new Uint8Array([1, 2, 3])
const h = header({ type, fin: true, rst: false, streamId: 42, payloadLen: payload.length })
const decoded = decodeMuxFrame(encodeMuxFrame(h, payload))
expect(decoded.header.type).toBe(type)
expect(decoded.header.fin).toBe(true)
expect(decoded.header.rst).toBe(false)
expect(decoded.header.streamId).toBe(42)
expect([...decoded.payload]).toEqual([1, 2, 3])
}
})
test('streamId=0 link-level frames decode with streamId===0', () => {
const h = header({ type: 'ping', streamId: 0, payloadLen: 0 })
expect(decodeHeader(encodeMuxFrame(h, new Uint8Array(0))).streamId).toBe(0)
})
test('rst bit round-trips independently of fin', () => {
const h = header({ type: 'close', fin: false, rst: true, payloadLen: 0 })
const d = decodeHeader(encodeMuxFrame(h, new Uint8Array(0)))
expect(d.rst).toBe(true)
expect(d.fin).toBe(false)
})
test('type ⇄ byte maps agree with §4.1 (0x01 OPEN … 0x07 GOAWAY)', () => {
expect(TYPE_TO_BYTE.open).toBe(0x01)
expect(TYPE_TO_BYTE.goaway).toBe(0x07)
expect(BYTE_TO_TYPE[0x02]).toBe('data')
})
test('negative: truncated buffer (< 15B) throws', () => {
expect(() => decodeHeader(new Uint8Array(MUX_HEADER_BYTES - 1))).toThrow()
})
test('negative: payloadLen larger than actual payload throws', () => {
const buf = encodeMuxFrame(header({ payloadLen: 2 }), new Uint8Array([9, 9]))
// Claim a longer payload than present.
buf[14] = 5
expect(() => decodeMuxFrame(buf)).toThrow()
})
test('negative: unknown type byte throws', () => {
const buf = encodeMuxFrame(header({ type: 'data', payloadLen: 0 }), new Uint8Array(0))
buf[1] = 0x08 // unknown type code
expect(() => decodeHeader(buf)).toThrow()
buf[1] = 0x00
expect(() => decodeHeader(buf)).toThrow()
})
test('negative: version ≠ 0x01 throws', () => {
const buf = encodeMuxFrame(header({ payloadLen: 0 }), new Uint8Array(0))
buf[0] = 0x02
expect(() => decodeHeader(buf)).toThrow()
})
test('OPEN payload round-trips via CBOR + Zod guard', () => {
const open: MuxOpen = {
streamId: 3,
subdomain: 'alice',
requestPath: '/term?join=x',
originHeader: 'https://alice.term.example.com',
remoteAddrHash: 'deadbeef',
capabilityTokenRef: 'jti-1',
}
expect(decodeOpen(encodeOpen(open))).toEqual(open)
})
test('decodeOpen rejects a malformed/partial CBOR payload (never returns partial)', () => {
expect(() => decodeOpen(new Uint8Array([0x00, 0x01, 0x02]))).toThrow()
})
test('WINDOW_UPDATE + GOAWAY int codecs round-trip', () => {
expect(decodeWindowUpdate(encodeWindowUpdate(65_536))).toBe(65_536)
const g = decodeGoaway(encodeGoaway(9, 'revoked'))
expect(g).toEqual({ lastStreamId: 9, reason: 'revoked' })
})
})

View File

@@ -0,0 +1,74 @@
import { describe, test, expect, vi } from 'vitest'
import {
createHeartbeat,
HEARTBEAT_INTERVAL_MS,
HEARTBEAT_MISS_LIMIT,
type ScheduleHandle,
} from '../mux/heartbeat.js'
/** A manually-driven fake scheduler: `tick()` fires the scheduled callback; cancel stops it. */
function fakeScheduler() {
let scheduled: (() => void) | null = null
const schedule = (fn: () => void): ScheduleHandle => {
scheduled = fn
return { cancel: () => (scheduled = null) }
}
return { schedule, tick: () => scheduled?.() }
}
describe('heartbeat (T5, §4.1 15s PING/PONG)', () => {
test('start sends a PING immediately with a fresh 8-byte token', () => {
const { schedule } = fakeScheduler()
const pings: Uint8Array[] = []
const hb = createHeartbeat({ schedule, sendPing: (t) => pings.push(t), onDead: () => {} })
hb.start()
expect(pings).toHaveLength(1)
expect(pings[0]!.length).toBe(8)
})
test('a matching onPong resets the miss counter; 3 misses ⇒ onDead exactly once', () => {
const sched = fakeScheduler()
const pings: Uint8Array[] = []
const onDead = vi.fn()
const hb = createHeartbeat({ schedule: sched.schedule, sendPing: (t) => pings.push(t), onDead })
hb.start() // ping #1, outstanding set
hb.onPong(pings[0]!) // matched → miss counter reset
sched.tick() // outstanding was cleared → no miss, ping #2
sched.tick() // miss 1, ping #3
sched.tick() // miss 2, ping #4
sched.tick() // miss 3 → dead
expect(onDead).toHaveBeenCalledTimes(1)
const pingsAfterDead = pings.length
sched.tick() // no further pings after dead
expect(pings.length).toBe(pingsAfterDead)
})
test('a stale/unknown PONG token does NOT reset the miss counter (anti-replay)', () => {
const sched = fakeScheduler()
const onDead = vi.fn()
const hb = createHeartbeat({ schedule: sched.schedule, sendPing: () => {}, onDead })
hb.start()
hb.onPong(new Uint8Array([9, 9, 9, 9, 9, 9, 9, 9])) // wrong token — ignored
sched.tick() // miss 1
sched.tick() // miss 2
sched.tick() // miss 3 → dead despite the spoofed PONG
expect(onDead).toHaveBeenCalledTimes(1)
})
test('stop cancels cleanly (no onDead after stop)', () => {
const sched = fakeScheduler()
const onDead = vi.fn()
const hb = createHeartbeat({ schedule: sched.schedule, sendPing: () => {}, onDead })
hb.start()
hb.stop()
sched.tick() // scheduler cancelled → no-op
sched.tick()
sched.tick()
expect(onDead).not.toHaveBeenCalled()
})
test('exposes the §4.1 constants', () => {
expect(HEARTBEAT_INTERVAL_MS).toBe(15_000)
expect(HEARTBEAT_MISS_LIMIT).toBe(3)
})
})

View File

@@ -0,0 +1,101 @@
import { describe, test, expect } from 'vitest'
import { createMuxSession, type MuxSession } from '../../mux/mux-session.js'
import { createRelayNode } from '../../data-plane/relay-node.js'
import { loadDataPlaneConfig } from '../../data-plane/config.js'
import type { AgentListener, AgentTunnel } from '../../data-plane/agent-listener.js'
import type { UpgradeDecision, UpgradeRequest } from '../../data-plane/upgrade.js'
const config = loadDataPlaneConfig({
BASE_DOMAIN: 'term.example.com',
TLS_CERT_PATH: '/c',
TLS_KEY_PATH: '/k',
AGENT_CA_CERT_PATH: '/ca',
RELAY_NODE_ID: 'node-1',
})
const noSchedule = () => ({ cancel: () => {} })
/** Build a real relay↔agent MuxSession pair; the agent echoes each DATA payload back verbatim. */
function wiredTunnel(hostId: string): AgentTunnel {
let agent!: MuxSession
const relay = createMuxSession({
role: 'relay',
sendWire: (f) => agent.onWire(f),
onOpen: () => {},
onData: () => {},
onClose: () => {},
onDead: () => {},
maxFrameBytes: 1024,
initialWindowBytes: 1_000_000,
schedule: noSchedule,
})
agent = createMuxSession({
role: 'agent',
sendWire: (f) => relay.onWire(f),
onOpen: (_open, stream) => {
// The agent dials its local ws://127.0.0.1:3000 (here: an echo) — OPAQUE bytes only.
stream.onData((bytes) => stream.writeData(bytes))
},
onData: () => {},
onClose: () => {},
onDead: () => {},
maxFrameBytes: 1024,
initialWindowBytes: 1_000_000,
schedule: noSchedule,
})
return { hostId, accountId: 'acct-1', session: relay, closeTunnel: () => relay.close() }
}
function listenerWith(tunnels: Map<string, AgentTunnel>): AgentListener {
return { attach: () => {}, tunnels: () => tunnels, close: () => {} }
}
function browserWs() {
let onMsg: ((b: Uint8Array) => void) | null = null
return {
sent: [] as Uint8Array[],
closedWith: undefined as number | undefined,
send(b: Uint8Array) {
this.sent.push(b)
},
close(code?: number) {
this.closedWith = code
},
onMessage(h: (b: Uint8Array) => void) {
onMsg = h
},
onClose() {},
type: (b: Uint8Array) => onMsg?.(b),
}
}
function okDecision(hostId: string): UpgradeDecision {
return {
ok: true,
hostId,
acceptedSubprotocol: 'term.relay.v1',
open: { streamId: 0, subdomain: 'alice', requestPath: '/', originHeader: 'o', remoteAddrHash: 'h', capabilityTokenRef: 'jti' },
}
}
describe('data-plane integration (T13) — end-to-end opaque splice through a real MuxSession pair', () => {
test('INV2: bytes round-trip browser → agent → browser byte-identical, never parsed', async () => {
const tunnels = new Map([['host-A', wiredTunnel('host-A')]])
const node = createRelayNode({ config, listener: listenerWith(tunnels), authorize: async () => okDecision('host-A') })
const ws = browserWs()
await node.handleBrowserUpgrade({} as UpgradeRequest, ws)
const payload = new TextEncoder().encode('echo-me-🔒-ciphertext')
ws.type(payload)
expect(ws.sent).toHaveLength(1)
expect([...ws.sent[0]!]).toEqual([...payload]) // opaque round-trip
})
test('INV1: an upgrade authorized for a host not on this node cannot reach any tunnel', async () => {
const tunnels = new Map([['host-A', wiredTunnel('host-A')]])
const node = createRelayNode({ config, listener: listenerWith(tunnels), authorize: async () => okDecision('host-B') })
const ws = browserWs()
await node.handleBrowserUpgrade({} as UpgradeRequest, ws)
expect(ws.closedWith).toBe(1013) // no tunnel for host-B; A is never reachable by another host's token
expect(ws.sent).toHaveLength(0)
})
})

View File

@@ -0,0 +1,65 @@
import { describe, test, expect } from 'vitest'
import { readdirSync, readFileSync, statSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { createMuxSession, type MuxSession } from '../mux/mux-session.js'
import type { MuxOpen } from '../mux/frame-codec.js'
/** Recursively collect .ts source files under a directory. */
function tsFiles(dir: string): string[] {
const out: string[] = []
for (const entry of readdirSync(dir)) {
const full = `${dir}/${entry}`
if (statSync(full).isDirectory()) out.push(...tsFiles(full))
else if (entry.endsWith('.ts')) out.push(full)
}
return out
}
const MUX_DIR = fileURLToPath(new URL('../mux', import.meta.url))
const DP_DIR = fileURLToPath(new URL('../data-plane', import.meta.url))
const TERMINAL_PARSER_RE = /xterm|ansi|vt100|terminal-parser/i
describe('P1 invariant tripwires (T13)', () => {
test('INV11: no mux/ or data-plane/ source imports a terminal/ANSI parser', () => {
const files = [...tsFiles(MUX_DIR), ...tsFiles(DP_DIR)]
expect(files.length).toBeGreaterThan(0)
const offenders = files.filter((f) => {
const src = readFileSync(f, 'utf8')
const importLines = src.split('\n').filter((l) => /\bimport\b/.test(l) || /\bfrom\b/.test(l))
return importLines.some((l) => TERMINAL_PARSER_RE.test(l))
})
expect(offenders).toEqual([])
})
test('INV2: a plaintext marker rides DATA as OPAQUE bytes; never inspected or retained', () => {
const marker = new TextEncoder().encode('SECRET-PLAINTEXT-MARKER')
let delivered: Uint8Array | null = null
let agent!: MuxSession
const relay = createMuxSession({
role: 'relay',
sendWire: (f) => agent.onWire(f),
onOpen: () => {},
onData: () => {},
onClose: () => {},
onDead: () => {},
maxFrameBytes: 1024,
initialWindowBytes: 1_000_000,
schedule: () => ({ cancel: () => {} }),
})
agent = createMuxSession({
role: 'agent',
sendWire: (f) => relay.onWire(f),
onOpen: () => {},
onData: (_id, payload) => (delivered = payload),
onClose: () => {},
onDead: () => {},
maxFrameBytes: 1024,
initialWindowBytes: 1_000_000,
schedule: () => ({ cancel: () => {} }),
})
const open: MuxOpen = { streamId: 0, subdomain: 'alice', requestPath: '/', originHeader: 'o', remoteAddrHash: 'h', capabilityTokenRef: 'j' }
relay.openStream(open).writeData(marker)
expect(delivered).not.toBeNull()
expect([...delivered!]).toEqual([...marker]) // byte-identical, opaque
})
})

View File

@@ -0,0 +1,172 @@
import { describe, test, expect, vi } from 'vitest'
import { createMuxSession, type MuxSession, type MuxStreamHandle } from '../mux/mux-session.js'
import { encodeMuxFrame, decodeHeader, type MuxOpen } from '../mux/frame-codec.js'
import type { ScheduleHandle } from '../mux/heartbeat.js'
const noAutoSchedule = (): { schedule: (fn: () => void) => ScheduleHandle; tick: () => void } => {
let scheduled: (() => void) | null = null
return {
schedule: (fn) => {
scheduled = fn
return { cancel: () => (scheduled = null) }
},
tick: () => scheduled?.(),
}
}
function openFor(streamId: number): MuxOpen {
return {
streamId,
subdomain: 'alice',
requestPath: '/term',
originHeader: 'https://alice.term.example.com',
remoteAddrHash: 'hash',
capabilityTokenRef: 'jti-1',
}
}
interface Pair {
relay: MuxSession
agent: MuxSession
opened: { open: MuxOpen; stream: MuxStreamHandle }[]
agentData: { streamId: number; payload: Uint8Array }[]
}
function wirePair(relayWindow = 1_000_000, agentWindow = 1_000_000): Pair {
const opened: Pair['opened'] = []
const agentData: Pair['agentData'] = []
let agent!: MuxSession
const relay = createMuxSession({
role: 'relay',
sendWire: (f) => agent.onWire(f),
onOpen: () => {},
onData: () => {},
onClose: () => {},
onDead: () => {},
maxFrameBytes: 1024,
initialWindowBytes: relayWindow,
schedule: noAutoSchedule().schedule,
})
agent = createMuxSession({
role: 'agent',
sendWire: (f) => relay.onWire(f),
onOpen: (open, stream) => opened.push({ open, stream }),
onData: (streamId, payload) => agentData.push({ streamId, payload }),
onClose: () => {},
onDead: () => {},
maxFrameBytes: 1024,
initialWindowBytes: agentWindow,
schedule: noAutoSchedule().schedule,
})
return { relay, agent, opened, agentData }
}
describe('mux session (T6)', () => {
test('openStream → agent onOpen with a fresh monotonic streamId; never reused', () => {
const p = wirePair()
p.relay.openStream(openFor(0))
p.relay.openStream(openFor(0))
expect(p.opened.map((o) => o.open.streamId)).toEqual([1, 2])
})
test('writeData delivers a byte-identical OPAQUE payload (INV2), never inspected', () => {
const p = wirePair()
const h = p.relay.openStream(openFor(0))
const marker = new TextEncoder().encode('PLAINTEXT-MARKER-🔒')
expect(h.writeData(marker)).toBe(true)
expect(p.agentData).toHaveLength(1)
expect([...p.agentData[0]!.payload]).toEqual([...marker])
})
test('backpressure: writeData returns false when the window is full; WINDOW_UPDATE resumes', () => {
// relay send window = 10, agent receive window huge (no auto-replenish crossing threshold).
const p = wirePair(10, 1_000_000)
const h = p.relay.openStream(openFor(0))
const streamId = h.streamId
const a = new Uint8Array(8).fill(1)
const b = new Uint8Array(8).fill(2)
expect(h.writeData(a)).toBe(true) // window 10 → 2
expect(h.writeData(b)).toBe(false) // 8 > 2 → buffered
expect(p.agentData).toHaveLength(1)
// Simulate a peer WINDOW_UPDATE granting credit → buffered `b` flushes.
p.relay.onWire(encodeWU(streamId, 50))
expect(p.agentData).toHaveLength(2)
expect([...p.agentData[1]!.payload]).toEqual([...b])
})
test('per-stream isolation: exhausting stream A does not block stream B', () => {
const p = wirePair(10, 1_000_000)
const a = p.relay.openStream(openFor(0))
const b = p.relay.openStream(openFor(0))
expect(a.writeData(new Uint8Array(10).fill(1))).toBe(true) // A window exhausted
expect(a.writeData(new Uint8Array(1))).toBe(false)
expect(b.writeData(new Uint8Array(10).fill(2))).toBe(true) // B unaffected
})
test('inbound DATA for an unknown stream → CLOSE+RST for that stream only (tunnel stays up)', () => {
const wire: Uint8Array[] = []
const s = createMuxSession({
role: 'agent',
sendWire: (f) => wire.push(f),
onOpen: () => {},
onData: () => {},
onClose: () => {},
onDead: () => {},
maxFrameBytes: 1024,
initialWindowBytes: 1000,
schedule: noAutoSchedule().schedule,
})
s.onWire(encodeMuxFrame({ version: 1, type: 'data', fin: false, rst: false, streamId: 99, payloadLen: 1 }, new Uint8Array([7])))
const rst = wire.map(decodeHeader).find((x) => x.type === 'close' && x.rst)
expect(rst?.streamId).toBe(99)
})
test('frame ceiling: inbound payloadLen > maxFrameBytes → RST that stream, no OOM', () => {
const wire: Uint8Array[] = []
const s = createMuxSession({
role: 'agent',
sendWire: (f) => wire.push(f),
onOpen: () => {},
onData: () => {},
onClose: () => {},
onDead: () => {},
maxFrameBytes: 4,
initialWindowBytes: 1000,
schedule: noAutoSchedule().schedule,
})
const oversized = encodeMuxFrame({ version: 1, type: 'data', fin: false, rst: false, streamId: 5, payloadLen: 5 }, new Uint8Array(5))
s.onWire(oversized)
const rst = wire.map(decodeHeader).find((x) => x.rst)
expect(rst?.streamId).toBe(5)
})
test('heartbeat: no PONG for the miss limit ⇒ onDead', () => {
const sched = noAutoSchedule()
const onDead = vi.fn()
const s = createMuxSession({
role: 'relay',
sendWire: () => {}, // peer never responds
onOpen: () => {},
onData: () => {},
onClose: () => {},
onDead,
maxFrameBytes: 1024,
initialWindowBytes: 1000,
schedule: sched.schedule,
})
s.openStream(openFor(0)) // starts heartbeat (ping #1)
sched.tick() // miss 1
sched.tick() // miss 2
sched.tick() // miss 3 → dead
expect(onDead).toHaveBeenCalledTimes(1)
})
})
function encodeWU(streamId: number, credit: number): Uint8Array {
const payload = new Uint8Array(4)
new DataView(payload.buffer).setUint32(0, credit, false)
return encodeMuxFrame(
{ version: 1, type: 'windowUpdate', fin: false, rst: false, streamId, payloadLen: 4 },
payload,
)
}

View File

@@ -0,0 +1,33 @@
import { describe, test, expect, vi } from 'vitest'
import { handleFrpHook, type ControlPlaneAuthz } from '../frp-scaffold/plugin-hook.js'
function authz(allow: boolean): ControlPlaneAuthz {
return { authorize: vi.fn(async () => ({ allow })) }
}
const loginReq = { version: '0.1.0', op: 'Login', content: { user: 'alice', token: 't' } }
describe('frp plugin-hook shim (T12, deny-by-default, delegates to P3)', () => {
test('unknown token → reject (deny-by-default)', async () => {
const res = await handleFrpHook({ ...loginReq, op: 'NewUserConn' }, authz(false))
expect(res).toEqual({ reject: true, reject_reason: 'denied by control plane' })
})
test('valid + control-plane-owned subdomain → allow (pass-through unchanged)', async () => {
const res = await handleFrpHook(loginReq, authz(true))
expect(res).toEqual({ reject: false, unchange: true })
})
test('forwards the decision to P3, never decides tenancy itself', async () => {
const a = authz(true)
await handleFrpHook({ ...loginReq, op: 'NewProxy' }, a)
expect(a.authorize).toHaveBeenCalledWith('NewProxy', loginReq.content)
})
test('malformed payload → reject at the Zod boundary, authorizer NOT called', async () => {
const a = authz(true)
const res = await handleFrpHook({ op: 'Bogus' }, a)
expect(res).toEqual({ reject: true, reject_reason: 'malformed hook request' })
expect(a.authorize).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,167 @@
import { describe, test, expect, vi } from 'vitest'
import { createRelayNode } from '../data-plane/relay-node.js'
import type { AgentListener, AgentTunnel } from '../data-plane/agent-listener.js'
import type { MuxSession, MuxStreamHandle } from '../mux/mux-session.js'
import type { WebSocketLike } from '../data-plane/ws-like.js'
import type { UpgradeDecision, UpgradeRequest } from '../data-plane/upgrade.js'
import { loadDataPlaneConfig } from '../data-plane/config.js'
const config = loadDataPlaneConfig({
BASE_DOMAIN: 'term.example.com',
TLS_CERT_PATH: '/c',
TLS_KEY_PATH: '/k',
AGENT_CA_CERT_PATH: '/ca',
RELAY_NODE_ID: 'node-1',
})
interface FakeStream {
handle: MuxStreamHandle
emitData(b: Uint8Array): void
emitClose(): void
writes: Uint8Array[]
rstClosed: boolean
}
function fakeTunnel(hostId: string) {
let nextId = 0
const fakeStreams: FakeStream[] = []
const session = {
openStream: () => {
let dataCb: ((b: Uint8Array) => void) | null = null
let closeCb: ((rst: boolean) => void) | null = null
const fs: FakeStream = { writes: [], rstClosed: false, emitData: () => {}, emitClose: () => {}, handle: undefined as unknown as MuxStreamHandle }
const handle: MuxStreamHandle = {
streamId: ++nextId,
writeData: (b) => {
fs.writes.push(b)
return true
},
close: (rst = false) => {
fs.rstClosed = rst
},
onData: (cb) => (dataCb = cb),
onClose: (cb) => (closeCb = cb),
}
fs.handle = handle
fs.emitData = (b) => dataCb?.(b)
fs.emitClose = () => closeCb?.(false)
fakeStreams.push(fs)
return handle
},
onWire: () => {},
drain: vi.fn(),
close: vi.fn(),
} satisfies MuxSession
const tunnel: AgentTunnel = { hostId, accountId: 'acct-1', session, closeTunnel: vi.fn() }
return { tunnel, fakeStreams }
}
function fakeWs() {
let onMsg: ((b: Uint8Array) => void) | null = null
let onClose: (() => void) | null = null
return {
sent: [] as Uint8Array[],
closedWith: undefined as number | undefined,
send(b: Uint8Array) {
this.sent.push(b)
},
close(code?: number) {
this.closedWith = code
},
onMessage(h: (b: Uint8Array) => void) {
onMsg = h
},
onClose(h: () => void) {
onClose = h
},
emitMessage: (b: Uint8Array) => onMsg?.(b),
emitClose: () => onClose?.(),
}
}
function listenerWith(tunnels: Map<string, AgentTunnel>): AgentListener {
return { attach: vi.fn(), tunnels: () => tunnels, close: vi.fn() }
}
function decisionFor(hostId: string, jti: string): UpgradeDecision {
return {
ok: true,
hostId,
acceptedSubprotocol: 'term.relay.v1',
open: { streamId: 0, subdomain: 'alice', requestPath: '/term', originHeader: 'o', remoteAddrHash: 'h', capabilityTokenRef: jti },
}
}
const req = {} as UpgradeRequest
describe('relay node opaque splice (T10, INV2/INV5/INV7/INV11/INV12)', () => {
test('authorized upgrade → bytes splice byte-identical in both directions', async () => {
const { tunnel, fakeStreams } = fakeTunnel('host-1')
const node = createRelayNode({ config, listener: listenerWith(new Map([['host-1', tunnel]])), authorize: async () => decisionFor('host-1', 'jti-a') })
const ws = fakeWs()
await node.handleBrowserUpgrade(req, ws)
const fs = fakeStreams[0]!
const up = new Uint8Array([1, 2, 3])
ws.emitMessage(up) // browser → agent
expect([...fs.writes[0]!]).toEqual([1, 2, 3])
const down = new Uint8Array([9, 8, 7])
fs.emitData(down) // agent → browser
expect([...ws.sent[0]!]).toEqual([9, 8, 7])
})
test('unauthorized upgrade → ws closed with the status; no stream, no tunnel touched', async () => {
const { tunnel } = fakeTunnel('host-1')
const node = createRelayNode({ config, listener: listenerWith(new Map([['host-1', tunnel]])), authorize: async () => ({ ok: false, status: 403 }) })
const ws = fakeWs()
await node.handleBrowserUpgrade(req, ws)
expect(ws.closedWith).toBe(403)
})
test('agent offline (no tunnel) → ws closed 1013, no crash', async () => {
const node = createRelayNode({ config, listener: listenerWith(new Map()), authorize: async () => decisionFor('host-x', 'jti') })
const ws = fakeWs()
await node.handleBrowserUpgrade(req, ws)
expect(ws.closedWith).toBe(1013)
})
test('single-device revocation (Finding-3): closeStream by jti RSTs only that jti', async () => {
const { tunnel, fakeStreams } = fakeTunnel('host-1')
const node = createRelayNode({ config, listener: listenerWith(new Map([['host-1', tunnel]])), authorize: async () => decisionFor('host-1', jtiRef()) })
let currentJti = 'jtiA'
function jtiRef() {
return currentJti
}
const wsA = fakeWs()
await node.handleBrowserUpgrade(req, wsA)
currentJti = 'jtiB'
const wsB = fakeWs()
await node.handleBrowserUpgrade(req, wsB)
const count = node.closeStream('host-1', { jti: 'jtiA' })
expect(count).toBe(1)
expect(fakeStreams[0]!.rstClosed).toBe(true) // A RST
expect(wsA.closedWith).toBe(4403)
expect(fakeStreams[1]!.rstClosed).toBe(false) // B untouched
// B still flows
fakeStreams[1]!.emitData(new Uint8Array([5]))
expect([...wsB.sent[0]!]).toEqual([5])
})
test('closeStream with two splices under the SAME jti returns 2; unknown jti returns 0', async () => {
const { tunnel } = fakeTunnel('host-1')
const node = createRelayNode({ config, listener: listenerWith(new Map([['host-1', tunnel]])), authorize: async () => decisionFor('host-1', 'jtiSame') })
await node.handleBrowserUpgrade(req, fakeWs())
await node.handleBrowserUpgrade(req, fakeWs())
expect(node.closeStream('host-1', { jti: 'jtiSame' })).toBe(2)
expect(node.closeStream('host-1', { jti: 'unknown' })).toBe(0)
})
test('after a browser ws closes normally, its splice entry is gone (no stale handle)', async () => {
const { tunnel } = fakeTunnel('host-1')
const node = createRelayNode({ config, listener: listenerWith(new Map([['host-1', tunnel]])), authorize: async () => decisionFor('host-1', 'jtiZ') })
const ws = fakeWs()
await node.handleBrowserUpgrade(req, ws)
ws.emitClose()
expect(node.closeStream('host-1', { jti: 'jtiZ' })).toBe(0)
})
})

View File

@@ -0,0 +1,141 @@
import { describe, test, expect, vi } from 'vitest'
import { RELAY_REVOCATIONS_CHANNEL, REVOCATION_PUSH_BUDGET_MS, type KillSignal } from 'relay-contracts'
import { startRevocationSubscriber, hostsForScope, type Subscription } from '../data-plane/revocation-subscriber.js'
import { DRAIN_REASON } from '../data-plane/drain.js'
import type { AgentTunnel } from '../data-plane/agent-listener.js'
const UUID_H = '11111111-1111-4111-8111-111111111111'
const UUID_H2 = '22222222-2222-4222-8222-222222222222'
const UUID_A = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'
function tunnel(hostId: string, accountId: string): AgentTunnel {
return { hostId, accountId, session: {} as AgentTunnel['session'], closeTunnel: () => {} }
}
function fakeBus() {
let handler: ((raw: string) => void) | null = null
const sub: Subscription = { close: vi.fn() }
return {
subscribe: (channel: string, onMessage: (raw: string) => void) => {
expect(channel).toBe(RELAY_REVOCATIONS_CHANNEL)
handler = onMessage
return sub
},
publish: (signal: KillSignal | string) => handler?.(typeof signal === 'string' ? signal : JSON.stringify(signal)),
sub,
}
}
describe('hostsForScope (T14, pure blast-radius selector)', () => {
const live = new Map([
[UUID_H, tunnel(UUID_H, UUID_A)],
[UUID_H2, tunnel(UUID_H2, 'other-acct')],
])
const accountOf = (h: string) => live.get(h)?.accountId
test('host scope: only if this node serves it', () => {
expect(hostsForScope({ kind: 'host', hostId: UUID_H }, live, accountOf)).toEqual([UUID_H])
expect(hostsForScope({ kind: 'host', hostId: 'nope' }, live, accountOf)).toEqual([])
})
test('account scope: only the accounts hosts on this node', () => {
expect(hostsForScope({ kind: 'account', accountId: UUID_A }, live, accountOf)).toEqual([UUID_H])
})
test('global scope: every live host', () => {
expect(hostsForScope({ kind: 'global' }, live, accountOf)).toEqual([UUID_H, UUID_H2])
})
})
describe('revocation subscriber (T14, INV12/FIX 4)', () => {
function setup(tunnels: Map<string, AgentTunnel>) {
const bus = fakeBus()
const drainHost = vi.fn(async () => {})
const onApplied = vi.fn()
const onDropped = vi.fn()
const clock = { t: 0 }
startRevocationSubscriber({
subscribe: bus.subscribe,
drainHost,
tunnels: () => tunnels,
now: () => clock.t,
onApplied,
onDropped,
})
return { bus, drainHost, onApplied, onDropped, clock }
}
test('host signal → drainHost(H, revoked) once, within the push budget', async () => {
const { bus, drainHost } = setup(new Map([[UUID_H, tunnel(UUID_H, UUID_A)]]))
bus.publish({ scope: { kind: 'host', hostId: UUID_H }, at: 1, reason: 'compromised' })
await new Promise((r) => setTimeout(r, 0))
expect(drainHost).toHaveBeenCalledTimes(1)
expect(drainHost).toHaveBeenCalledWith(UUID_H, DRAIN_REASON.revoked)
expect(REVOCATION_PUSH_BUDGET_MS).toBe(2000)
})
test('a host NOT on this node → no-op (0 calls, no cross-tenant reach)', async () => {
const { bus, drainHost } = setup(new Map([[UUID_H, tunnel(UUID_H, UUID_A)]]))
bus.publish({ scope: { kind: 'host', hostId: UUID_H2 }, at: 1, reason: 'x' })
await new Promise((r) => setTimeout(r, 0))
expect(drainHost).not.toHaveBeenCalled()
})
test('account scope tears down only that accounts hosts (sibling account keeps flowing, INV1)', async () => {
const { bus, drainHost } = setup(
new Map([
[UUID_H, tunnel(UUID_H, UUID_A)],
[UUID_H2, tunnel(UUID_H2, 'other-acct')],
]),
)
bus.publish({ scope: { kind: 'account', accountId: UUID_A }, at: 1, reason: 'x' })
await new Promise((r) => setTimeout(r, 0))
expect(drainHost).toHaveBeenCalledTimes(1)
expect(drainHost).toHaveBeenCalledWith(UUID_H, DRAIN_REASON.revoked)
})
test('global scope tears down every live tunnel', async () => {
const { bus, drainHost } = setup(
new Map([
[UUID_H, tunnel(UUID_H, UUID_A)],
[UUID_H2, tunnel(UUID_H2, 'other-acct')],
]),
)
bus.publish({ scope: { kind: 'global' }, at: 1, reason: 'x' })
await new Promise((r) => setTimeout(r, 0))
expect(drainHost).toHaveBeenCalledTimes(2)
})
test('malformed signal → dropped + counted, no teardown, subscriber stays alive', async () => {
const { bus, drainHost, onDropped } = setup(new Map([[UUID_H, tunnel(UUID_H, UUID_A)]]))
bus.publish('{ not valid json')
bus.publish(JSON.stringify({ scope: { kind: 'nonsense' }, at: 1, reason: 'x' }))
await new Promise((r) => setTimeout(r, 0))
expect(onDropped).toHaveBeenCalledTimes(2)
expect(drainHost).not.toHaveBeenCalled()
// Still alive: a good signal after the poison ones is applied.
bus.publish({ scope: { kind: 'host', hostId: UUID_H }, at: 2, reason: 'x' })
await new Promise((r) => setTimeout(r, 0))
expect(drainHost).toHaveBeenCalledTimes(1)
})
test('onApplied reports metadata only (count + elapsed), never the reason as payload (INV10)', async () => {
const { bus, onApplied } = setup(new Map([[UUID_H, tunnel(UUID_H, UUID_A)]]))
bus.publish({ scope: { kind: 'host', hostId: UUID_H }, at: 1, reason: 'super-secret-reason' })
await new Promise((r) => setTimeout(r, 0))
expect(onApplied).toHaveBeenCalledTimes(1)
const [, hostsAffected, elapsed] = onApplied.mock.calls[0]!
expect(hostsAffected).toBe(1)
expect(typeof elapsed).toBe('number')
})
test('close() unsubscribes cleanly', () => {
const bus = fakeBus()
const handle = startRevocationSubscriber({
subscribe: bus.subscribe,
drainHost: vi.fn(async () => {}),
tunnels: () => new Map(),
now: () => 0,
})
handle.close()
expect(bus.sub.close).toHaveBeenCalledTimes(1)
})
})

View File

@@ -0,0 +1,67 @@
import { describe, test, expect } from 'vitest'
import {
initialStreamState,
nextStreamState,
isTerminal,
type StreamState,
type StreamTransition,
} from '../mux/stream.js'
function legal(t: StreamTransition): StreamState {
if ('illegal' in t) throw new Error('expected legal transition')
return t.next
}
describe('stream state machine (T3, §4.1 lifecycle)', () => {
test('legal path: idle --OPEN--> open --DATA*--> open --CLOSE--> closed', () => {
let s = initialStreamState()
expect(s).toBe('idle')
s = legal(nextStreamState(s, 'open', false, 'outbound'))
expect(s).toBe('open')
s = legal(nextStreamState(s, 'data', false, 'outbound'))
s = legal(nextStreamState(s, 'data', false, 'inbound'))
expect(s).toBe('open')
s = legal(nextStreamState(s, 'close', true, 'outbound'))
expect(s).toBe('closed')
expect(isTerminal(s)).toBe(true)
})
test('DATA with fin half-closes the SENDING direction; both-side FIN ⇒ closed', () => {
const outFin = legal(nextStreamState('open', 'data', true, 'outbound'))
expect(outFin).toBe('halfClosedLocal')
const inFin = legal(nextStreamState('open', 'data', true, 'inbound'))
expect(inFin).toBe('halfClosedRemote')
// The other side FINs ⇒ fully closed.
expect(legal(nextStreamState('halfClosedLocal', 'data', true, 'inbound'))).toBe('closed')
expect(legal(nextStreamState('halfClosedRemote', 'data', true, 'outbound'))).toBe('closed')
})
test('illegal: DATA before OPEN', () => {
expect(nextStreamState('idle', 'data', false, 'inbound')).toEqual({ illegal: true })
})
test('illegal: any frame after closed', () => {
for (const t of ['data', 'close', 'windowUpdate', 'open'] as const) {
expect(nextStreamState('closed', t, false, 'inbound')).toEqual({ illegal: true })
}
})
test('illegal: OPEN on an already-open stream', () => {
expect(nextStreamState('open', 'open', false, 'inbound')).toEqual({ illegal: true })
})
test('illegal: CLOSE before OPEN', () => {
expect(nextStreamState('idle', 'close', false, 'inbound')).toEqual({ illegal: true })
})
test('WINDOW_UPDATE keeps state and is legal while flowing', () => {
expect(legal(nextStreamState('open', 'windowUpdate', false, 'inbound'))).toBe('open')
expect(legal(nextStreamState('halfClosedLocal', 'windowUpdate', false, 'inbound'))).toBe(
'halfClosedLocal',
)
})
test('illegal: re-FIN on the already-closed direction', () => {
expect(nextStreamState('halfClosedLocal', 'data', true, 'outbound')).toEqual({ illegal: true })
})
})

View File

@@ -0,0 +1,38 @@
import { describe, test, expect } from 'vitest'
import { extractSubdomain } from '../data-plane/subdomain-router.js'
const BASE = 'term.example.com'
describe('extractSubdomain (T7, INV1 Host-confusion guard)', () => {
test('single-label tenant resolves to the leftmost label', () => {
expect(extractSubdomain('alice.term.example.com', BASE)).toBe('alice')
})
test('bare base domain → null (no tenant)', () => {
expect(extractSubdomain('term.example.com', BASE)).toBeNull()
})
test('suffix-confusion attempt → null', () => {
expect(extractSubdomain('alice.term.example.com.evil.com', BASE)).toBeNull()
})
test('multi-label (nested) subdomain → null (single-label tenant only)', () => {
expect(extractSubdomain('a.b.term.example.com', BASE)).toBeNull()
})
test('case-insensitive host', () => {
expect(extractSubdomain('ALICE.Term.Example.COM', BASE)).toBe('alice')
})
test('trailing dot tolerated', () => {
expect(extractSubdomain('alice.term.example.com.', BASE)).toBe('alice')
})
test('host:port suffix is stripped before matching', () => {
expect(extractSubdomain('alice.term.example.com:8443', BASE)).toBe('alice')
})
test('invalid DNS label → null', () => {
expect(extractSubdomain('_bad.term.example.com', BASE)).toBeNull()
})
})

View File

@@ -0,0 +1,124 @@
import { describe, test, expect, vi } from 'vitest'
import { APP_SUBPROTOCOL, encodeTokenSubprotocol } from 'relay-contracts'
import {
authorizeUpgrade,
extractCapabilityToken,
TOKEN_COOKIE_NAME,
type UpgradeRequest,
type AuthorizeDeps,
} from '../data-plane/upgrade.js'
import type { Authorizer, AuthzOutcome } from '../data-plane/authz-port.js'
import type { RouteResolver, ResolvedHost } from '../data-plane/subdomain-router.js'
const RAW_TOKEN = 'cap-token-abc123'
const RESOLVED: ResolvedHost = { hostId: 'host-uuid-1', accountId: 'acct-1', subdomain: 'alice' }
function resolver(overrides?: Partial<Record<string, ResolvedHost | null>>): RouteResolver {
return {
resolveSubdomain: async (sub) => {
if (overrides && sub in overrides) return overrides[sub] ?? null
return sub === 'alice' ? RESOLVED : null
},
}
}
function okAuthorizer(): Authorizer {
const ok: AuthzOutcome = { ok: true, hostId: RESOLVED.hostId, principal: 'p-1', jti: 'jti-9' }
return { onUpgrade: vi.fn(async () => ok), onReattach: vi.fn(async () => ok) }
}
function baseReq(over?: Partial<UpgradeRequest>): UpgradeRequest {
return {
host: 'alice.term.example.com',
origin: 'https://alice.term.example.com',
url: '/term?join=x',
subprotocols: [APP_SUBPROTOCOL, encodeTokenSubprotocol(RAW_TOKEN)],
cookies: {},
remoteAddr: '203.0.113.7',
dpop: { proof: 'proof', publicKeyThumbprint: 'jkt' },
activeSessionCount: 2,
...over,
}
}
function deps(authorizer: Authorizer, r: RouteResolver = resolver()): AuthorizeDeps {
return {
authorizer,
resolver: r,
baseDomain: 'term.example.com',
now: () => 1000,
remoteAddrSalt: 'salt',
requiredRight: 'attach',
}
}
describe('authorizeUpgrade — thin adapter over P5 (T8, FIX 6a)', () => {
test('happy: delegates to onUpgrade with a fully-populated ctx; maps to {ok:true}', async () => {
const auth = okAuthorizer()
const d = deps(auth)
const res = await authorizeUpgrade(baseReq(), d)
expect(res.ok).toBe(true)
if (!res.ok) return
expect(res.hostId).toBe(RESOLVED.hostId)
expect(res.open.subdomain).toBe('alice')
expect(res.open.capabilityTokenRef).toBe('jti-9')
expect(res.acceptedSubprotocol).toBe(APP_SUBPROTOCOL)
expect(auth.onUpgrade).toHaveBeenCalledTimes(1)
const ctx = (auth.onUpgrade as ReturnType<typeof vi.fn>).mock.calls[0]![0]
expect(ctx.expectedAud).toBe('alice')
expect(ctx.requestedHostId).toBe(RESOLVED.hostId)
expect(ctx.requiredRight).toBe('attach')
expect(ctx.capabilityRaw).toBe(RAW_TOKEN) // decoded from the subprotocol carrier
expect(ctx.dpop).toEqual({ proof: 'proof', publicKeyThumbprint: 'jkt' })
expect(ctx.activeSessionCount).toBe(2)
expect(ctx.principal).toBeNull() // P5 resolves identity from the signed token (INV3)
})
test('P5 deny is passed through verbatim; no independent allow branch', async () => {
const deny: AuthzOutcome = { ok: false, status: 401 }
const auth: Authorizer = { onUpgrade: vi.fn(async () => deny), onReattach: vi.fn(async () => deny) }
const res = await authorizeUpgrade(baseReq(), deps(auth))
expect(res).toEqual({ ok: false, status: 401 })
})
test('reattach: sessionId present → onReattach is called (not onUpgrade)', async () => {
const auth = okAuthorizer()
await authorizeUpgrade(baseReq({ sessionId: 'sess-1' }), deps(auth))
expect(auth.onReattach).toHaveBeenCalledTimes(1)
expect(auth.onUpgrade).not.toHaveBeenCalled()
const ctx = (auth.onReattach as ReturnType<typeof vi.fn>).mock.calls[0]![0]
expect(ctx.sessionId).toBe('sess-1')
})
test('token via cookie fallback is accepted', async () => {
const auth = okAuthorizer()
const req = baseReq({ subprotocols: [APP_SUBPROTOCOL], cookies: { [TOKEN_COOKIE_NAME]: RAW_TOKEN } })
const res = await authorizeUpgrade(req, deps(auth))
expect(res.ok).toBe(true)
const via = extractCapabilityToken(req)
expect(via).toEqual({ token: RAW_TOKEN, via: 'cookie' })
})
test('INV15: a token ONLY in the query string → 401 and the authorizer is NEVER called', async () => {
const auth = okAuthorizer()
const req = baseReq({ url: `/term?cap=${RAW_TOKEN}`, subprotocols: [APP_SUBPROTOCOL], cookies: {} })
const res = await authorizeUpgrade(req, deps(auth))
expect(res).toEqual({ ok: false, status: 401 })
expect(auth.onUpgrade).not.toHaveBeenCalled()
expect(extractCapabilityToken(req)).toBeNull() // never reads req.url
})
test('local parse: unparseable Host → 401 without calling the authorizer', async () => {
const auth = okAuthorizer()
const res = await authorizeUpgrade(baseReq({ host: 'term.example.com' }), deps(auth))
expect(res).toEqual({ ok: false, status: 401 })
expect(auth.onUpgrade).not.toHaveBeenCalled()
})
test('local parse: unknown subdomain (resolver null) → 403 without calling the authorizer', async () => {
const auth = okAuthorizer()
const res = await authorizeUpgrade(baseReq({ host: 'bob.term.example.com' }), deps(auth))
expect(res).toEqual({ ok: false, status: 403 })
expect(auth.onUpgrade).not.toHaveBeenCalled()
})
})