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:
Yaojia Wang
2026-07-06 16:13:34 +02:00
parent 95b9cccf07
commit aa1912b962
40 changed files with 4783 additions and 19 deletions

View File

@@ -0,0 +1,59 @@
/**
* A2 — unit tests for the ioredis → RedisPublisher adapter (boot/redis.ts). The adapter is the only
* non-trivial logic in the server entrypoint's Redis wiring: it must forward publish(channel, message)
* verbatim to the underlying client and surface the driver's subscriber-count result unchanged. The
* live `createRedisClient` (a thin `new Redis(url)`) needs a real broker and is covered by the boot
* smoke, not here.
*/
import { describe, test, expect } from 'vitest'
import type { Redis } from 'ioredis'
import { createRedisPublisher } from '../src/boot/redis.js'
import { createRedisRevocationBus } from '../src/routing/bus.js'
import { RELAY_REVOCATIONS_CHANNEL, type KillSignal } from 'relay-contracts'
/** Minimal ioredis stand-in recording publish calls; cast to Redis at the seam (only `publish` is used). */
function fakeRedis(returnValue = 1): { calls: Array<[string, string]>; client: Redis } {
const calls: Array<[string, string]> = []
const client = {
publish: async (channel: string, message: string): Promise<number> => {
calls.push([channel, message])
return returnValue
},
} as unknown as Redis
return { calls, client }
}
describe('A2 createRedisPublisher (RedisPublisher adapter)', () => {
test('forwards channel + message verbatim and returns the driver subscriber count', async () => {
// Arrange
const { calls, client } = fakeRedis(3)
const publisher = createRedisPublisher(client)
// Act
const receivers = await publisher.publish('relay:revocations', 'payload')
// Assert
expect(receivers).toBe(3)
expect(calls).toEqual([['relay:revocations', 'payload']])
})
test('drives createRedisRevocationBus onto the FROZEN relay:revocations channel with JSON payload', async () => {
// Arrange
const { calls, client } = fakeRedis()
const bus = createRedisRevocationBus(createRedisPublisher(client))
const signal: KillSignal = {
scope: { kind: 'host', hostId: '11111111-1111-4111-8111-111111111111' },
at: 1_700_000_000,
reason: 'revoked',
}
// Act
await bus.publish(signal)
// Assert — bus publishes exactly one message on the shared channel, JSON-encoded.
expect(calls).toHaveLength(1)
const [channel, message] = calls[0]!
expect(channel).toBe(RELAY_REVOCATIONS_CHANNEL)
expect(JSON.parse(message)).toMatchObject({ scope: { kind: 'host', hostId: signal.scope.kind === 'host' ? signal.scope.hostId : '' } })
})
})