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).
266 lines
10 KiB
TypeScript
266 lines
10 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import type { AuditEvent } from 'relay-auth'
|
|
import { NO_STEPUP_POLICY } from 'relay-auth/src/human/stepup/stepup.js'
|
|
import type { QueryFn } from 'control-plane/src/db/pool.js'
|
|
import { createRelayEnforceDeps, type RedisLike } from '../../src/wiring/stores-pg.js'
|
|
|
|
// ── Fakes ─────────────────────────────────────────────────────────────────────────────────────
|
|
|
|
interface QueryCall {
|
|
readonly sql: string
|
|
readonly params: readonly unknown[]
|
|
}
|
|
|
|
/** A `QueryFn` that records every call and returns rows chosen by `rowsFor`. */
|
|
function makeFakeQuery(
|
|
rowsFor: (sql: string, params: readonly unknown[]) => unknown[],
|
|
): { query: QueryFn; calls: QueryCall[] } {
|
|
const calls: QueryCall[] = []
|
|
const query: QueryFn = async <T>(sql: string, params: readonly unknown[]): Promise<readonly T[]> => {
|
|
calls.push({ sql, params })
|
|
return rowsFor(sql, params) as T[]
|
|
}
|
|
return { query, calls }
|
|
}
|
|
|
|
interface RedisCall {
|
|
readonly method: string
|
|
readonly args: readonly unknown[]
|
|
}
|
|
|
|
/** A `RedisLike` mock whose methods can be overridden per test; every call is recorded. */
|
|
function makeMockRedis(over: Partial<RedisLike> = {}): RedisLike & { calls: RedisCall[] } {
|
|
const calls: RedisCall[] = []
|
|
const record = (method: string, args: unknown[]): void => void calls.push({ method, args })
|
|
const redis: RedisLike = {
|
|
exists: over.exists ?? (async (key) => (record('exists', [key]), 0)),
|
|
set: over.set ?? (async (key, value, mode) => (record('set', [key, value, mode]), 'OK')),
|
|
expireat: over.expireat ?? (async (key, ts) => (record('expireat', [key, ts]), 1)),
|
|
eval: over.eval ?? (async (...args) => (record('eval', args), 1)),
|
|
}
|
|
return Object.assign(redis, { calls })
|
|
}
|
|
|
|
const NOOP_QUERY: QueryFn = async () => []
|
|
|
|
// ── hosts (Postgres pass-through) ───────────────────────────────────────────────────────────────
|
|
|
|
describe('createRelayEnforceDeps.hosts', () => {
|
|
it('maps a CP host row to a relay-auth HostRecord', async () => {
|
|
const { query, calls } = makeFakeQuery(() => [
|
|
{
|
|
host_id: 'host-1',
|
|
account_id: 'acct-1',
|
|
subdomain: 'demo',
|
|
agent_pubkey: Buffer.from([1, 2, 3]),
|
|
enroll_fpr: 'fpr-1',
|
|
status: 'online',
|
|
last_seen: '2026-07-06T00:00:00.000Z',
|
|
created_at: '2026-07-05T00:00:00.000Z',
|
|
revoked_at: null,
|
|
},
|
|
])
|
|
const deps = createRelayEnforceDeps({ query, redis: makeMockRedis() })
|
|
|
|
const host = await deps.hosts.getById('host-1')
|
|
|
|
expect(host).not.toBeNull()
|
|
expect(host?.hostId).toBe('host-1')
|
|
expect(host?.accountId).toBe('acct-1')
|
|
expect(host?.subdomain).toBe('demo')
|
|
expect(host?.status).toBe('online')
|
|
expect(host?.revokedAt).toBeNull()
|
|
expect(Array.from(host?.agentPubkey ?? [])).toEqual([1, 2, 3])
|
|
// accountId came from the stored row, never fabricated (INV3).
|
|
expect(calls[0]?.params).toEqual(['host-1'])
|
|
})
|
|
|
|
it('returns null for an unknown host', async () => {
|
|
const { query } = makeFakeQuery(() => [])
|
|
const deps = createRelayEnforceDeps({ query, redis: makeMockRedis() })
|
|
expect(await deps.hosts.getById('nope')).toBeNull()
|
|
})
|
|
})
|
|
|
|
// ── sessions (Postgres, remapped to the port shape) ──────────────────────────────────────────────
|
|
|
|
describe('createRelayEnforceDeps.sessions', () => {
|
|
it('maps a session row to { hostId, accountId }', async () => {
|
|
const { query } = makeFakeQuery(() => [
|
|
{
|
|
session_id: 'sess-1',
|
|
host_id: 'host-1',
|
|
account_id: 'acct-1',
|
|
created_at: '2026-07-06T00:00:00.000Z',
|
|
last_attach_at: '2026-07-06T00:00:00.000Z',
|
|
},
|
|
])
|
|
const deps = createRelayEnforceDeps({ query, redis: makeMockRedis() })
|
|
|
|
expect(await deps.sessions.getById('sess-1')).toEqual({ hostId: 'host-1', accountId: 'acct-1' })
|
|
})
|
|
|
|
it('returns null for an unknown session', async () => {
|
|
const { query } = makeFakeQuery(() => [])
|
|
const deps = createRelayEnforceDeps({ query, redis: makeMockRedis() })
|
|
expect(await deps.sessions.getById('nope')).toBeNull()
|
|
})
|
|
})
|
|
|
|
// ── revocation (Redis) ───────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('createRelayEnforceDeps.revocation', () => {
|
|
it('isRevoked is true iff the revoked:<jti> key exists', async () => {
|
|
const present = createRelayEnforceDeps({
|
|
query: NOOP_QUERY,
|
|
redis: makeMockRedis({ exists: async () => 1 }),
|
|
})
|
|
const absent = createRelayEnforceDeps({
|
|
query: NOOP_QUERY,
|
|
redis: makeMockRedis({ exists: async () => 0 }),
|
|
})
|
|
expect(await present.revocation.isRevoked('j1')).toBe(true)
|
|
expect(await absent.revocation.isRevoked('j1')).toBe(false)
|
|
})
|
|
|
|
it('revokeJti sets revoked:<jti> and pins its expiry to exp', async () => {
|
|
const redis = makeMockRedis()
|
|
const deps = createRelayEnforceDeps({ query: NOOP_QUERY, redis })
|
|
|
|
await deps.revocation.revokeJti('j1', 1_800_000_000)
|
|
|
|
expect(redis.calls).toEqual([
|
|
{ method: 'set', args: ['revoked:j1', '1', undefined] },
|
|
{ method: 'expireat', args: ['revoked:j1', 1_800_000_000] },
|
|
])
|
|
})
|
|
|
|
it('consumeOnce burns the jti: first use wins (SET NX), replays lose', async () => {
|
|
let existing = false
|
|
const redis = makeMockRedis({
|
|
set: async (_k, _v, mode) => {
|
|
if (mode !== 'NX') return 'OK'
|
|
if (existing) return null
|
|
existing = true
|
|
return 'OK'
|
|
},
|
|
})
|
|
const deps = createRelayEnforceDeps({ query: NOOP_QUERY, redis })
|
|
|
|
expect(await deps.revocation.consumeOnce('j1', 1_800_000_000)).toBe(true)
|
|
expect(await deps.revocation.consumeOnce('j1', 1_800_000_000)).toBe(false)
|
|
// expiry is only pinned on the winning first use.
|
|
expect(redis.calls.filter((c) => c.method === 'expireat')).toEqual([
|
|
{ method: 'expireat', args: ['used:j1', 1_800_000_000] },
|
|
])
|
|
})
|
|
})
|
|
|
|
// ── buckets (Redis token bucket) ─────────────────────────────────────────────────────────────────
|
|
|
|
describe('createRelayEnforceDeps.buckets', () => {
|
|
it('namespaces the key and forwards refill/burst/now + a computed ttl to the Lua script', async () => {
|
|
let evalArgs: readonly unknown[] = []
|
|
const redis = makeMockRedis({
|
|
eval: async (...args) => {
|
|
evalArgs = args
|
|
return 1
|
|
},
|
|
})
|
|
const deps = createRelayEnforceDeps({ query: NOOP_QUERY, redis })
|
|
|
|
const ok = await deps.buckets.take('connect:acct:a1', 1, 60, 1000)
|
|
|
|
expect(ok).toBe(true)
|
|
// eval(script, numKeys, key, refillPerSec, burst, now, ttl)
|
|
expect(evalArgs[1]).toBe(1) // numKeys
|
|
expect(evalArgs[2]).toBe('bucket:connect:acct:a1')
|
|
expect(evalArgs[3]).toBe(1) // refillPerSec
|
|
expect(evalArgs[4]).toBe(60) // burst
|
|
expect(evalArgs[5]).toBe(1000) // now
|
|
expect(evalArgs[6]).toBe(61) // ttl = ceil(60/1) + 1
|
|
})
|
|
|
|
it('maps a 0 result to throttled (false)', async () => {
|
|
const deps = createRelayEnforceDeps({
|
|
query: NOOP_QUERY,
|
|
redis: makeMockRedis({ eval: async () => 0 }),
|
|
})
|
|
expect(await deps.buckets.take('preauth:ip:x', 1, 60, 1000)).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ── audit (Postgres audit_log, metadata only) ────────────────────────────────────────────────────
|
|
|
|
describe('createRelayEnforceDeps.audit', () => {
|
|
it('maps an AuditEvent onto the audit_log row, folding non-column fields into meta (INV10)', async () => {
|
|
const { query, calls } = makeFakeQuery(() => [])
|
|
const deps = createRelayEnforceDeps({ query, redis: makeMockRedis() })
|
|
const event: AuditEvent = {
|
|
ts: '2026-07-06T00:00:00.000Z',
|
|
action: 'attach',
|
|
principalId: 'cred-1',
|
|
accountId: 'acct-1',
|
|
hostId: 'host-1',
|
|
sessionId: 'sess-1',
|
|
jti: 'jti-1',
|
|
outcome: 'allow',
|
|
reason: 'ok',
|
|
remoteAddrHash: 'iphash',
|
|
}
|
|
|
|
await deps.audit.append(event)
|
|
|
|
const params = calls[0]?.params ?? []
|
|
expect(params[0]).toBe('attach') // action
|
|
expect(params[1]).toBe('cred-1') // principal_id
|
|
expect(params[2]).toBe('acct-1') // account_id
|
|
expect(params[3]).toBe('host-1') // host_id
|
|
expect(params[4]).toBe('2026-07-06T00:00:00.000Z') // ts
|
|
expect(JSON.parse(params[5] as string)).toEqual({
|
|
outcome: 'allow',
|
|
reason: 'ok',
|
|
remoteAddrHash: 'iphash',
|
|
sessionId: 'sess-1',
|
|
jti: 'jti-1',
|
|
})
|
|
})
|
|
|
|
it('omits null sessionId/jti from meta', async () => {
|
|
const { query, calls } = makeFakeQuery(() => [])
|
|
const deps = createRelayEnforceDeps({ query, redis: makeMockRedis() })
|
|
const event: AuditEvent = {
|
|
ts: '2026-07-06T00:00:00.000Z',
|
|
action: 'attach',
|
|
principalId: '',
|
|
accountId: '',
|
|
hostId: null,
|
|
sessionId: null,
|
|
jti: null,
|
|
outcome: 'deny',
|
|
reason: 'bad_origin',
|
|
remoteAddrHash: 'iphash',
|
|
}
|
|
|
|
await deps.audit.append(event)
|
|
|
|
expect(calls[0]?.params[3]).toBeNull() // host_id
|
|
expect(JSON.parse((calls[0]?.params[5] as string) ?? '{}')).toEqual({
|
|
outcome: 'deny',
|
|
reason: 'bad_origin',
|
|
remoteAddrHash: 'iphash',
|
|
})
|
|
})
|
|
})
|
|
|
|
// ── stepUpPolicyFor (staging) ────────────────────────────────────────────────────────────────────
|
|
|
|
describe('createRelayEnforceDeps.stepUpPolicyFor', () => {
|
|
it('returns NO_STEPUP_POLICY (staging single-operator)', () => {
|
|
const deps = createRelayEnforceDeps({ query: NOOP_QUERY, redis: makeMockRedis() })
|
|
// host argument is irrelevant in staging; the policy is never-required.
|
|
expect(deps.stepUpPolicyFor({} as never)).toBe(NO_STEPUP_POLICY)
|
|
expect(deps.stepUpPolicyFor({} as never).required).toBe(false)
|
|
})
|
|
})
|