/** * Phase 0 world — composes the REAL relay-auth (P5) enforcement + relay-e2e (P4) handshake through * the in-memory stores, tailored for the live data-plane path: a host's capability-token `aud` is * the SINGLE subdomain label (what `authorizeUpgrade` derives from the Host header), not the FQDN. * Directly modelled on `e2e/harness/world.ts` (the reference wiring named in the build spec). * * Only the true I/O boundaries are faked; every security check (Origin/CSWSH, token verify, DPoP * PoP, cross-tenant gate, single-use jti) is the production relay-auth path. */ import { randomUUID, randomBytes } from 'node:crypto' import type { AeadAlg, E2ESession, HostHello } from 'relay-contracts' import { createClientHandshake, createHostHandshake, createE2ESession, MemoryDevicePinStore, } from 'relay-e2e' import { signDeviceAuthProof, verifyDeviceProof, type EnforceDeps, type StepUpPolicy, } from 'relay-auth' import { generateEd25519KeyPair, exportEd25519PublicRaw, importEd25519PublicRaw, signEd25519, verifyEd25519, } from 'relay-auth/src/crypto/ed25519.js' import type { MtlsVerifier } from 'term-relay/data-plane/agent-listener.js' import type { Authorizer } from 'term-relay/data-plane/authz-port.js' import type { RouteResolver } from 'term-relay/data-plane/subdomain-router.js' import { fakeAuditSink, fakeHostRegistry, fakeRevocationStore, fakeSessionRegistry, fakeTokenBucket, makeHostRecord, principal, type AuditFake, type MutableSessionRegistry, type RevocationFake, type TokenBucketFake, } from './memory-stores.js' import { issueCapBundle, setupP5SigningKey, resetDpopCacheForTest, type CapBundle } from './capability.js' import { createAuthorizer } from './authorizer.js' import { memoryRouteResolver } from './data-plane.js' export const DEFAULT_NOW = 1_700_000_000 const REPLAY_ALG: AeadAlg = 'xchacha20-poly1305' /** Phase 0 default: step-up never required (F2: null session-principal is correct). */ export const NO_STEP_UP: StepUpPolicy = { required: false, maxAgeSeconds: Number.MAX_SAFE_INTEGER, requiredMethod: 'passkey', } /** A seeded host: authz identity (hostId/accountId/label) + §4.4 e2e identity + replay secret. */ export interface HostFixture { readonly label: string readonly hostId: string readonly accountId: string readonly subdomain: string readonly origin: string readonly agentPubkey: Uint8Array readonly hostContentSecret: Uint8Array readonly replayAlg: AeadAlg readonly signingPriv: CryptoKey } async function makeHostFixture( label: string, accountId: string, baseDomain: string, ): Promise { const kp = await generateEd25519KeyPair() const agentPubkey = await exportEd25519PublicRaw(kp.publicKey) return { label, hostId: randomUUID(), accountId, subdomain: label, origin: `https://${label}.${baseDomain}`, agentPubkey, hostContentSecret: new Uint8Array(randomBytes(32)), replayAlg: REPLAY_ALG, signingPriv: kp.privateKey, } } export interface EstablishedSessions { readonly client: E2ESession readonly host: E2ESession readonly agentPubkey: Uint8Array } export interface Phase0World { readonly now: number readonly signingKey: CryptoKey /** Raw 32-byte Ed25519 P5 verify pubkey (for CAPABILITY_SIGN_PUBKEY_B64). */ readonly publicRaw: Uint8Array readonly baseDomain: string readonly host: HostFixture readonly allowedOrigins: readonly string[] readonly deps: EnforceDeps readonly audit: AuditFake readonly revocation: RevocationFake readonly buckets: TokenBucketFake readonly sessions: MutableSessionRegistry readonly resolver: RouteResolver readonly authorizer: Authorizer readonly mtls: MtlsVerifier issueCap(o?: { ttl?: number; now?: number }): Promise establishSession(opts?: { now?: number verifyAgainstPubkey?: Uint8Array tamperHostHello?: (h: HostHello) => HostHello }): Promise } export async function buildPhase0World(now: number = DEFAULT_NOW): Promise { const baseDomain = 'term.localhost' const { signingKey, publicRaw } = await setupP5SigningKey() resetDpopCacheForTest() const host = await makeHostFixture('alice', 'acct-alice', baseDomain) const hosts = fakeHostRegistry([makeHostRecord(host.accountId, host.hostId, host.subdomain)]) const sessions = fakeSessionRegistry([]) const revocation = fakeRevocationStore() const buckets = fakeTokenBucket() const audit = fakeAuditSink() const allowedOrigins = [host.origin] const deps: EnforceDeps = { hosts, sessions, revocation, buckets, audit, stepUpPolicyFor: () => NO_STEP_UP, } const routeMap = new Map([[host.subdomain, { hostId: host.hostId, accountId: host.accountId }]]) const resolver = memoryRouteResolver(routeMap) const authorizer = createAuthorizer({ deps, allowedOrigins, now: () => now }) const mtls: MtlsVerifier = { verifyPeer: () => ({ hostId: host.hostId, accountId: host.accountId }), } return { now, signingKey, publicRaw, baseDomain, host, allowedOrigins, deps, audit, revocation, buckets, sessions, resolver, authorizer, mtls, issueCap(o = {}) { return issueCapBundle(signingKey, { accountId: host.accountId, host: host.hostId, aud: host.subdomain, ttl: o.ttl, now: o.now ?? now, }) }, async establishSession(opts = {}): Promise { const hsNow = opts.now ?? now const client = createClientHandshake({ aeadOffer: ['xchacha20-poly1305', 'aes-256-gcm'], deviceAuthProofProvider: { proofFor: (_hostId, binding) => signDeviceAuthProof(principal(host.accountId), binding, signingKey, hsNow), }, verifier: { verify: async (pub, transcript, sig) => verifyEd25519(await importEd25519PublicRaw(pub), sig, transcript), }, pinStore: new MemoryDevicePinStore(), hostId: host.hostId, }) const hostHs = createHostHandshake({ signer: { sign: (transcript) => signEd25519(host.signingPriv, transcript) }, agentPubkey: host.agentPubkey, supported: ['xchacha20-poly1305', 'aes-256-gcm'], verifyDeviceProof: (proof, binding) => verifyDeviceProof(proof, binding, hsNow), }) const clientHello = await client.start() const rawHostHello = await hostHs.onClientHello(clientHello) const hostHello = opts.tamperHostHello ? opts.tamperHostHello(rawHostHello) : rawHostHello const clientResult = await client.onHostHello( hostHello, opts.verifyAgainstPubkey ?? host.agentPubkey, ) return { client: createE2ESession('client', clientResult), host: createE2ESession('host', hostHs.result!), agentPubkey: host.agentPubkey, } }, } }