Files
web-terminal/e2e/harness/fakes.ts
Yaojia Wang ad5cf06207 test(relay): cross-package e2e adversarial security harness
New e2e/ package wires the REAL P5(auth)+P4(crypto)+P2(agent) exports through
in-memory seams and an untrusted-relay attacker vantage (RelaySpy). Dynamically
re-validates F1–F4/F6 plus MITM/reflection/replay/reorder/INV2/cross-tenant/
single-use — every assertion runs the production code path. 21 tests green, tsc
clean. node_modules via symlinks (gitignored); no npm install needed.
2026-07-02 16:41:19 +02:00

133 lines
3.6 KiB
TypeScript

/**
* In-memory port fakes — the ONLY seams that stand in for the true I/O boundaries P1/P3 own
* (host registry / session registry / revocation store / token buckets / audit sink / revocation
* bus). Every security decision still runs through the REAL relay-auth exports; these fakes only
* supply storage. Adapted from `relay-auth/test/_helpers.ts` (same shapes, bare import paths).
*/
import type { HostRecord, KillSignal, RevocationBus } from 'relay-contracts'
import type {
AuthenticatedPrincipal,
AuditEvent,
AuditSink,
HostRegistryPort,
SessionRegistryPort,
RevocationStore,
TokenBucketStore,
} from 'relay-auth'
/** An authenticated principal (the ONLY source of accountId, INV3). */
export function principal(
accountId: string,
overrides: Partial<AuthenticatedPrincipal> = {},
): AuthenticatedPrincipal {
return {
kind: 'human',
accountId,
principalId: 'cred-' + accountId,
amr: ['passkey'],
authAt: 1000,
stepUpAt: null,
...overrides,
}
}
/** A minimal online HostRecord for the authz registry (agentPubkey here is unused by authz). */
export function makeHostRecord(
accountId: string,
hostId: string,
status: HostRecord['status'] = 'online',
): HostRecord {
return {
hostId,
accountId,
subdomain: 'sub-' + hostId,
agentPubkey: new Uint8Array(32),
enrollFpr: 'fpr-' + hostId,
status,
lastSeen: '2026-01-01T00:00:00.000Z',
createdAt: '2026-01-01T00:00:00.000Z',
revokedAt: null,
}
}
export function fakeHostRegistry(hosts: readonly HostRecord[]): HostRegistryPort {
const map = new Map(hosts.map((h) => [h.hostId, h]))
return { getById: async (id) => map.get(id) ?? null }
}
export interface MutableSessionRegistry extends SessionRegistryPort {
add(entry: { sessionId: string; hostId: string; accountId: string }): void
}
export function fakeSessionRegistry(
seed: readonly { sessionId: string; hostId: string; accountId: string }[] = [],
): MutableSessionRegistry {
const map = new Map(seed.map((s) => [s.sessionId, { hostId: s.hostId, accountId: s.accountId }]))
return {
getById: async (id) => map.get(id) ?? null,
add: (e) => {
map.set(e.sessionId, { hostId: e.hostId, accountId: e.accountId })
},
}
}
export interface RevocationFake extends RevocationStore {
readonly revoked: Set<string>
readonly consumed: Set<string>
}
export function fakeRevocationStore(): RevocationFake {
const revoked = new Set<string>()
const consumed = new Set<string>()
return {
revoked,
consumed,
isRevoked: async (jti) => revoked.has(jti),
revokeJti: async (jti) => {
revoked.add(jti)
},
consumeOnce: async (jti) => {
if (consumed.has(jti)) return false
consumed.add(jti)
return true
},
}
}
export interface TokenBucketFake extends TokenBucketStore {
readonly blocked: Set<string>
readonly calls: string[]
}
/** Token bucket that always allows unless a key is added to `blocked`. */
export function fakeTokenBucket(): TokenBucketFake {
const blocked = new Set<string>()
const calls: string[] = []
return {
blocked,
calls,
take: async (key) => {
calls.push(key)
return !blocked.has(key)
},
}
}
export interface AuditFake extends AuditSink {
readonly events: AuditEvent[]
}
export function fakeAuditSink(): AuditFake {
const events: AuditEvent[] = []
return { events, append: async (e) => void events.push(e) }
}
export interface RevocationBusFake extends RevocationBus {
readonly published: KillSignal[]
}
export function fakeRevocationBus(): RevocationBusFake {
const published: KillSignal[] = []
return { published, publish: async (s) => void published.push(s) }
}