diff --git a/frontend/src/lib/xray/protocol-capabilities.ts b/frontend/src/lib/xray/protocol-capabilities.ts
new file mode 100644
index 00000000..417071df
--- /dev/null
+++ b/frontend/src/lib/xray/protocol-capabilities.ts
@@ -0,0 +1,74 @@
+// Pure-function ports of the legacy Inbound class capability predicates
+// (canEnableTls, canEnableReality, canEnableTlsFlow, canEnableStream,
+// canEnableVisionSeed, isSS2022, isSSMultiUser). Each accepts the minimal
+// slice of an InboundFormValues it needs, so the same predicate can be
+// called against a partial-row, a full form value, or a hand-built test
+// fixture without the caller projecting a whole object.
+
+const TLS_ELIGIBLE_PROTOCOLS = ['vmess', 'vless', 'trojan', 'shadowsocks'];
+const TLS_NETWORKS = ['tcp', 'ws', 'http', 'grpc', 'httpupgrade', 'xhttp'];
+const REALITY_ELIGIBLE_PROTOCOLS = ['vless', 'trojan'];
+const REALITY_NETWORKS = ['tcp', 'http', 'grpc', 'xhttp'];
+const STREAM_PROTOCOLS = ['vmess', 'vless', 'trojan', 'shadowsocks', 'hysteria'];
+const VISION_FLOW = 'xtls-rprx-vision';
+const SS_2022_PREFIX = '2022';
+const SS_BLAKE3_CHACHA20 = '2022-blake3-chacha20-poly1305';
+
+export interface CapabilityProtocolSlice {
+ protocol: string;
+ streamSettings?: { network?: string; security?: string };
+}
+
+export interface CapabilityVlessSlice extends CapabilityProtocolSlice {
+ settings?: { clients?: { flow?: string }[] };
+}
+
+export interface CapabilityShadowsocksSlice {
+ protocol: string;
+ settings?: { method?: string };
+}
+
+export function canEnableTls(values: CapabilityProtocolSlice): boolean {
+ if (values.protocol === 'hysteria') return true;
+ if (!TLS_ELIGIBLE_PROTOCOLS.includes(values.protocol)) return false;
+ return TLS_NETWORKS.includes(values.streamSettings?.network ?? '');
+}
+
+export function canEnableReality(values: CapabilityProtocolSlice): boolean {
+ if (!REALITY_ELIGIBLE_PROTOCOLS.includes(values.protocol)) return false;
+ return REALITY_NETWORKS.includes(values.streamSettings?.network ?? '');
+}
+
+export function canEnableTlsFlow(values: CapabilityProtocolSlice): boolean {
+ const security = values.streamSettings?.security;
+ if (security !== 'tls' && security !== 'reality') return false;
+ if (values.streamSettings?.network !== 'tcp') return false;
+ return values.protocol === 'vless';
+}
+
+export function canEnableStream(values: { protocol: string }): boolean {
+ return STREAM_PROTOCOLS.includes(values.protocol);
+}
+
+// Vision seed applies only when XTLS Vision (TCP/TLS) flow is selected
+// AND at least one VLESS client uses the vision flow. Excludes UDP variant.
+export function canEnableVisionSeed(values: CapabilityVlessSlice): boolean {
+ if (!canEnableTlsFlow(values)) return false;
+ const clients = values.settings?.clients;
+ if (!Array.isArray(clients)) return false;
+ return clients.some((c) => c?.flow === VISION_FLOW);
+}
+
+// Why: legacy returns true on non-SS protocols too (the method getter
+// resolves to "" and "" !== blake3-chacha20-poly1305). Preserved for
+// parity with the legacy class; in practice the callers all narrow on
+// protocol === shadowsocks before checking.
+export function isSSMultiUser(values: CapabilityShadowsocksSlice): boolean {
+ const method = values.protocol === 'shadowsocks' ? (values.settings?.method ?? '') : '';
+ return method !== SS_BLAKE3_CHACHA20;
+}
+
+export function isSS2022(values: CapabilityShadowsocksSlice): boolean {
+ const method = values.protocol === 'shadowsocks' ? (values.settings?.method ?? '') : '';
+ return method.substring(0, 4) === SS_2022_PREFIX;
+}
diff --git a/frontend/src/test/protocol-capabilities.test.ts b/frontend/src/test/protocol-capabilities.test.ts
new file mode 100644
index 00000000..77520f77
--- /dev/null
+++ b/frontend/src/test/protocol-capabilities.test.ts
@@ -0,0 +1,85 @@
+///
+import { describe, expect, it } from 'vitest';
+
+import { Inbound } from '@/models/inbound';
+import {
+ canEnableTls,
+ canEnableReality,
+ canEnableTlsFlow,
+ canEnableStream,
+ canEnableVisionSeed,
+ isSS2022,
+ isSSMultiUser,
+} from '@/lib/xray/protocol-capabilities';
+
+// Parity harness for the capability predicates. For each golden fixture
+// (protocol+settings), cross with a matrix of stream configurations
+// (network × security), build the legacy Inbound class via fromJson, and
+// assert each pure-function predicate matches the class method.
+//
+// Only the (protocol × stream-shape) cross matters here — the predicates
+// never read sniffing/port/listen, so we hold those constant.
+
+const fixtures = import.meta.glob(
+ './golden/fixtures/inbound/*.json',
+ { eager: true, import: 'default' },
+);
+
+interface FixtureShape { protocol: string; settings: Record }
+
+const STREAM_CASES: { network: string; security: string }[] = [
+ { network: 'tcp', security: 'none' },
+ { network: 'tcp', security: 'tls' },
+ { network: 'tcp', security: 'reality' },
+ { network: 'ws', security: 'none' },
+ { network: 'ws', security: 'tls' },
+ { network: 'grpc', security: 'none' },
+ { network: 'grpc', security: 'tls' },
+ { network: 'grpc', security: 'reality' },
+ { network: 'kcp', security: 'none' },
+ { network: 'httpupgrade', security: 'none' },
+ { network: 'httpupgrade', security: 'tls' },
+ { network: 'xhttp', security: 'none' },
+ { network: 'xhttp', security: 'tls' },
+ { network: 'xhttp', security: 'reality' },
+];
+
+function fixtureName(path: string): string {
+ return (path.split('/').pop() ?? path).replace(/\.json$/, '');
+}
+
+describe('protocol capability predicates: pure ↔ legacy parity', () => {
+ const entries = Object.entries(fixtures).sort(([a], [b]) => a.localeCompare(b));
+ for (const [path, raw] of entries) {
+ const name = fixtureName(path);
+ const fix = raw as FixtureShape;
+
+ for (const stream of STREAM_CASES) {
+
+ it(`${name} :: ${stream.network}/${stream.security}`, () => {
+ const wireConfig = {
+ port: 12345,
+ listen: '127.0.0.1',
+ protocol: fix.protocol,
+ settings: fix.settings,
+ streamSettings: { network: stream.network, security: stream.security },
+ sniffing: {},
+ };
+ const legacy = Inbound.fromJson(wireConfig);
+ const values = {
+ protocol: fix.protocol,
+ streamSettings: { network: stream.network, security: stream.security },
+ settings: fix.settings,
+ };
+
+ expect(canEnableTls(values)).toBe(legacy.canEnableTls());
+ expect(canEnableReality(values)).toBe(legacy.canEnableReality());
+ expect(canEnableTlsFlow(values)).toBe(legacy.canEnableTlsFlow());
+ expect(canEnableStream(values)).toBe(legacy.canEnableStream());
+ expect(canEnableVisionSeed(values)).toBe(legacy.canEnableVisionSeed());
+ expect(isSS2022(values)).toBe(legacy.isSS2022);
+ expect(isSSMultiUser(values)).toBe(legacy.isSSMultiUser);
+ });
+ }
+ }
+});