import { describe, expect, it } from 'vitest' import { buildNativeFrpcToml, type NativeFrpcOptions } from '../src/transport/frpcToml.js' const BASE: NativeFrpcOptions = { subdomain: 'alice', localPort: 3000, authToken: 'super-secret-token', certFile: '/home/alice/.web-terminal-agent/frpc.cert.pem', keyFile: '/home/alice/.web-terminal-agent/frpc.key.pem', trustedCaFile: '/home/alice/.web-terminal-agent/frps-ctrl-ca.pem', } describe('buildNativeFrpcToml (B2h)', () => { it('emits the native-tunnel server/port/tls keys (PLAN_NATIVE_TUNNEL ยง4)', () => { const toml = buildNativeFrpcToml(BASE) expect(toml).toContain('serverAddr = "8.138.1.192"') expect(toml).toContain('serverPort = 443') expect(toml).toContain('transport.tls.enable = true') expect(toml).toContain('transport.tls.serverName = "frp.terminal.yaojia.wang"') expect(toml).toContain('transport.tls.disableCustomTLSFirstByte = true') expect(toml).toContain(`transport.tls.certFile = "${BASE.certFile}"`) expect(toml).toContain(`transport.tls.keyFile = "${BASE.keyFile}"`) expect(toml).toContain(`transport.tls.trustedCaFile = "${BASE.trustedCaFile}"`) expect(toml).toContain('auth.method = "token"') expect(toml).toContain('auth.token = "super-secret-token"') }) it('emits a well-formed [[proxies]] http block bound to loopback', () => { const toml = buildNativeFrpcToml(BASE) expect(toml).toContain('[[proxies]]') expect(toml).toContain('type = "http"') expect(toml).toContain('subdomain = "alice"') expect(toml).toContain('localIP = "127.0.0.1"') expect(toml).toContain('localPort = 3000') // the proxy block comes after the server/tls preamble expect(toml.indexOf('[[proxies]]')).toBeGreaterThan(toml.indexOf('serverAddr')) }) it('does NOT emit the retired v0.8 [common]/tls_enable shape', () => { const toml = buildNativeFrpcToml(BASE) expect(toml).not.toContain('[common]') expect(toml).not.toContain('tls_enable') }) it('honours a configurable serverAddr (default 8.138.1.192)', () => { expect(buildNativeFrpcToml(BASE)).toContain('serverAddr = "8.138.1.192"') expect(buildNativeFrpcToml({ ...BASE, serverAddr: '10.9.8.7' })).toContain( 'serverAddr = "10.9.8.7"', ) }) it('THROWS when localIP is non-loopback (anti-SSRF hard invariant)', () => { expect(() => buildNativeFrpcToml({ ...BASE, localIP: '10.0.0.5' })).toThrow(/loopback/i) expect(() => buildNativeFrpcToml({ ...BASE, localIP: '0.0.0.0' })).toThrow(/loopback/i) expect(() => buildNativeFrpcToml({ ...BASE, localIP: '192.168.1.9' })).toThrow(/loopback/i) }) it('accepts the three explicit loopback localIP forms', () => { for (const ip of ['127.0.0.1', '::1', 'localhost']) { expect(() => buildNativeFrpcToml({ ...BASE, localIP: ip })).not.toThrow() } }) it('rejects an empty or non-label-safe subdomain', () => { expect(() => buildNativeFrpcToml({ ...BASE, subdomain: '' })).toThrow(/subdomain/i) expect(() => buildNativeFrpcToml({ ...BASE, subdomain: 'has space' })).toThrow(/subdomain/i) expect(() => buildNativeFrpcToml({ ...BASE, subdomain: '-bad' })).toThrow(/subdomain/i) expect(() => buildNativeFrpcToml({ ...BASE, subdomain: 'bad-' })).toThrow(/subdomain/i) expect(() => buildNativeFrpcToml({ ...BASE, subdomain: 'a'.repeat(64) })).toThrow(/subdomain/i) expect(() => buildNativeFrpcToml({ ...BASE, subdomain: 'a/b' })).toThrow(/subdomain/i) }) it('rejects an out-of-range or non-integer localPort', () => { expect(() => buildNativeFrpcToml({ ...BASE, localPort: 0 })).toThrow(/port/i) expect(() => buildNativeFrpcToml({ ...BASE, localPort: 70000 })).toThrow(/port/i) expect(() => buildNativeFrpcToml({ ...BASE, localPort: 3000.5 })).toThrow(/port/i) expect(() => buildNativeFrpcToml({ ...BASE, localPort: -1 })).toThrow(/port/i) }) it('rejects missing keystore paths and an empty auth token', () => { expect(() => buildNativeFrpcToml({ ...BASE, certFile: '' })).toThrow(/certFile/i) expect(() => buildNativeFrpcToml({ ...BASE, keyFile: '' })).toThrow(/keyFile/i) expect(() => buildNativeFrpcToml({ ...BASE, trustedCaFile: '' })).toThrow(/trustedCaFile/i) expect(() => buildNativeFrpcToml({ ...BASE, authToken: '' })).toThrow(/token/i) }) it('escapes backslashes and quotes in Windows-style paths (valid TOML basic string)', () => { const winCert = 'C:\\Users\\alice\\.web-terminal-agent\\frpc.cert.pem' const toml = buildNativeFrpcToml({ ...BASE, certFile: winCert }) expect(toml).toContain( 'transport.tls.certFile = "C:\\\\Users\\\\alice\\\\.web-terminal-agent\\\\frpc.cert.pem"', ) }) it('rejects control characters in paths/token (TOML-injection guard)', () => { expect(() => buildNativeFrpcToml({ ...BASE, authToken: 'a\nb' })).toThrow() expect(() => buildNativeFrpcToml({ ...BASE, certFile: 'a\nb' })).toThrow() expect(() => buildNativeFrpcToml({ ...BASE, keyFile: 'a"b\nq' })).toThrow() }) })