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.
84 lines
2.9 KiB
TypeScript
84 lines
2.9 KiB
TypeScript
/**
|
|
* Big-endian (network order) integer read/write helpers shared by every binary codec.
|
|
* Kept dependency-free (no Buffer) so the browser bundle (P6) imports it unchanged.
|
|
*/
|
|
import { ContractDecodeError, ContractEncodeError } from './errors.js'
|
|
|
|
export const U32_MAX = 0xffff_ffff
|
|
export const U64_MAX = 0xffff_ffff_ffff_ffffn
|
|
|
|
/** Assert a value is a non-negative integer that fits in uint32; throw otherwise. */
|
|
export function assertUint32(value: number, field: string): void {
|
|
if (!Number.isInteger(value) || value < 0 || value > U32_MAX) {
|
|
throw new ContractEncodeError(`${field} must be a uint32 (0..${U32_MAX}); got ${value}`)
|
|
}
|
|
}
|
|
|
|
/** Write a uint32 big-endian into `out` at `offset`. Returns the next offset. */
|
|
export function writeUint32BE(out: Uint8Array, offset: number, value: number): number {
|
|
assertUint32(value, 'uint32')
|
|
out[offset] = (value >>> 24) & 0xff
|
|
out[offset + 1] = (value >>> 16) & 0xff
|
|
out[offset + 2] = (value >>> 8) & 0xff
|
|
out[offset + 3] = value & 0xff
|
|
return offset + 4
|
|
}
|
|
|
|
/** Read a uint32 big-endian from `buf` at `offset`. */
|
|
export function readUint32BE(buf: Uint8Array, offset: number): number {
|
|
if (offset + 4 > buf.length) {
|
|
throw new ContractDecodeError(`truncated uint32 at offset ${offset} (len ${buf.length})`)
|
|
}
|
|
return (
|
|
((buf[offset]! << 24) >>> 0) +
|
|
(buf[offset + 1]! << 16) +
|
|
(buf[offset + 2]! << 8) +
|
|
buf[offset + 3]!
|
|
)
|
|
}
|
|
|
|
/** Write a uint64 big-endian (from a bigint) into `out` at `offset`. Returns next offset. */
|
|
export function writeUint64BE(out: Uint8Array, offset: number, value: bigint): number {
|
|
if (value < 0n || value > U64_MAX) {
|
|
throw new ContractEncodeError(`uint64 out of range: ${value}`)
|
|
}
|
|
for (let i = 7; i >= 0; i--) {
|
|
out[offset + i] = Number(value & 0xffn)
|
|
value >>= 8n
|
|
}
|
|
return offset + 8
|
|
}
|
|
|
|
/** Read a uint64 big-endian as a bigint from `buf` at `offset`. */
|
|
export function readUint64BE(buf: Uint8Array, offset: number): bigint {
|
|
if (offset + 8 > buf.length) {
|
|
throw new ContractDecodeError(`truncated uint64 at offset ${offset} (len ${buf.length})`)
|
|
}
|
|
let value = 0n
|
|
for (let i = 0; i < 8; i++) {
|
|
value = (value << 8n) | BigInt(buf[offset + i]!)
|
|
}
|
|
return value
|
|
}
|
|
|
|
/** Read a uint64 that must fit in a JS safe integer (§4.1 payloadLen safe-int guard). */
|
|
export function readSafeUint64BE(buf: Uint8Array, offset: number, field: string): number {
|
|
const value = readUint64BE(buf, offset)
|
|
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
throw new ContractDecodeError(`${field} exceeds Number.MAX_SAFE_INTEGER: ${value}`)
|
|
}
|
|
return Number(value)
|
|
}
|
|
|
|
/** Concatenate byte chunks into one Uint8Array. */
|
|
export function concatBytes(chunks: readonly Uint8Array[]): Uint8Array {
|
|
const total = chunks.reduce((n, c) => n + c.length, 0)
|
|
const out = new Uint8Array(total)
|
|
let offset = 0
|
|
for (const c of chunks) {
|
|
out.set(c, offset)
|
|
offset += c.length
|
|
}
|
|
return out
|
|
}
|