blob: 828b90a352ce35e2ad4cf07d23077c22f48bd341 [file]
import type { RuntimePolicy } from '@maka/core';
import type { TestProxyResult } from '@maka/core';
import {
requireEncodedByteLimit,
requireExactRecord,
requireRecord,
requireShapedRecord,
requireUtf8String,
} from './codec.js';
import { invalidProtocolFrame } from './errors.js';
import { defineOperation } from './operation-spec.js';
const SECRET_MAX_BYTES = 16 * 1024;
const RESULT_MAX_BYTES = 8 * 1024;
const ERRORS = [
'host_not_ready',
'host_draining',
'operation_unavailable',
'invalid_request',
'internal_failure',
] as const;
export interface NetworkProxyTestInput {
readonly networkProxy?: RuntimePolicy['networkProxy'];
readonly password?: string;
readonly url?: string;
readonly timeoutMs?: number;
}
export type NetworkProxyTestResult = TestProxyResult;
export const NETWORK_PROXY_OPERATION_SPECS = {
'network-proxy.test': defineOperation<
NetworkProxyTestInput,
NetworkProxyTestResult,
(typeof ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: ERRORS,
decodeInput: decodeNetworkProxyTestInput,
decodeOutput: decodeNetworkProxyTestResult,
}),
} as const;
function decodeNetworkProxyTestInput(value: unknown): NetworkProxyTestInput {
const input = requireShapedRecord(
value,
'network proxy test input',
[],
['networkProxy', 'password', 'url', 'timeoutMs'],
);
return {
...(input.networkProxy === undefined
? {}
: { networkProxy: decodeNetworkProxy(input.networkProxy) }),
...(input.password === undefined
? {}
: {
password: requireUtf8String(input.password, 'network proxy password', SECRET_MAX_BYTES),
}),
...(input.url === undefined ? {} : { url: decodeProbeUrl(input.url) }),
...(input.timeoutMs === undefined
? {}
: { timeoutMs: boundedInteger(input.timeoutMs, 1, 30_000, 'network proxy timeout') }),
};
}
function decodeNetworkProxy(value: unknown): RuntimePolicy['networkProxy'] {
const proxy = requireExactRecord(value, 'network proxy configuration', [
'enabled',
'protocol',
'host',
'port',
'authEnabled',
'username',
'bypassList',
'autoBypassDomains',
]);
if (
typeof proxy.enabled !== 'boolean' ||
(proxy.protocol !== 'http' && proxy.protocol !== 'https' && proxy.protocol !== 'socks5') ||
typeof proxy.host !== 'string' ||
proxy.host.length > 255 ||
typeof proxy.authEnabled !== 'boolean' ||
typeof proxy.username !== 'string' ||
proxy.username.length > 256
) {
throw invalidProtocolFrame('Invalid network proxy configuration');
}
return {
enabled: proxy.enabled,
protocol: proxy.protocol,
host: proxy.host,
port: boundedInteger(proxy.port, 1, 65_535, 'network proxy port'),
authEnabled: proxy.authEnabled,
username: proxy.username,
bypassList: stringList(proxy.bypassList, 'network proxy bypass list'),
autoBypassDomains: stringList(proxy.autoBypassDomains, 'network proxy automatic bypass list'),
};
}
function decodeNetworkProxyTestResult(value: unknown): NetworkProxyTestResult {
const result = requireShapedRecord(
value,
'network proxy test result',
['ok', 'latencyMs'],
['status', 'ip', 'countryCode', 'countryFlag', 'error'],
);
if (typeof result.ok !== 'boolean') {
throw invalidProtocolFrame('Invalid network proxy test result');
}
const decoded: NetworkProxyTestResult = {
ok: result.ok,
latencyMs: boundedInteger(result.latencyMs, 0, 300_000, 'network proxy latency'),
...(result.status === undefined
? {}
: { status: boundedInteger(result.status, 100, 599, 'network proxy HTTP status') }),
...optionalText(result, 'ip', 256),
...optionalText(result, 'countryCode', 16),
...optionalText(result, 'countryFlag', 32),
...optionalText(result, 'error', 2_048),
};
requireEncodedByteLimit(decoded, 'network proxy test result', RESULT_MAX_BYTES);
return decoded;
}
function decodeProbeUrl(value: unknown): string {
const raw = requireUtf8String(value, 'network proxy probe URL', 2_048);
let parsed: URL;
try {
parsed = new URL(raw);
} catch {
throw invalidProtocolFrame('Invalid network proxy probe URL');
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw invalidProtocolFrame('Invalid network proxy probe URL');
}
return parsed.toString();
}
function boundedInteger(value: unknown, min: number, max: number, label: string): number {
if (!Number.isSafeInteger(value) || (value as number) < min || (value as number) > max) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return value as number;
}
function stringList(value: unknown, label: string): string[] {
if (
!Array.isArray(value) ||
value.length > 256 ||
value.some((item) => typeof item !== 'string' || item.length > 512)
) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return [...value];
}
function optionalText(
record: Record<string, unknown>,
key: 'ip' | 'countryCode' | 'countryFlag' | 'error',
maxBytes: number,
): Partial<Record<typeof key, string>> {
const value = record[key];
if (value === undefined) return {};
return { [key]: requireUtf8String(value, `network proxy ${key}`, maxBytes) };
}