| import { describe, expect, test } from 'bun:test' |
| import { |
| classifyH5Request, |
| isLoopbackHost, |
| shouldBlockDisabledH5Access, |
| shouldRequireH5Token, |
| } from '../h5AccessPolicy.js' |
|
|
| function req(url: string, init: RequestInit = {}) { |
| return new Request(url, init) |
| } |
|
|
| const localContext = { clientAddress: '127.0.0.1' } |
| const remoteContext = { clientAddress: '192.168.0.44' } |
|
|
| describe('h5AccessPolicy', () => { |
| test('recognizes loopback hosts as local trusted requests', () => { |
| expect(isLoopbackHost('localhost')).toBe(true) |
| expect(isLoopbackHost('127.0.0.1')).toBe(true) |
| expect(isLoopbackHost('[::1]')).toBe(true) |
| expect(isLoopbackHost('192.168.0.20')).toBe(false) |
| }) |
|
|
| test('keeps Tauri WebView requests to loopback tokenless', () => { |
| const request = req('http://127.0.0.1:3456/api/status', { |
| headers: { Origin: 'http://tauri.localhost' }, |
| }) |
| expect(classifyH5Request(request, new URL(request.url), localContext)).toBe('local-trusted') |
| expect(shouldRequireH5Token({ request, url: new URL(request.url), h5Enabled: true, context: localContext })).toBe(false) |
| }) |
|
|
| test('keeps local internal SDK websocket routes tokenless', () => { |
| const request = req('http://127.0.0.1:3456/sdk/session-1') |
| expect(classifyH5Request(request, new URL(request.url), localContext)).toBe('internal-sdk') |
| expect(shouldRequireH5Token({ request, url: new URL(request.url), h5Enabled: true, context: localContext })).toBe(false) |
| }) |
|
|
| test('does not trust remote SDK websocket routes by path alone', () => { |
| const request = req('http://192.168.0.20:3456/sdk/session-1') |
| expect(classifyH5Request(request, new URL(request.url), remoteContext)).toBe('h5-browser') |
| expect(shouldRequireH5Token({ request, url: new URL(request.url), h5Enabled: true, context: remoteContext })).toBe(false) |
| }) |
|
|
| test('keeps adapter API routes tokenless for local integrations', () => { |
| const request = req('http://127.0.0.1:3456/api/adapters') |
| expect(classifyH5Request(request, new URL(request.url), localContext)).toBe('local-trusted') |
| expect(shouldRequireH5Token({ request, url: new URL(request.url), h5Enabled: true, context: localContext })).toBe(false) |
| }) |
|
|
| test('does not trust loopback adapter requests from non-local browser origins', () => { |
| const request = req('http://127.0.0.1:3456/api/adapters', { |
| headers: { Origin: 'https://blocked.example.com' }, |
| }) |
| expect(classifyH5Request(request, new URL(request.url), localContext)).toBe('h5-browser') |
| expect(shouldRequireH5Token({ request, url: new URL(request.url), h5Enabled: true, context: localContext })).toBe(true) |
| }) |
|
|
| test('does not trust spoofed loopback hosts from remote clients', () => { |
| const request = req('http://127.0.0.1:3456/api/status', { |
| headers: { Origin: 'http://127.0.0.1:5179' }, |
| }) |
| expect(classifyH5Request(request, new URL(request.url), remoteContext)).toBe('h5-browser') |
| expect(shouldBlockDisabledH5Access({ |
| request, |
| url: new URL(request.url), |
| h5Enabled: false, |
| explicitAuthRequired: false, |
| context: remoteContext, |
| })).toBe(true) |
| }) |
|
|
| test('keeps local desktop chat websocket routes tokenless', () => { |
| for (const init of [{}, { headers: { Origin: 'http://tauri.localhost' } }]) { |
| const request = req('http://127.0.0.1:3456/ws/session-1', init) |
| expect(classifyH5Request(request, new URL(request.url), localContext)).toBe('local-trusted') |
| expect(shouldRequireH5Token({ request, url: new URL(request.url), h5Enabled: true, context: localContext })).toBe(false) |
| } |
| }) |
|
|
| test('requires H5 token for LAN browser API, proxy, and chat websocket routes when enabled', () => { |
| for (const pathname of [ |
| '/api/status', |
| '/api/mcp', |
| '/api/plugins', |
| '/api/agents', |
| '/proxy/openai/v1/chat/completions', |
| '/ws/session-1', |
| ]) { |
| const request = req(`http://192.168.0.20:3456${pathname}`, { |
| headers: { Origin: 'http://192.168.0.20:3456' }, |
| }) |
| expect(classifyH5Request(request, new URL(request.url), remoteContext)).toBe('h5-browser') |
| expect(shouldRequireH5Token({ request, url: new URL(request.url), h5Enabled: true, context: remoteContext })).toBe(true) |
| } |
| }) |
|
|
| test('blocks LAN browser capability routes while H5 access is disabled', () => { |
| for (const pathname of [ |
| '/api/status', |
| '/api/mcp', |
| '/api/plugins', |
| '/api/agents', |
| '/proxy/openai/v1/chat/completions', |
| '/ws/session-1', |
| '/sdk/session-1', |
| ]) { |
| const request = req(`http://192.168.0.20:3456${pathname}`, { |
| headers: { Origin: 'http://192.168.0.20:3456' }, |
| }) |
| expect(shouldBlockDisabledH5Access({ |
| request, |
| url: new URL(request.url), |
| h5Enabled: false, |
| explicitAuthRequired: false, |
| context: remoteContext, |
| })).toBe(true) |
| } |
| }) |
|
|
| test('keeps local capability routes and static bootstrap routes available while H5 access is disabled', () => { |
| for (const pathname of ['/api/status', '/proxy/openai/v1/chat/completions', '/ws/session-1', '/sdk/session-1']) { |
| const request = req(`http://127.0.0.1:3456${pathname}`) |
| expect(shouldBlockDisabledH5Access({ |
| request, |
| url: new URL(request.url), |
| h5Enabled: false, |
| explicitAuthRequired: false, |
| context: localContext, |
| })).toBe(false) |
| } |
|
|
| for (const pathname of ['/', '/health', '/assets/app.js']) { |
| const request = req(`http://192.168.0.20:3456${pathname}`, { |
| headers: { Origin: 'http://192.168.0.20:3456' }, |
| }) |
| expect(shouldBlockDisabledH5Access({ |
| request, |
| url: new URL(request.url), |
| h5Enabled: false, |
| explicitAuthRequired: false, |
| context: remoteContext, |
| })).toBe(false) |
| } |
| }) |
|
|
| test('explicit deployment auth does not use the H5 token gate when H5 is disabled', () => { |
| const request = req('http://127.0.0.1:3456/api/status') |
| expect(shouldRequireH5Token({ |
| request, |
| url: new URL(request.url), |
| h5Enabled: false, |
| context: localContext, |
| })).toBe(false) |
| }) |
|
|
| test('does not block explicitly authenticated deployments before auth middleware runs', () => { |
| const request = req('http://192.168.0.20:3456/api/status', { |
| headers: { Origin: 'https://phone.example' }, |
| }) |
| expect(shouldBlockDisabledH5Access({ |
| request, |
| url: new URL(request.url), |
| h5Enabled: false, |
| explicitAuthRequired: true, |
| context: remoteContext, |
| })).toBe(false) |
| }) |
| }) |
|
|