/** * Happy-path full flow, end-to-end across P5 (auth) + P4 (E2E crypto) + P2 (replay), asserting each * stage. Everything runs through the REAL package exports; only registries/buckets/sockets are faked. */ import { describe, it, expect, beforeEach } from 'vitest' import { buildRelayWorld, DEFAULT_NOW, type RelayWorld } from '../harness/world.js' const utf8 = (s: string): Uint8Array => new TextEncoder().encode(s) const fromUtf8 = (b: Uint8Array): string => new TextDecoder().decode(b) const NOW = DEFAULT_NOW describe('relay flow (happy path)', () => { let world: RelayWorld beforeEach(async () => { world = await buildRelayWorld(NOW) }) it('issueCap → upgrade allows (origin ok, DPoP bound, deny-by-default satisfied)', async () => { const { hostA } = world const cap = await world.issueCap({ accountId: hostA.accountId, host: hostA.hostId, aud: hostA.aud }) const out = await world.upgrade({ raw: cap.raw, dpop: cap.dpop, host: hostA.hostId, aud: hostA.aud, origin: hostA.origin, now: NOW, }) expect(out.ok).toBe(true) expect(world.audit.events.at(-1)).toMatchObject({ outcome: 'allow', action: 'attach' }) }) it('handshake establishes MATCHING session keys on both sides (client↔host round-trip)', async () => { const { client, host } = await world.establishSession() // Matching keys ⇒ bidirectional plaintext round-trips through the direction split. const c2h = host.open(client.seal(utf8('ls -la'))) expect(fromUtf8(c2h)).toBe('ls -la') const h2c = client.open(host.seal(utf8('total 0'))) expect(fromUtf8(h2c)).toBe('total 0') }) it('seals client→host and host→client through the RelaySpy; the RelaySpy never sees plaintext (INV2)', async () => { const { client, host, spy } = await world.establishSession() const marker = `PLAINTEXT_${crypto.randomUUID()}` const up = spy.forward(client.seal(utf8(marker))) expect(fromUtf8(host.open(up))).toBe(marker) const down = spy.forward(host.seal(utf8(`reply_${marker}`))) expect(fromUtf8(client.open(down))).toBe(`reply_${marker}`) expect(spy.contains(marker)).toBe(false) expect(spy.captured.length).toBe(2) }) it('reattach to an own-account session is allowed', async () => { const { hostA } = world const sessionId = crypto.randomUUID() world.addSession({ sessionId, hostId: hostA.hostId, accountId: hostA.accountId }) const cap = await world.issueCap({ accountId: hostA.accountId, host: hostA.hostId, aud: hostA.aud }) const out = await world.reattach({ raw: cap.raw, dpop: cap.dpop, host: hostA.hostId, aud: hostA.aud, origin: hostA.origin, sessionId, now: NOW, }) expect(out.ok).toBe(true) expect(world.audit.events.at(-1)).toMatchObject({ outcome: 'allow', action: 'reattach' }) }) it('revokeToken then re-presenting the same jti (fresh DPoP) is denied', async () => { const { hostA } = world const cap = await world.issueCap({ accountId: hostA.accountId, host: hostA.hostId, aud: hostA.aud }) const first = await world.upgrade({ raw: cap.raw, dpop: cap.dpop, host: hostA.hostId, aud: hostA.aud, origin: hostA.origin, now: NOW, }) expect(first.ok).toBe(true) if (!first.ok) return await world.revokeToken(first.jti) const second = await world.upgrade({ raw: cap.raw, dpop: await cap.newDpop(NOW), // fresh DPoP jti so we reach the revocation gate, not the DPoP cache host: hostA.hostId, aud: hostA.aud, origin: hostA.origin, now: NOW, }) expect(second).toMatchObject({ ok: false, status: 403, reason: 'token_revoked' }) }) })