import { describe, test, expect, vi } from 'vitest' import { APP_SUBPROTOCOL, encodeTokenSubprotocol } from 'relay-contracts' import { authorizeUpgrade, extractCapabilityToken, TOKEN_COOKIE_NAME, type UpgradeRequest, type AuthorizeDeps, } from '../data-plane/upgrade.js' import type { Authorizer, AuthzOutcome } from '../data-plane/authz-port.js' import type { RouteResolver, ResolvedHost } from '../data-plane/subdomain-router.js' const RAW_TOKEN = 'cap-token-abc123' const RESOLVED: ResolvedHost = { hostId: 'host-uuid-1', accountId: 'acct-1', subdomain: 'alice' } function resolver(overrides?: Partial>): RouteResolver { return { resolveSubdomain: async (sub) => { if (overrides && sub in overrides) return overrides[sub] ?? null return sub === 'alice' ? RESOLVED : null }, } } function okAuthorizer(): Authorizer { const ok: AuthzOutcome = { ok: true, hostId: RESOLVED.hostId, principal: 'p-1', jti: 'jti-9' } return { onUpgrade: vi.fn(async () => ok), onReattach: vi.fn(async () => ok) } } function baseReq(over?: Partial): UpgradeRequest { return { host: 'alice.term.example.com', origin: 'https://alice.term.example.com', url: '/term?join=x', subprotocols: [APP_SUBPROTOCOL, encodeTokenSubprotocol(RAW_TOKEN)], cookies: {}, remoteAddr: '203.0.113.7', dpop: { proof: 'proof', publicKeyThumbprint: 'jkt' }, activeSessionCount: 2, ...over, } } function deps(authorizer: Authorizer, r: RouteResolver = resolver()): AuthorizeDeps { return { authorizer, resolver: r, baseDomain: 'term.example.com', now: () => 1000, remoteAddrSalt: 'salt', requiredRight: 'attach', } } describe('authorizeUpgrade — thin adapter over P5 (T8, FIX 6a)', () => { test('happy: delegates to onUpgrade with a fully-populated ctx; maps to {ok:true}', async () => { const auth = okAuthorizer() const d = deps(auth) const res = await authorizeUpgrade(baseReq(), d) expect(res.ok).toBe(true) if (!res.ok) return expect(res.hostId).toBe(RESOLVED.hostId) expect(res.open.subdomain).toBe('alice') expect(res.open.capabilityTokenRef).toBe('jti-9') expect(res.acceptedSubprotocol).toBe(APP_SUBPROTOCOL) expect(auth.onUpgrade).toHaveBeenCalledTimes(1) const ctx = (auth.onUpgrade as ReturnType).mock.calls[0]![0] expect(ctx.expectedAud).toBe('alice') expect(ctx.requestedHostId).toBe(RESOLVED.hostId) expect(ctx.requiredRight).toBe('attach') expect(ctx.capabilityRaw).toBe(RAW_TOKEN) // decoded from the subprotocol carrier expect(ctx.dpop).toEqual({ proof: 'proof', publicKeyThumbprint: 'jkt' }) expect(ctx.activeSessionCount).toBe(2) expect(ctx.principal).toBeNull() // P5 resolves identity from the signed token (INV3) }) test('P5 deny is passed through verbatim; no independent allow branch', async () => { const deny: AuthzOutcome = { ok: false, status: 401 } const auth: Authorizer = { onUpgrade: vi.fn(async () => deny), onReattach: vi.fn(async () => deny) } const res = await authorizeUpgrade(baseReq(), deps(auth)) expect(res).toEqual({ ok: false, status: 401 }) }) test('reattach: sessionId present → onReattach is called (not onUpgrade)', async () => { const auth = okAuthorizer() await authorizeUpgrade(baseReq({ sessionId: 'sess-1' }), deps(auth)) expect(auth.onReattach).toHaveBeenCalledTimes(1) expect(auth.onUpgrade).not.toHaveBeenCalled() const ctx = (auth.onReattach as ReturnType).mock.calls[0]![0] expect(ctx.sessionId).toBe('sess-1') }) test('token via cookie fallback is accepted', async () => { const auth = okAuthorizer() const req = baseReq({ subprotocols: [APP_SUBPROTOCOL], cookies: { [TOKEN_COOKIE_NAME]: RAW_TOKEN } }) const res = await authorizeUpgrade(req, deps(auth)) expect(res.ok).toBe(true) const via = extractCapabilityToken(req) expect(via).toEqual({ token: RAW_TOKEN, via: 'cookie' }) }) test('INV15: a token ONLY in the query string → 401 and the authorizer is NEVER called', async () => { const auth = okAuthorizer() const req = baseReq({ url: `/term?cap=${RAW_TOKEN}`, subprotocols: [APP_SUBPROTOCOL], cookies: {} }) const res = await authorizeUpgrade(req, deps(auth)) expect(res).toEqual({ ok: false, status: 401 }) expect(auth.onUpgrade).not.toHaveBeenCalled() expect(extractCapabilityToken(req)).toBeNull() // never reads req.url }) test('local parse: unparseable Host → 401 without calling the authorizer', async () => { const auth = okAuthorizer() const res = await authorizeUpgrade(baseReq({ host: 'term.example.com' }), deps(auth)) expect(res).toEqual({ ok: false, status: 401 }) expect(auth.onUpgrade).not.toHaveBeenCalled() }) test('local parse: unknown subdomain (resolver null) → 403 without calling the authorizer', async () => { const auth = okAuthorizer() const res = await authorizeUpgrade(baseReq({ host: 'bob.term.example.com' }), deps(auth)) expect(res).toEqual({ ok: false, status: 403 }) expect(auth.onUpgrade).not.toHaveBeenCalled() }) })