Files
web-terminal/relay-contracts/test/mux-open-control.test.ts
Yaojia Wang 2af57e6686 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.
2026-07-02 06:10:16 +02:00

114 lines
3.5 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
ContractDecodeError,
decodeCborMap,
decodeGoAwayReason,
decodeGoaway,
decodeOpen,
decodeWindowUpdate,
encodeCborMap,
encodeGoaway,
encodeOpen,
encodeWindowUpdate,
type MuxOpen,
} from '../src/index.js'
const sampleOpen = (): MuxOpen => ({
streamId: 7,
subdomain: 'alice',
requestPath: '/term?join=abc',
originHeader: 'https://alice.term.example.com',
remoteAddrHash: 'deadbeef',
capabilityTokenRef: 'jti-123',
})
describe('§4.1 CBOR primitive', () => {
it('KAT: decodes a hand-written CBOR map {"a":"b"} from fixed bytes', () => {
const bytes = Uint8Array.of(0xa1, 0x61, 0x61, 0x61, 0x62)
expect(decodeCborMap(bytes)).toEqual({ a: 'b' })
})
it('KAT: encodes {a:1} to fixed bytes (uint value)', () => {
const bytes = encodeCborMap({ a: 1 }, ['a'])
expect([...bytes]).toEqual([0xa1, 0x61, 0x61, 0x01])
})
it('rejects trailing bytes after the map', () => {
expect(() => decodeCborMap(Uint8Array.of(0xa1, 0x61, 0x61, 0x61, 0x62, 0x00))).toThrow(
/trailing bytes/,
)
})
it('rejects a duplicate key', () => {
// map(2) with key "a" twice
const bytes = Uint8Array.of(0xa2, 0x61, 0x61, 0x01, 0x61, 0x61, 0x02)
expect(() => decodeCborMap(bytes)).toThrow(/duplicate/)
})
})
describe('§4.1 OPEN payload (CBOR of MuxOpen)', () => {
it('round-trips MuxOpen through encode/decode (identity)', () => {
const open = sampleOpen()
expect(decodeOpen(encodeOpen(open))).toEqual(open)
})
it('is deterministic — same input yields identical bytes', () => {
expect([...encodeOpen(sampleOpen())]).toEqual([...encodeOpen(sampleOpen())])
})
it('rejects a MuxOpen with streamId 0 (reserved for connection-level control)', () => {
expect(() => encodeOpen({ ...sampleOpen(), streamId: 0 })).toThrow()
})
it('rejects a decoded payload with a missing field', () => {
const bad = encodeCborMap({ subdomain: 'x' }, ['subdomain'])
expect(() => decodeOpen(bad)).toThrow(ContractDecodeError)
})
})
describe('§4.1 WINDOW_UPDATE codec', () => {
it('KAT: encodes credit=255 to 4 big-endian bytes', () => {
expect([...encodeWindowUpdate(255)]).toEqual([0x00, 0x00, 0x00, 0xff])
})
it('round-trips a credit value', () => {
expect(decodeWindowUpdate(encodeWindowUpdate(0x0abbccdd))).toBe(0x0abbccdd)
})
it('rejects a wrong-length payload', () => {
expect(() => decodeWindowUpdate(Uint8Array.of(1, 2, 3))).toThrow(ContractDecodeError)
})
})
describe('§4.1 GOAWAY codec', () => {
it('KAT: encodes lastStreamId=10, reason=revoked to fixed bytes', () => {
expect([...encodeGoaway(10, 'revoked')]).toEqual([
0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x02,
])
})
it.each([
['operatorDrain', 1],
['revoked', 2],
['shutdown', 3],
] as const)('round-trips reason=%s (code %i)', (reason, code) => {
const bytes = encodeGoaway(1, reason)
expect([...bytes.subarray(4)]).toEqual([0x00, 0x00, 0x00, code])
expect(decodeGoaway(bytes)).toEqual({ lastStreamId: 1, reason })
})
it('decodeGoAwayReason maps frozen wire codes', () => {
expect(decodeGoAwayReason(1)).toBe('operatorDrain')
expect(decodeGoAwayReason(2)).toBe('revoked')
expect(decodeGoAwayReason(3)).toBe('shutdown')
})
it('decodeGoAwayReason throws on an unknown code', () => {
expect(() => decodeGoAwayReason(99)).toThrow(ContractDecodeError)
})
it('rejects a wrong-length GOAWAY payload', () => {
expect(() => decodeGoaway(Uint8Array.of(0, 0, 0, 1))).toThrow(ContractDecodeError)
})
})