/** * LocalAuthProvider — local-auth safety + audience binding (build-plan §3, §5). */ import { describe, it, expect } from 'vitest'; import { LocalAuthProvider, defaultLocalPrincipals, OPS_AUDIENCE, FINANCE_AUDIENCE, } from './local-auth.js'; import { AuthError } from './cognito-auth.js'; function bearer(token: string) { return { headers: { authorization: `Bearer ${token}` } }; } describe('LocalAuthProvider safety', () => { it('refuses to construct inside an AWS runtime even when env=local', () => { for (const v of ['AWS_LAMBDA_FUNCTION_NAME', 'AWS_EXECUTION_ENV']) { const prev = process.env[v]; process.env[v] = 'sh-mcp-finance'; try { expect( () => new LocalAuthProvider({ audience: OPS_AUDIENCE, principals: defaultLocalPrincipals(), env: 'local', }), ).toThrow(/refuses to run inside an AWS/); } finally { if (prev === undefined) delete process.env[v]; else process.env[v] = prev; } } }); it('refuses to construct unless SH_MCP_ENV=local', () => { expect( () => new LocalAuthProvider({ audience: OPS_AUDIENCE, principals: defaultLocalPrincipals(), env: 'aws', }), ).toThrow(/only be constructed when SH_MCP_ENV=local/); expect( () => new LocalAuthProvider({ audience: OPS_AUDIENCE, principals: defaultLocalPrincipals(), env: undefined, }), ).toThrow(); }); it('constructs in local mode', () => { expect( () => new LocalAuthProvider({ audience: OPS_AUDIENCE, principals: defaultLocalPrincipals(), env: 'local', }), ).not.toThrow(); }); }); describe('LocalAuthProvider authentication', () => { const ops = new LocalAuthProvider({ audience: OPS_AUDIENCE, principals: defaultLocalPrincipals(), env: 'local', }); const finance = new LocalAuthProvider({ audience: FINANCE_AUDIENCE, principals: defaultLocalPrincipals(), env: 'local', }); it('resolves a known dev token to the right AuthContext', async () => { const ctx = await ops.authenticate(bearer('dev-ops-only')); expect(ctx.sub).toBe('ops-only@seahavenind.com'); expect(ctx.scopes).toContain('ops:read'); expect(ctx.aud).toBe(OPS_AUDIENCE); }); it('rejects a missing token (→401)', async () => { await expect(ops.authenticate({ headers: {} })).rejects.toMatchObject({ name: 'AuthError', code: 'missing_token', }); }); it('rejects an unknown token (→401)', async () => { await expect(ops.authenticate(bearer('not-a-real-token'))).rejects.toBeInstanceOf(AuthError); }); it('AUDIENCE BINDING: an ops principal is rejected by the finance server', async () => { await expect(finance.authenticate(bearer('dev-ops-only'))).rejects.toMatchObject({ code: 'client_not_allowed', }); }); it('AUDIENCE BINDING: a finance principal is rejected by the ops server', async () => { await expect(ops.authenticate(bearer('dev-finance'))).rejects.toMatchObject({ code: 'client_not_allowed', }); }); it('the finance principal authenticates against the finance server', async () => { const ctx = await finance.authenticate(bearer('dev-finance')); expect(ctx.scopes).toContain('finance:read'); expect(ctx.aud).toBe(FINANCE_AUDIENCE); }); });