feat(relay): Phase1 waves A2-E — server entry, shared-store data plane, agent runtime, deploy artifacts
RELAY-PHASE1 Wave A2/B/C/D/E (12-agent workflow, all tsc-clean, 314/314 tests pass): - A2: control-plane server.ts entry + boot/redis.ts revocation-bus wiring + start script. - B1: relay-run shared-store EnforceDeps (relay-auth ports over the SAME Postgres+Redis as P3). - B2: registry-backed MtlsVerifier (verifyAgentCert, fail-closed, INV14). - B3: store-backed RouteResolver (subdomain->hostId). - B4: Redis relay:revocations subscriber -> tunnel teardown (INV12). - B5: main-phase1.ts production entry (public bind, real TLS, async-mTLS prefetch bridge) + staging /auth/mint. - B6 (PARTIAL): relay-web operator login + browser DPoP; proof offered via term.dpop.<b64u> subprotocol. - C: agent dist/cli.js build (esbuild) + runTunnel run-loop + CliDeps. - D1: same-origin static serve of relay-web/public from the browser WSS. - E: systemd units + gen-ca/gen-capability-key/issue-tls-cert scripts + deploy/RUNBOOK.md. Adversarial review: all hard invariants PASS. Follow-ups (B7): close DPoP-subprotocol read on browser-server (blocks browser connect); rate-limit /auth/mint (F1); wire activeSessionCount (F2); scrub error logs (F5). Excludes unrelated public/style.css (concurrent iOS job).
This commit is contained in:
128
relay-run/tests/wiring/route-resolver.test.ts
Normal file
128
relay-run/tests/wiring/route-resolver.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { HostRecord, HostStatus } from 'control-plane/src/model/records.js'
|
||||
import {
|
||||
createStoreRouteResolver,
|
||||
type HostSubdomainLookup,
|
||||
} from '../../src/wiring/route-resolver.js'
|
||||
|
||||
const NOW_ISO = '2026-07-06T00:00:00.000Z'
|
||||
|
||||
/** Build a full §4.2 HostRecord fixture keyed by its subdomain label. */
|
||||
function makeHost(subdomain: string, status: HostStatus = 'online'): HostRecord {
|
||||
return {
|
||||
hostId: randomUUID(),
|
||||
accountId: randomUUID(),
|
||||
subdomain,
|
||||
agentPubkey: new Uint8Array([1, 2, 3]),
|
||||
enrollFpr: 'fpr:' + subdomain,
|
||||
status,
|
||||
lastSeen: NOW_ISO,
|
||||
createdAt: NOW_ISO,
|
||||
revokedAt: status === 'revoked' ? NOW_ISO : null,
|
||||
}
|
||||
}
|
||||
|
||||
/** A minimal HostStore lookup fake: exact-match subdomain → record, else null (matches pg.ts). */
|
||||
function fakeHosts(records: readonly HostRecord[]): HostSubdomainLookup {
|
||||
const bySub = new Map(records.map((r) => [r.subdomain, r]))
|
||||
return {
|
||||
getBySubdomain: vi.fn(async (subdomain: string) => bySub.get(subdomain) ?? null),
|
||||
}
|
||||
}
|
||||
|
||||
describe('createStoreRouteResolver', () => {
|
||||
it('resolves a known online host to {hostId, accountId, subdomain}', async () => {
|
||||
// Arrange
|
||||
const host = makeHost('alice')
|
||||
const resolver = createStoreRouteResolver({ hosts: fakeHosts([host]) })
|
||||
|
||||
// Act
|
||||
const resolved = await resolver.resolveSubdomain('alice')
|
||||
|
||||
// Assert
|
||||
expect(resolved).toEqual({
|
||||
hostId: host.hostId,
|
||||
accountId: host.accountId,
|
||||
subdomain: 'alice',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null (fail-closed) for an unknown subdomain', async () => {
|
||||
// Arrange
|
||||
const resolver = createStoreRouteResolver({ hosts: fakeHosts([makeHost('alice')]) })
|
||||
|
||||
// Act
|
||||
const resolved = await resolver.resolveSubdomain('nobody')
|
||||
|
||||
// Assert
|
||||
expect(resolved).toBeNull()
|
||||
})
|
||||
|
||||
it('fails closed (returns null) for a revoked host even though the row still exists', async () => {
|
||||
// Arrange
|
||||
const resolver = createStoreRouteResolver({ hosts: fakeHosts([makeHost('alice', 'revoked')]) })
|
||||
|
||||
// Act
|
||||
const resolved = await resolver.resolveSubdomain('alice')
|
||||
|
||||
// Assert
|
||||
expect(resolved).toBeNull()
|
||||
})
|
||||
|
||||
it('resolves offline and draining hosts (only revoked fails closed)', async () => {
|
||||
// Arrange
|
||||
const offline = makeHost('bob', 'offline')
|
||||
const draining = makeHost('carol', 'draining')
|
||||
const resolver = createStoreRouteResolver({ hosts: fakeHosts([offline, draining]) })
|
||||
|
||||
// Act + Assert
|
||||
expect(await resolver.resolveSubdomain('bob')).toEqual({
|
||||
hostId: offline.hostId,
|
||||
accountId: offline.accountId,
|
||||
subdomain: 'bob',
|
||||
})
|
||||
expect(await resolver.resolveSubdomain('carol')).toEqual({
|
||||
hostId: draining.hostId,
|
||||
accountId: draining.accountId,
|
||||
subdomain: 'carol',
|
||||
})
|
||||
})
|
||||
|
||||
it('derives identity only from the store record, not the caller (INV3)', async () => {
|
||||
// Arrange: the stored record carries the authoritative accountId/hostId.
|
||||
const host = makeHost('alice')
|
||||
const resolver = createStoreRouteResolver({ hosts: fakeHosts([host]) })
|
||||
|
||||
// Act
|
||||
const resolved = await resolver.resolveSubdomain('alice')
|
||||
|
||||
// Assert: values come from the record, never fabricated from the input label.
|
||||
expect(resolved?.accountId).toBe(host.accountId)
|
||||
expect(resolved?.hostId).toBe(host.hostId)
|
||||
})
|
||||
|
||||
it('passes the exact subdomain label through to getBySubdomain', async () => {
|
||||
// Arrange
|
||||
const hosts = fakeHosts([makeHost('alice')])
|
||||
const resolver = createStoreRouteResolver({ hosts })
|
||||
|
||||
// Act
|
||||
await resolver.resolveSubdomain('alice')
|
||||
|
||||
// Assert
|
||||
expect(hosts.getBySubdomain).toHaveBeenCalledWith('alice')
|
||||
expect(hosts.getBySubdomain).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('propagates store errors (no silent swallow)', async () => {
|
||||
// Arrange: a store that throws (e.g. DB unavailable) must NOT be masked as a null resolve.
|
||||
const boom = new Error('db down')
|
||||
const resolver = createStoreRouteResolver({
|
||||
hosts: { getBySubdomain: vi.fn(async () => { throw boom }) },
|
||||
})
|
||||
|
||||
// Act + Assert
|
||||
await expect(resolver.resolveSubdomain('alice')).rejects.toThrow('db down')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user