Files
web-terminal/relay-contracts/test/model.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

131 lines
3.8 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
AccountRecordSchema,
HostRecordSchema,
KillSignalSchema,
RELAY_REVOCATIONS_CHANNEL,
REVOCATION_PUSH_BUDGET_MS,
RevocationScopeSchema,
RouteEntrySchema,
SessionRecordSchema,
INV8_VERSION_TABLE_DDL,
HOST_VERSIONS_DDL,
HOSTS_CURRENT_DDL,
} from '../src/index.js'
const UUID = '11111111-1111-4111-8111-111111111111'
const TS = '2026-07-01T00:00:00.000Z'
describe('§4.2 account/host/session Zod schemas', () => {
it('accepts a valid HostRecord', () => {
const rec = {
hostId: UUID,
accountId: UUID,
subdomain: 'alice',
agentPubkey: new Uint8Array([1, 2, 3]),
enrollFpr: 'fpr:abc',
status: 'online',
lastSeen: TS,
createdAt: TS,
revokedAt: null,
}
expect(HostRecordSchema.parse(rec)).toEqual(rec)
})
it('rejects a HostRecord with an invalid status', () => {
expect(() =>
HostRecordSchema.parse({
hostId: UUID,
accountId: UUID,
subdomain: 'a',
agentPubkey: new Uint8Array(),
enrollFpr: 'f',
status: 'zombie',
lastSeen: TS,
createdAt: TS,
revokedAt: null,
}),
).toThrow()
})
it('rejects a HostRecord with a non-UUID hostId', () => {
const bad = {
hostId: 'not-a-uuid',
accountId: UUID,
subdomain: 'a',
agentPubkey: new Uint8Array(),
enrollFpr: 'f',
status: 'online',
lastSeen: TS,
createdAt: TS,
revokedAt: null,
}
expect(HostRecordSchema.safeParse(bad).success).toBe(false)
})
it('rejects unknown extra keys (strict)', () => {
expect(
AccountRecordSchema.safeParse({
accountId: UUID,
plan: 'pro',
createdAt: TS,
status: 'active',
extra: 1,
}).success,
).toBe(false)
})
it('accepts a valid AccountRecord across all plan tiers', () => {
for (const plan of ['free', 'personal', 'pro', 'team'] as const) {
expect(AccountRecordSchema.parse({ accountId: UUID, plan, createdAt: TS, status: 'active' }).plan).toBe(
plan,
)
}
})
it('validates SessionRecord and RouteEntry', () => {
expect(
SessionRecordSchema.parse({
sessionId: UUID,
hostId: UUID,
accountId: UUID,
createdAt: TS,
lastAttachAt: TS,
}).sessionId,
).toBe(UUID)
expect(RouteEntrySchema.parse({ relayNodeId: 'node-1', updatedAt: TS }).relayNodeId).toBe('node-1')
})
})
describe('§4.2 FIX 4 revocation teardown shapes', () => {
it('exposes the frozen channel + budget constants', () => {
expect(RELAY_REVOCATIONS_CHANNEL).toBe('relay:revocations')
expect(REVOCATION_PUSH_BUDGET_MS).toBe(2000)
})
it('accepts each RevocationScope variant', () => {
expect(RevocationScopeSchema.parse({ kind: 'host', hostId: UUID }).kind).toBe('host')
expect(RevocationScopeSchema.parse({ kind: 'account', accountId: UUID }).kind).toBe('account')
expect(RevocationScopeSchema.parse({ kind: 'global' }).kind).toBe('global')
})
it('rejects a host scope missing hostId', () => {
expect(RevocationScopeSchema.safeParse({ kind: 'host' }).success).toBe(false)
})
it('validates a full KillSignal', () => {
const signal = { scope: { kind: 'global' }, at: 1719800000, reason: 'operator drain' }
expect(KillSignalSchema.parse(signal)).toEqual(signal)
})
})
describe('§4.2 INV8 version-table DDL contract', () => {
it('exposes host_versions + hosts_current DDL in order', () => {
expect(INV8_VERSION_TABLE_DDL).toEqual([HOST_VERSIONS_DDL, HOSTS_CURRENT_DDL])
expect(HOST_VERSIONS_DDL).toContain('host_versions')
expect(HOST_VERSIONS_DDL).toContain('supersedes')
expect(HOSTS_CURRENT_DDL).toContain('hosts_current')
expect(HOSTS_CURRENT_DDL).toContain('version_id')
})
})