import { describe, it, expect } from 'vitest' import { revoke, revokeToken } from '../src/revocation/revoke.js' import { isTokenRevoked, killsScope } from '../src/revocation/check.js' import { REVOCATION_PUSH_BUDGET_MS } from '../src/types.js' import type { KillSignal, RevocationBus } from '../src/types.js' import { fakeHostRegistry, fakeRevocationStore, makeHost, uuid } from './_helpers.js' const NOW = 1_700_000_000 /** A fake bus + a fake P1 subscriber holding an already-open, authorized stream. */ function fakeBusWithSubscriber(stream: { hostAccountId: string; hostId: string; open: boolean }) { const published: KillSignal[] = [] const bus: RevocationBus = { publish: async (signal) => { published.push(signal) // P1 subscriber: inject an RST for every covered stream (force-close). if (killsScope(signal, stream.hostAccountId, stream.hostId)) stream.open = false }, } return { bus, published } } describe('revocation (INV12)', () => { it('host revoke publishes a KillSignal that tears down the live tunnel within budget', async () => { const hostId = uuid() const hosts = fakeHostRegistry([makeHost('acct-A', hostId)]) const store = fakeRevocationStore() const stream = { hostAccountId: 'acct-A', hostId, open: true } const { bus, published } = fakeBusWithSubscriber(stream) const t0 = Date.now() const signal = await revoke({ kind: 'host', hostId }, store, hosts, bus, NOW) const elapsedMs = Date.now() - t0 expect(published).toHaveLength(1) expect(signal.scope).toEqual({ kind: 'host', hostId }) expect(stream.open).toBe(false) // live shell dropped, not left alive expect(elapsedMs).toBeLessThan(REVOCATION_PUSH_BUDGET_MS) }) it('account revocation covers only that account, not another tenant', () => { const signal: KillSignal = { scope: { kind: 'account', accountId: 'acct-A' }, at: NOW, reason: 'x' } expect(killsScope(signal, 'acct-A', uuid())).toBe(true) expect(killsScope(signal, 'acct-B', uuid())).toBe(false) // foreign tenant untouched }) it('global revocation covers all hosts (GOAWAY)', () => { const signal: KillSignal = { scope: { kind: 'global' }, at: NOW, reason: 'drain' } expect(killsScope(signal, 'acct-A', uuid())).toBe(true) expect(killsScope(signal, 'acct-B', uuid())).toBe(true) }) it('revoked token jti stops validating; a subsequent reconnect is refused', async () => { const store = fakeRevocationStore() await revokeToken('jti-1', NOW + 60, store) expect(await isTokenRevoked('jti-1', store)).toBe(true) expect(await isTokenRevoked('jti-2', store)).toBe(false) }) it('revocation is idempotent (re-publish is a no-op teardown)', async () => { const hostId = uuid() const hosts = fakeHostRegistry([makeHost('acct-A', hostId)]) const store = fakeRevocationStore() const stream = { hostAccountId: 'acct-A', hostId, open: true } const { bus, published } = fakeBusWithSubscriber(stream) await revoke({ kind: 'host', hostId }, store, hosts, bus, NOW) await revoke({ kind: 'host', hostId }, store, hosts, bus, NOW) expect(published).toHaveLength(2) expect(stream.open).toBe(false) }) })