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.
This commit is contained in:
132
e2e/harness/fakes.ts
Normal file
132
e2e/harness/fakes.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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) }
|
||||
}
|
||||
Reference in New Issue
Block a user