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,158 @@
/**
* B2 tests — registry-backed MtlsVerifier (INV4/INV14, fail-closed).
*
* Two layers, mirroring relay-auth/test/mtls.test.ts:
* 1. REAL X.509 path: a self-signed root → leaf chain (relay-auth's committed mTLS fixtures) fed to
* `verifyPeer` as DER bytes, exercising the production `defaultParseX509` chain walk + DER→PEM
* conversion end-to-end against a fake host registry.
* 2. Deterministic seam: an injected `ParseCert` isolates the DER→PEM conversion + registry-gating +
* fail-closed mapping without depending on fixture contents.
*/
import { readFileSync } from 'node:fs'
import { X509Certificate } from 'node:crypto'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import { describe, it, expect } from 'vitest'
import { defaultParseX509, spiffeIdFor, type ParseCert, type ParsedCert } from 'relay-auth'
import type { HostRegistryPort } from 'relay-auth'
import { createMtlsVerifier, derToPem } from '../../src/wiring/mtls-verifier.js'
import { fakeHostRegistry, makeHostRecord } from '../../src/wiring/memory-stores.js'
// Reuse the committed real mTLS fixtures (leaf chains to ca-chain; SPIFFE account/acct-A/host/host-1).
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), '../../../relay-auth/test/fixtures/mtls')
const readFixture = (name: string): string => readFileSync(join(FIXTURES, name), 'utf8')
const leafPem = readFixture('leaf.pem')
const caChainPem = readFixture('ca-chain.pem')
const foreignLeafPem = readFixture('foreign-leaf.pem')
const leafDer = new Uint8Array(new X509Certificate(leafPem).raw)
const foreignDer = new Uint8Array(new X509Certificate(foreignLeafPem).raw)
// A time strictly inside the leaf's validity window (fixtures are long-lived; do not hardcode).
const parsedLeaf = defaultParseX509(leafPem, caChainPem)
const IN_WINDOW = parsedLeaf.notBefore + 60
const enrolledHosts = (): HostRegistryPort =>
fakeHostRegistry([makeHostRecord('acct-A', 'host-1', 'sub-host-1')])
describe('derToPem (DER → PEM)', () => {
it('wraps DER as a 64-column PEM CERTIFICATE block that round-trips back to the same DER', () => {
const pem = derToPem(leafDer)
expect(pem.startsWith('-----BEGIN CERTIFICATE-----\n')).toBe(true)
expect(pem.trimEnd().endsWith('-----END CERTIFICATE-----')).toBe(true)
// No base64 body line exceeds the PEM 64-column width.
const body = pem.split('\n').slice(1, -2)
for (const line of body) expect(line.length).toBeLessThanOrEqual(64)
// Parsing the produced PEM yields byte-identical DER.
expect(new Uint8Array(new X509Certificate(pem).raw)).toEqual(leafDer)
})
})
describe('createMtlsVerifier — real X.509 path (INV14)', () => {
it('accepts an enrolled, in-date leaf chaining to the pinned CA → {hostId, accountId}', async () => {
const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => IN_WINDOW })
expect(await v.verifyPeer(leafDer)).toEqual({ hostId: 'host-1', accountId: 'acct-A' })
})
it('refuses a leaf signed by a foreign CA not in the pinned bundle → null', async () => {
const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => IN_WINDOW })
expect(await v.verifyPeer(foreignDer)).toBeNull()
})
it('refuses a valid leaf whose host is NOT in the registry (INV4) → null', async () => {
const v = createMtlsVerifier({ caChainPem, hosts: fakeHostRegistry([]), now: () => IN_WINDOW })
expect(await v.verifyPeer(leafDer)).toBeNull()
})
it('refuses when the registry account ≠ the cert SPIFFE account → null', async () => {
const hosts = fakeHostRegistry([makeHostRecord('acct-B', 'host-1', 'sub-host-1')])
const v = createMtlsVerifier({ caChainPem, hosts, now: () => IN_WINDOW })
expect(await v.verifyPeer(leafDer)).toBeNull()
})
it('refuses a revoked host (INV12/registry gate) → null', async () => {
const hosts = fakeHostRegistry([makeHostRecord('acct-A', 'host-1', 'sub-host-1', 'revoked')])
const v = createMtlsVerifier({ caChainPem, hosts, now: () => IN_WINDOW })
expect(await v.verifyPeer(leafDer)).toBeNull()
})
it('refuses an expired leaf → null', async () => {
const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => parsedLeaf.notAfter + 100 })
expect(await v.verifyPeer(leafDer)).toBeNull()
})
it('refuses a not-yet-valid leaf → null', async () => {
const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => parsedLeaf.notBefore - 100 })
expect(await v.verifyPeer(leafDer)).toBeNull()
})
})
describe('createMtlsVerifier — fail-closed on malformed / absent input', () => {
it('returns null for an empty DER (no client cert presented)', async () => {
const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => IN_WINDOW })
expect(await v.verifyPeer(new Uint8Array(0))).toBeNull()
})
it('returns null for garbage DER bytes that are not a certificate', async () => {
const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => IN_WINDOW })
expect(await v.verifyPeer(new Uint8Array([1, 2, 3, 4, 5]))).toBeNull()
})
})
describe('createMtlsVerifier — construction validation (INV14)', () => {
it('throws when the CA bundle is empty', () => {
expect(() => createMtlsVerifier({ caChainPem: '', hosts: enrolledHosts(), now: () => IN_WINDOW })).toThrow()
})
it('throws when the CA bundle has no PEM CERTIFICATE block', () => {
expect(() =>
createMtlsVerifier({ caChainPem: 'not a certificate', hosts: enrolledHosts(), now: () => IN_WINDOW }),
).toThrow()
})
})
// ── Deterministic seam: isolate DER→PEM + registry gating from fixture contents ──────────────────
const okParsed = (spiffeUri: string): ParsedCert => ({
spiffeUri,
notBefore: 0,
notAfter: 4_000_000_000,
chainValid: true,
})
describe('createMtlsVerifier — ParseCert seam', () => {
it('feeds verifyAgentCert the exact PEM produced by derToPem from the input DER', async () => {
let seenLeafPem: string | null = null
const capturingParse: ParseCert = (leaf) => {
seenLeafPem = leaf
return okParsed(spiffeIdFor('acct-A', 'host-1', 'example.com'))
}
const der = new Uint8Array([9, 8, 7, 6, 5, 4, 3, 2, 1])
const v = createMtlsVerifier({
caChainPem,
hosts: enrolledHosts(),
now: () => 1_000_000,
parse: capturingParse,
})
expect(await v.verifyPeer(der)).toEqual({ hostId: 'host-1', accountId: 'acct-A' })
expect(seenLeafPem).toBe(derToPem(der))
})
it('fails closed (null) and reports to onError when the registry lookup throws', async () => {
const errors: unknown[] = []
const throwingHosts: HostRegistryPort = {
getById: async () => {
throw new Error('registry unavailable')
},
}
const v = createMtlsVerifier({
caChainPem,
hosts: throwingHosts,
now: () => 1_000_000,
onError: (e) => errors.push(e),
parse: () => okParsed(spiffeIdFor('acct-A', 'host-1', 'example.com')),
})
expect(await v.verifyPeer(new Uint8Array([1, 2, 3]))).toBeNull()
expect(errors).toHaveLength(1)
})
})

View File

@@ -0,0 +1,237 @@
import { describe, it, expect, vi } from 'vitest'
import { randomUUID } from 'node:crypto'
import { RELAY_REVOCATIONS_CHANNEL, type KillSignal } from 'relay-contracts'
import {
startRevocationSubscriber,
type ActiveTunnelRef,
type RedisSubscriber,
} from '../../src/wiring/revocation-subscriber.js'
/** Fake ioredis subscriber-mode client: records subscribe/unsubscribe and lets a test emit frames. */
class FakeRedisSubscriber implements RedisSubscriber {
readonly subscribed: string[] = []
readonly unsubscribed: string[] = []
private readonly handlers = new Set<(channel: string, message: string) => void>()
async subscribe(channel: string): Promise<number> {
this.subscribed.push(channel)
return this.subscribed.length
}
async unsubscribe(channel: string): Promise<number> {
this.unsubscribed.push(channel)
return this.unsubscribed.length
}
on(_event: 'message', listener: (channel: string, message: string) => void): this {
this.handlers.add(listener)
return this
}
off(_event: 'message', listener: (channel: string, message: string) => void): this {
this.handlers.delete(listener)
return this
}
/** Test driver: deliver a raw pub/sub frame to every registered listener. */
emit(channel: string, message: string): void {
for (const h of [...this.handlers]) h(channel, message)
}
get listenerCount(): number {
return this.handlers.size
}
}
/** Fake relay node: a live-tunnel map + a closeStream that records + removes the host (models the
* real whole-host teardown mutating the tunnel set, so snapshot-safety is exercised). */
function makeNode(initial: readonly ActiveTunnelRef[]) {
const tunnels = new Map(initial.map((t) => [t.hostId, t]))
const closed: string[] = []
return {
closed,
// Live iterator on purpose: the subscriber must snapshot before tearing down.
activeTunnels: () => tunnels.values(),
closeStream: (hostId: string): void => {
closed.push(hostId)
tunnels.delete(hostId)
},
}
}
const AT = 1_700_000_000
function killMessage(signal: KillSignal): string {
return JSON.stringify(signal)
}
describe('startRevocationSubscriber', () => {
it('subscribes to the relay:revocations channel on start', () => {
const redisSubscriber = new FakeRedisSubscriber()
startRevocationSubscriber({ redisSubscriber, node: makeNode([]) })
expect(redisSubscriber.subscribed).toEqual([RELAY_REVOCATIONS_CHANNEL])
expect(redisSubscriber.listenerCount).toBe(1)
})
it('tears down only the host a host-scoped signal names', () => {
const hostA = randomUUID()
const hostB = randomUUID()
const redisSubscriber = new FakeRedisSubscriber()
const node = makeNode([
{ hostId: hostA, accountId: randomUUID() },
{ hostId: hostB, accountId: randomUUID() },
])
const onApplied = vi.fn()
startRevocationSubscriber({ redisSubscriber, node, onApplied })
redisSubscriber.emit(
RELAY_REVOCATIONS_CHANNEL,
killMessage({ scope: { kind: 'host', hostId: hostA }, at: AT, reason: 'compromised' }),
)
expect(node.closed).toEqual([hostA])
expect(onApplied).toHaveBeenCalledTimes(1)
expect(onApplied).toHaveBeenCalledWith(expect.objectContaining({ at: AT }), 1)
})
it('tears down every host under an account-scoped signal, leaving other accounts running', () => {
const acct1 = randomUUID()
const acct2 = randomUUID()
const hostA = randomUUID()
const hostB = randomUUID()
const hostC = randomUUID()
const redisSubscriber = new FakeRedisSubscriber()
const node = makeNode([
{ hostId: hostA, accountId: acct1 },
{ hostId: hostB, accountId: acct1 },
{ hostId: hostC, accountId: acct2 },
])
startRevocationSubscriber({ redisSubscriber, node })
redisSubscriber.emit(
RELAY_REVOCATIONS_CHANNEL,
killMessage({ scope: { kind: 'account', accountId: acct1 }, at: AT, reason: 'billing' }),
)
expect(node.closed.sort()).toEqual([hostA, hostB].sort())
expect(node.closed).not.toContain(hostC)
})
it('tears down every live host on a global-scoped signal', () => {
const hosts = [
{ hostId: randomUUID(), accountId: randomUUID() },
{ hostId: randomUUID(), accountId: randomUUID() },
{ hostId: randomUUID(), accountId: randomUUID() },
]
const redisSubscriber = new FakeRedisSubscriber()
const node = makeNode(hosts)
startRevocationSubscriber({ redisSubscriber, node })
redisSubscriber.emit(
RELAY_REVOCATIONS_CHANNEL,
killMessage({ scope: { kind: 'global' }, at: AT, reason: 'kill-switch' }),
)
expect(node.closed.sort()).toEqual(hosts.map((h) => h.hostId).sort())
})
it('is a no-op for a host-scoped signal naming a host this node does not serve', () => {
const served = randomUUID()
const redisSubscriber = new FakeRedisSubscriber()
const node = makeNode([{ hostId: served, accountId: randomUUID() }])
const onApplied = vi.fn()
startRevocationSubscriber({ redisSubscriber, node, onApplied })
redisSubscriber.emit(
RELAY_REVOCATIONS_CHANNEL,
killMessage({ scope: { kind: 'host', hostId: randomUUID() }, at: AT, reason: 'other-node' }),
)
expect(node.closed).toEqual([])
expect(onApplied).toHaveBeenCalledWith(expect.anything(), 0)
})
it('drops a malformed (non-JSON) message: no teardown, counted as dropped', () => {
const redisSubscriber = new FakeRedisSubscriber()
const node = makeNode([{ hostId: randomUUID(), accountId: randomUUID() }])
const onDropped = vi.fn()
const onApplied = vi.fn()
startRevocationSubscriber({ redisSubscriber, node, onDropped, onApplied })
redisSubscriber.emit(RELAY_REVOCATIONS_CHANNEL, '{ this is not json')
expect(node.closed).toEqual([])
expect(onDropped).toHaveBeenCalledTimes(1)
expect(onApplied).not.toHaveBeenCalled()
})
it('drops a schema-invalid signal (non-uuid host / missing fields): no teardown', () => {
const redisSubscriber = new FakeRedisSubscriber()
const node = makeNode([{ hostId: randomUUID(), accountId: randomUUID() }])
const onDropped = vi.fn()
startRevocationSubscriber({ redisSubscriber, node, onDropped })
// hostId is not a UUID → RevocationScopeSchema rejects.
redisSubscriber.emit(
RELAY_REVOCATIONS_CHANNEL,
JSON.stringify({ scope: { kind: 'host', hostId: 'not-a-uuid' }, at: AT, reason: 'x' }),
)
// Missing `at` → KillSignalSchema rejects.
redisSubscriber.emit(
RELAY_REVOCATIONS_CHANNEL,
JSON.stringify({ scope: { kind: 'global' }, reason: 'x' }),
)
expect(node.closed).toEqual([])
expect(onDropped).toHaveBeenCalledTimes(2)
})
it('ignores messages published on a different channel', () => {
const hostA = randomUUID()
const redisSubscriber = new FakeRedisSubscriber()
const node = makeNode([{ hostId: hostA, accountId: randomUUID() }])
const onDropped = vi.fn()
const onApplied = vi.fn()
startRevocationSubscriber({ redisSubscriber, node, onDropped, onApplied })
redisSubscriber.emit(
'some:other:channel',
killMessage({ scope: { kind: 'host', hostId: hostA }, at: AT, reason: 'wrong-channel' }),
)
expect(node.closed).toEqual([])
expect(onDropped).not.toHaveBeenCalled()
expect(onApplied).not.toHaveBeenCalled()
})
it('close() removes the message listener, unsubscribes, and is idempotent', () => {
const hostA = randomUUID()
const redisSubscriber = new FakeRedisSubscriber()
const node = makeNode([{ hostId: hostA, accountId: randomUUID() }])
const sub = startRevocationSubscriber({ redisSubscriber, node })
sub.close()
sub.close() // idempotent — no throw, no double-unsubscribe
expect(redisSubscriber.unsubscribed).toEqual([RELAY_REVOCATIONS_CHANNEL])
expect(redisSubscriber.listenerCount).toBe(0)
// A frame delivered after close must not tear anything down.
redisSubscriber.emit(
RELAY_REVOCATIONS_CHANNEL,
killMessage({ scope: { kind: 'host', hostId: hostA }, at: AT, reason: 'after-close' }),
)
expect(node.closed).toEqual([])
})
it('routes a rejected subscribe() to onError instead of swallowing it', async () => {
const failing: RedisSubscriber = {
subscribe: () => Promise.reject(new Error('redis down')),
unsubscribe: async () => 0,
on: () => failing,
off: () => failing,
}
const onError = vi.fn()
startRevocationSubscriber({ redisSubscriber: failing, node: makeNode([]), onError })
await Promise.resolve() // let the rejected subscribe() microtask settle
expect(onError).toHaveBeenCalledTimes(1)
expect(onError.mock.calls[0][0]).toBeInstanceOf(Error)
})
})

View 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')
})
})

View File

@@ -0,0 +1,265 @@
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)
})
})