feat(frontend): stream extras + full InboundSchema with DU intersection

Step 3d's last scaffolding piece before link generators. Three new
stream-extras schemas land alongside the network/security DUs:

  - finalmask: TcpMask[] + UdpMask[] + QuicParams. Mask `settings` stays
    record<string, unknown> for now — there are 13 UDP mask types and 3
    TCP mask types with distinct per-type setting shapes, and modeling
    them all as DUs would dwarf the rest of stream/ without buying
    anything the shadow harness doesn't already catch. Tightened in
    Step 6.
  - sockopt: 17 socket-tuning knobs (TCP keepalive, TFO, mark, tproxy,
    mptcp, dialer proxy, IPv6-only, congestion). `interfaceName` field
    matches the panel class naming; serializers rename to `interface` on
    the wire.
  - external-proxy: rows ship per inbound describing edge fronts (CDN
    mirrors). Used by link generators to fan out share URLs.

schemas/api/inbound.ts composes the top-level wire shape with
intersection-of-DUs:

  StreamSettingsSchema = NetworkSettingsSchema
    .and(SecuritySettingsSchema)
    .and(StreamExtrasSchema)

  InboundSchema = InboundCoreSchema.and(InboundSettingsSchema)

A fixture (vless-ws-tls.json) exercises the full shape — protocol DU,
network DU, security DU, and TLS cert file branch in one round trip.
The snapshot pins the canonical parsed form so the upcoming link
extractor consumes typed input with no class hierarchy underneath.

Suite: 65 tests across 7 files; typecheck + lint clean. Zod 4
intersection-of-DUs works.
This commit is contained in:
MHSanaei
2026-05-26 00:00:34 +02:00
parent c4f5d841b0
commit d14eb6923f
8 changed files with 435 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`InboundSchema (full) fixtures > parses vless-ws-tls byte-stably 1`] = `
{
"down": 0,
"enable": true,
"expiryTime": 0,
"id": 42,
"listen": "",
"port": 443,
"protocol": "vless",
"remark": "alice-vless-ws-tls",
"settings": {
"clients": [
{
"comment": "",
"email": "alice@example.test",
"enable": true,
"expiryTime": 0,
"flow": "",
"id": "8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02",
"limitIp": 0,
"reset": 0,
"subId": "abc123def",
"tgId": 0,
"totalGB": 0,
},
],
"decryption": "none",
"encryption": "none",
"fallbacks": [],
},
"sniffing": {
"destOverride": [
"http",
"tls",
"quic",
"fakedns",
],
"domainsExcluded": [],
"enabled": true,
"ipsExcluded": [],
"metadataOnly": false,
"routeOnly": false,
},
"streamSettings": {
"network": "ws",
"security": "tls",
"tlsSettings": {
"alpn": [
"h2",
"http/1.1",
],
"certificates": [
{
"buildChain": false,
"certificateFile": "/etc/ssl/certs/cdn.example.test.crt",
"keyFile": "/etc/ssl/private/cdn.example.test.key",
"oneTimeLoading": false,
"usage": "encipherment",
},
],
"cipherSuites": "",
"disableSystemRoot": false,
"echServerKeys": "",
"enableSessionResumption": false,
"maxVersion": "1.3",
"minVersion": "1.2",
"rejectUnknownSni": false,
"serverName": "cdn.example.test",
"settings": {
"echConfigList": "",
"fingerprint": "chrome",
},
},
"wsSettings": {
"acceptProxyProtocol": false,
"headers": {},
"heartbeatPeriod": 0,
"host": "cdn.example.test",
"path": "/ws",
},
},
"tag": "inbound-vless-1",
"total": 0,
"up": 0,
}
`;

View File

@@ -0,0 +1,76 @@
{
"id": 42,
"up": 0,
"down": 0,
"total": 0,
"remark": "alice-vless-ws-tls",
"enable": true,
"expiryTime": 0,
"listen": "",
"port": 443,
"tag": "inbound-vless-1",
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic", "fakedns"],
"metadataOnly": false,
"routeOnly": false,
"ipsExcluded": [],
"domainsExcluded": []
},
"protocol": "vless",
"settings": {
"clients": [
{
"id": "8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02",
"email": "alice@example.test",
"flow": "",
"limitIp": 0,
"totalGB": 0,
"expiryTime": 0,
"enable": true,
"tgId": 0,
"subId": "abc123def",
"comment": "",
"reset": 0
}
],
"decryption": "none",
"encryption": "none",
"fallbacks": []
},
"streamSettings": {
"network": "ws",
"wsSettings": {
"acceptProxyProtocol": false,
"path": "/ws",
"host": "cdn.example.test",
"headers": {},
"heartbeatPeriod": 0
},
"security": "tls",
"tlsSettings": {
"serverName": "cdn.example.test",
"minVersion": "1.2",
"maxVersion": "1.3",
"cipherSuites": "",
"rejectUnknownSni": false,
"disableSystemRoot": false,
"enableSessionResumption": false,
"certificates": [
{
"certificateFile": "/etc/ssl/certs/cdn.example.test.crt",
"keyFile": "/etc/ssl/private/cdn.example.test.key",
"oneTimeLoading": false,
"usage": "encipherment",
"buildChain": false
}
],
"alpn": ["h2", "http/1.1"],
"echServerKeys": "",
"settings": {
"fingerprint": "chrome",
"echConfigList": ""
}
}
}
}

View File

@@ -0,0 +1,31 @@
/// <reference types="vite/client" />
import { describe, expect, it } from 'vitest';
import { InboundSchema } from '@/schemas/api/inbound';
// Full Inbound parse tests — exercises the intersection of network DU,
// security DU, settings DU, and orthogonal extras in a single
// round-trip. These fixtures are the input the link generators in
// lib/xray/inbound-link.ts will consume once extracted.
const fixtures = import.meta.glob<unknown>(
'./golden/fixtures/inbound-full/*.json',
{ eager: true, import: 'default' },
);
function fixtureName(path: string): string {
const file = path.split('/').pop() ?? path;
return file.replace(/\.json$/, '');
}
describe('InboundSchema (full) fixtures', () => {
const entries = Object.entries(fixtures).sort(([a], [b]) => a.localeCompare(b));
expect(entries.length, 'expected at least one fixture under golden/fixtures/inbound-full').toBeGreaterThan(0);
for (const [path, raw] of entries) {
it(`parses ${fixtureName(path)} byte-stably`, () => {
const parsed = InboundSchema.parse(raw);
expect(parsed).toMatchSnapshot();
});
}
});