🐛 fix: Add protocol-related constants, default configurations, field definitions, and validation modes

This commit is contained in:
web 2025-09-16 04:50:05 -07:00
parent 6991b69d40
commit d6854076fe
7 changed files with 434 additions and 374 deletions

View File

@ -0,0 +1,99 @@
export const protocols = [
'shadowsocks',
'vmess',
'vless',
'trojan',
'hysteria2',
'tuic',
'anytls',
'socks',
'naive',
'http',
'mieru',
] as const;
// Global label map for display; fallback to raw value if missing
export const LABELS = {
// transport
'tcp': 'TCP',
'udp': 'UDP',
'websocket': 'WebSocket',
'grpc': 'gRPC',
'mkcp': 'mKCP',
'httpupgrade': 'HTTP Upgrade',
'xhttp': 'XHTTP',
// security
'none': 'NONE',
'tls': 'TLS',
'reality': 'Reality',
// fingerprint
'chrome': 'Chrome',
'firefox': 'Firefox',
'safari': 'Safari',
'ios': 'IOS',
'android': 'Android',
'edge': 'edge',
'360': '360',
'qq': 'QQ',
// multiplex
'low': 'Low',
'middle': 'Middle',
'high': 'High',
} as const;
// Flat arrays for enum-like sets
export const SS_CIPHERS = [
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'chacha20-ietf-poly1305',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
'2022-blake3-chacha20-poly1305',
] as const;
export const TRANSPORTS = {
vmess: ['tcp', 'websocket', 'grpc'] as const,
vless: ['tcp', 'websocket', 'grpc', 'mkcp', 'httpupgrade', 'xhttp'] as const,
trojan: ['tcp', 'websocket', 'grpc'] as const,
mieru: ['tcp', 'udp'] as const,
} as const;
export const SECURITY = {
vmess: ['none', 'tls'] as const,
vless: ['none', 'tls', 'reality'] as const,
trojan: ['tls'] as const,
hysteria2: ['tls'] as const,
tuic: ['tls'] as const,
anytls: ['tls'] as const,
naive: ['none', 'tls'] as const,
http: ['none', 'tls'] as const,
} as const;
export const FLOWS = {
vless: ['none', 'xtls-rprx-direct', 'xtls-rprx-splice', 'xtls-rprx-vision'] as const,
} as const;
export const TUIC_UDP_RELAY_MODES = ['native', 'quic'] as const;
export const TUIC_CONGESTION = ['bbr', 'cubic', 'new_reno'] as const;
export const XHTTP_MODES = ['auto', 'packet-up', 'stream-up', 'stream-one'] as const;
export const ENCRYPTION_TYPES = ['none', 'mlkem768x25519plus'] as const;
export const ENCRYPTION_MODES = ['native', 'xorpub', 'random'] as const;
export const ENCRYPTION_RTT = ['0rtt', '1rtt'] as const;
export const FINGERPRINTS = [
'chrome',
'firefox',
'safari',
'ios',
'android',
'edge',
'360',
'qq',
] as const;
export const multiplexLevels = ['none', 'low', 'middle', 'high'] as const;
export function getLabel(value: string): string {
const label = (LABELS as Record<string, string>)[value];
return label ?? value.toUpperCase();
}

View File

@ -0,0 +1,83 @@
import type { ProtocolType } from './types';
export function getProtocolDefaultConfig(proto: ProtocolType) {
switch (proto) {
case 'shadowsocks':
return {
type: 'shadowsocks',
port: null,
cipher: 'chacha20-ietf-poly1305',
server_key: null,
obfs: 'none',
obfs_host: null,
obfs_path: null,
} as any;
case 'vmess':
return { type: 'vmess', port: null, transport: 'tcp', security: 'none' } as any;
case 'vless':
return { type: 'vless', port: null, transport: 'tcp', security: 'none', flow: 'none' } as any;
case 'trojan':
return { type: 'trojan', port: null, transport: 'tcp', security: 'tls' } as any;
case 'hysteria2':
return {
type: 'hysteria2',
port: null,
hop_ports: null,
hop_interval: null,
obfs: 'none',
obfs_password: null,
security: 'tls',
up_mbps: null,
down_mbps: null,
} as any;
case 'tuic':
return {
type: 'tuic',
port: null,
disable_sni: false,
reduce_rtt: false,
udp_relay_mode: 'native',
congestion_controller: 'bbr',
security: 'tls',
sni: null,
allow_insecure: false,
fingerprint: 'chrome',
} as any;
case 'socks':
return {
type: 'socks',
port: null,
} as any;
case 'naive':
return {
type: 'naive',
port: null,
security: 'none',
} as any;
case 'http':
return {
type: 'http',
port: null,
security: 'none',
} as any;
case 'mieru':
return {
type: 'mieru',
port: null,
multiplex: 'none',
transport: 'tcp',
} as any;
case 'anytls':
return {
type: 'anytls',
port: null,
security: 'tls',
padding_scheme: null,
sni: null,
allow_insecure: false,
fingerprint: 'chrome',
} as any;
default:
return {} as any;
}
}

View File

@ -1,378 +1,24 @@
import { z } from 'zod';
import { import {
generateMLKEM768KeyPair, generateMLKEM768KeyPair,
generatePassword, generatePassword,
generateRealityKeyPair, generateRealityKeyPair,
generateRealityShortId, generateRealityShortId,
} from './generate'; } from '../generate';
import {
export const protocols = [ ENCRYPTION_MODES,
'shadowsocks', ENCRYPTION_RTT,
'vmess', ENCRYPTION_TYPES,
'vless', FINGERPRINTS,
'trojan', FLOWS,
'hysteria2', multiplexLevels,
'tuic', SECURITY,
'anytls', SS_CIPHERS,
'socks', TRANSPORTS,
'naive', TUIC_CONGESTION,
'http', TUIC_UDP_RELAY_MODES,
'meru', XHTTP_MODES,
] as const; } from './constants';
import type { FieldConfig } from './types';
export type FieldConfig = {
name: string;
type: 'input' | 'select' | 'switch' | 'number' | 'textarea';
label: string;
placeholder?: string | ((t: (key: string) => string, protocol: any) => string);
options?: readonly string[];
defaultValue?: any;
min?: number;
max?: number;
step?: number;
suffix?: string;
generate?: {
function: () => any;
updateFields?: Record<string, string>;
};
condition?: (protocol: any, values: any) => boolean;
group?: 'basic' | 'transport' | 'security' | 'reality' | 'obfs' | 'encryption';
gridSpan?: 1 | 2;
};
// Global label map for display; fallback to raw value if missing
export const LABELS = {
// transport
'tcp': 'TCP',
'udp': 'UDP',
'websocket': 'WebSocket',
'grpc': 'gRPC',
'mkcp': 'mKCP',
'httpupgrade': 'HTTP Upgrade',
'xhttp': 'XHTTP',
// security
'none': 'NONE',
'tls': 'TLS',
'reality': 'Reality',
// fingerprint
'chrome': 'Chrome',
'firefox': 'Firefox',
'safari': 'Safari',
'ios': 'IOS',
'android': 'Android',
'edge': 'edge',
'360': '360',
'qq': 'QQ',
// multiplex
'low': 'Low',
'middle': 'Middle',
'high': 'High',
} as const;
// Flat arrays for enum-like sets
export const SS_CIPHERS = [
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'chacha20-ietf-poly1305',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
'2022-blake3-chacha20-poly1305',
] as const;
export const TRANSPORTS = {
vmess: ['tcp', 'websocket', 'grpc'] as const,
vless: ['tcp', 'websocket', 'grpc', 'mkcp', 'httpupgrade', 'xhttp'] as const,
trojan: ['tcp', 'websocket', 'grpc'] as const,
meru: ['tcp', 'udp'] as const,
} as const;
export const SECURITY = {
vmess: ['none', 'tls'] as const,
vless: ['none', 'tls', 'reality'] as const,
trojan: ['tls'] as const,
hysteria2: ['tls'] as const,
tuic: ['tls'] as const,
anytls: ['tls'] as const,
naive: ['none', 'tls'] as const,
http: ['none', 'tls'] as const,
} as const;
export const FLOWS = {
vless: ['none', 'xtls-rprx-direct', 'xtls-rprx-splice', 'xtls-rprx-vision'] as const,
} as const;
export const TUIC_UDP_RELAY_MODES = ['native', 'quic'] as const;
export const TUIC_CONGESTION = ['bbr', 'cubic', 'new_reno'] as const;
export const XHTTP_MODES = ['auto', 'packet-up', 'stream-up', 'stream-one'] as const;
export const ENCRYPTION_TYPES = ['none', 'mlkem768x25519plus'] as const;
export const ENCRYPTION_MODES = ['native', 'xorpub', 'random'] as const;
export const ENCRYPTION_RTT = ['0rtt', '1rtt'] as const;
export const FINGERPRINTS = [
'chrome',
'firefox',
'safari',
'ios',
'android',
'edge',
'360',
'qq',
] as const;
export const multiplexLevels = ['none', 'low', 'middle', 'high'] as const;
export function getLabel(value: string): string {
const label = (LABELS as Record<string, string>)[value];
return label ?? value.toUpperCase();
}
const nullableString = z.string().nullish();
const nullableBool = z.boolean().nullish();
const nullablePort = z.number().int().min(0).max(65535).nullish();
const ss = z.object({
type: z.literal('shadowsocks'),
host: nullableString,
port: nullablePort,
cipher: z.enum(SS_CIPHERS as any).nullish(),
server_key: nullableString,
obfs: z.enum(['none', 'http', 'tls'] as const).nullish(),
obfs_host: nullableString,
obfs_path: nullableString,
});
const vmess = z.object({
type: z.literal('vmess'),
host: nullableString,
port: nullablePort,
transport: z.enum(TRANSPORTS.vmess as any).nullish(),
security: z.enum(SECURITY.vmess as any).nullish(),
path: nullableString,
service_name: nullableString,
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
});
const vless = z.object({
type: z.literal('vless'),
host: nullableString,
port: nullablePort,
transport: z.enum(TRANSPORTS.vless as any).nullish(),
security: z.enum(SECURITY.vless as any).nullish(),
path: nullableString,
service_name: nullableString,
flow: z.enum(FLOWS.vless as any).nullish(),
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
reality_server_addr: nullableString,
reality_server_port: nullablePort,
reality_private_key: nullableString,
reality_public_key: nullableString,
reality_short_id: nullableString,
mode: nullableString,
extra: nullableString,
encryption: z.enum(ENCRYPTION_TYPES as any).nullish(),
encryption_mode: z.enum(ENCRYPTION_MODES as any).nullish(),
encryption_rtt: z.enum(ENCRYPTION_RTT as any).nullish(),
encryption_ticket: nullableString,
encryption_server_padding: nullableString,
encryption_private_key: nullableString,
encryption_client_padding: nullableString,
encryption_password: nullableString,
});
const trojan = z.object({
type: z.literal('trojan'),
host: nullableString,
port: nullablePort,
transport: z.enum(TRANSPORTS.trojan as any).nullish(),
security: z.enum(SECURITY.trojan as any).nullish(),
path: nullableString,
service_name: nullableString,
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
});
const hysteria2 = z.object({
type: z.literal('hysteria2'),
hop_ports: nullableString,
hop_interval: z.number().nullish(),
obfs_password: nullableString,
obfs: z.enum(['none', 'salamander'] as const).nullish(),
port: nullablePort,
security: z.enum(SECURITY.hysteria2 as any).nullish(),
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
up_mbps: z.number().nullish(),
down_mbps: z.number().nullish(),
});
const tuic = z.object({
type: z.literal('tuic'),
host: nullableString,
port: nullablePort,
disable_sni: z.boolean().nullish(),
reduce_rtt: z.boolean().nullish(),
udp_relay_mode: z.enum(TUIC_UDP_RELAY_MODES as any).nullish(),
congestion_controller: z.enum(TUIC_CONGESTION as any).nullish(),
security: z.enum(SECURITY.tuic as any).nullish(),
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
});
const anytls = z.object({
type: z.literal('anytls'),
port: nullablePort,
security: z.enum(SECURITY.anytls as any).nullish(),
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
padding_scheme: nullableString,
});
const socks = z.object({
type: z.literal('socks'),
port: nullablePort,
});
const naive = z.object({
type: z.literal('naive'),
port: nullablePort,
security: z.enum(SECURITY.naive as any).nullish(),
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
});
const http = z.object({
type: z.literal('http'),
port: nullablePort,
security: z.enum(SECURITY.http as any).nullish(),
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
});
const meru = z.object({
type: z.literal('meru'),
port: nullablePort,
multiplex: z.enum(multiplexLevels).nullish(),
transport: z.enum(TRANSPORTS.meru as any).nullish(),
});
export const protocolApiScheme = z.discriminatedUnion('type', [
ss,
vmess,
vless,
trojan,
hysteria2,
tuic,
anytls,
socks,
naive,
http,
meru,
]);
export const formSchema = z.object({
name: z.string().min(1),
address: z.string().min(1),
country: z.string().optional(),
city: z.string().optional(),
ratio: z.number().default(1),
protocols: z.array(protocolApiScheme),
});
export type ServerFormValues = z.infer<typeof formSchema>;
export type ProtocolType = (typeof protocols)[number];
export function getProtocolDefaultConfig(proto: ProtocolType) {
switch (proto) {
case 'shadowsocks':
return {
type: 'shadowsocks',
port: null,
cipher: 'chacha20-ietf-poly1305',
server_key: null,
obfs: 'none',
obfs_host: null,
obfs_path: null,
} as any;
case 'vmess':
return { type: 'vmess', port: null, transport: 'tcp', security: 'none' } as any;
case 'vless':
return { type: 'vless', port: null, transport: 'tcp', security: 'none', flow: 'none' } as any;
case 'trojan':
return { type: 'trojan', port: null, transport: 'tcp', security: 'tls' } as any;
case 'hysteria2':
return {
type: 'hysteria2',
port: null,
hop_ports: null,
hop_interval: null,
obfs: 'none',
obfs_password: null,
security: 'tls',
up_mbps: null,
down_mbps: null,
} as any;
case 'tuic':
return {
type: 'tuic',
port: null,
disable_sni: false,
reduce_rtt: false,
udp_relay_mode: 'native',
congestion_controller: 'bbr',
security: 'tls',
sni: null,
allow_insecure: false,
fingerprint: 'chrome',
} as any;
case 'socks':
return {
type: 'socks',
port: null,
} as any;
case 'naive':
return {
type: 'naive',
port: null,
security: 'none',
} as any;
case 'http':
return {
type: 'http',
port: null,
security: 'none',
} as any;
case 'meru':
return {
type: 'meru',
port: null,
multiplex: 'none',
transport: 'tcp',
} as any;
case 'anytls':
return {
type: 'anytls',
port: null,
security: 'tls',
padding_scheme: null,
sni: null,
allow_insecure: false,
fingerprint: 'chrome',
} as any;
default:
return {} as any;
}
}
export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = { export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
shadowsocks: [ shadowsocks: [
@ -1010,7 +656,7 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
condition: (p) => p.security !== 'none', condition: (p) => p.security !== 'none',
}, },
], ],
meru: [ mieru: [
{ {
name: 'port', name: 'port',
type: 'number', type: 'number',
@ -1032,7 +678,7 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
name: 'transport', name: 'transport',
type: 'select', type: 'select',
label: 'transport', label: 'transport',
options: TRANSPORTS.meru, options: TRANSPORTS.mieru,
defaultValue: 'tcp', defaultValue: 'tcp',
group: 'transport', group: 'transport',
}, },

View File

@ -0,0 +1,30 @@
// Re-export all constants
export {
ENCRYPTION_MODES,
ENCRYPTION_RTT,
ENCRYPTION_TYPES,
FINGERPRINTS,
FLOWS,
LABELS,
SECURITY,
SS_CIPHERS,
TRANSPORTS,
TUIC_CONGESTION,
TUIC_UDP_RELAY_MODES,
XHTTP_MODES,
getLabel,
multiplexLevels,
protocols,
} from './constants';
// Re-export all types
export type { FieldConfig, ProtocolType, ServerFormValues } from './types';
// Re-export all schemas
export { formSchema, protocolApiScheme } from './schemas';
// Re-export defaults
export { getProtocolDefaultConfig } from './defaults';
// Re-export fields
export { PROTOCOL_FIELDS } from './fields';

View File

@ -0,0 +1,175 @@
import { z } from 'zod';
import {
ENCRYPTION_MODES,
ENCRYPTION_RTT,
ENCRYPTION_TYPES,
FLOWS,
multiplexLevels,
SECURITY,
SS_CIPHERS,
TRANSPORTS,
TUIC_CONGESTION,
TUIC_UDP_RELAY_MODES,
} from './constants';
const nullableString = z.string().nullish();
const nullableBool = z.boolean().nullish();
const nullablePort = z.number().int().min(0).max(65535).nullish();
const ss = z.object({
type: z.literal('shadowsocks'),
host: nullableString,
port: nullablePort,
cipher: z.enum(SS_CIPHERS as any).nullish(),
server_key: nullableString,
obfs: z.enum(['none', 'http', 'tls'] as const).nullish(),
obfs_host: nullableString,
obfs_path: nullableString,
});
const vmess = z.object({
type: z.literal('vmess'),
host: nullableString,
port: nullablePort,
transport: z.enum(TRANSPORTS.vmess as any).nullish(),
security: z.enum(SECURITY.vmess as any).nullish(),
path: nullableString,
service_name: nullableString,
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
});
const vless = z.object({
type: z.literal('vless'),
host: nullableString,
port: nullablePort,
transport: z.enum(TRANSPORTS.vless as any).nullish(),
security: z.enum(SECURITY.vless as any).nullish(),
path: nullableString,
service_name: nullableString,
flow: z.enum(FLOWS.vless as any).nullish(),
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
reality_server_addr: nullableString,
reality_server_port: nullablePort,
reality_private_key: nullableString,
reality_public_key: nullableString,
reality_short_id: nullableString,
mode: nullableString,
extra: nullableString,
encryption: z.enum(ENCRYPTION_TYPES as any).nullish(),
encryption_mode: z.enum(ENCRYPTION_MODES as any).nullish(),
encryption_rtt: z.enum(ENCRYPTION_RTT as any).nullish(),
encryption_ticket: nullableString,
encryption_server_padding: nullableString,
encryption_private_key: nullableString,
encryption_client_padding: nullableString,
encryption_password: nullableString,
});
const trojan = z.object({
type: z.literal('trojan'),
host: nullableString,
port: nullablePort,
transport: z.enum(TRANSPORTS.trojan as any).nullish(),
security: z.enum(SECURITY.trojan as any).nullish(),
path: nullableString,
service_name: nullableString,
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
});
const hysteria2 = z.object({
type: z.literal('hysteria2'),
hop_ports: nullableString,
hop_interval: z.number().nullish(),
obfs_password: nullableString,
obfs: z.enum(['none', 'salamander'] as const).nullish(),
port: nullablePort,
security: z.enum(SECURITY.hysteria2 as any).nullish(),
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
up_mbps: z.number().nullish(),
down_mbps: z.number().nullish(),
});
const tuic = z.object({
type: z.literal('tuic'),
host: nullableString,
port: nullablePort,
disable_sni: z.boolean().nullish(),
reduce_rtt: z.boolean().nullish(),
udp_relay_mode: z.enum(TUIC_UDP_RELAY_MODES as any).nullish(),
congestion_controller: z.enum(TUIC_CONGESTION as any).nullish(),
security: z.enum(SECURITY.tuic as any).nullish(),
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
});
const anytls = z.object({
type: z.literal('anytls'),
port: nullablePort,
security: z.enum(SECURITY.anytls as any).nullish(),
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
padding_scheme: nullableString,
});
const socks = z.object({
type: z.literal('socks'),
port: nullablePort,
});
const naive = z.object({
type: z.literal('naive'),
port: nullablePort,
security: z.enum(SECURITY.naive as any).nullish(),
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
});
const http = z.object({
type: z.literal('http'),
port: nullablePort,
security: z.enum(SECURITY.http as any).nullish(),
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
});
const mieru = z.object({
type: z.literal('mieru'),
port: nullablePort,
multiplex: z.enum(multiplexLevels).nullish(),
transport: z.enum(TRANSPORTS.mieru as any).nullish(),
});
export const protocolApiScheme = z.discriminatedUnion('type', [
ss,
vmess,
vless,
trojan,
hysteria2,
tuic,
anytls,
socks,
naive,
http,
mieru,
]);
export const formSchema = z.object({
name: z.string().min(1),
address: z.string().min(1),
country: z.string().optional(),
city: z.string().optional(),
ratio: z.number().default(1),
protocols: z.array(protocolApiScheme),
});

View File

@ -0,0 +1,27 @@
import { z } from 'zod';
import { protocols } from './constants';
import { formSchema } from './schemas';
export type FieldConfig = {
name: string;
type: 'input' | 'select' | 'switch' | 'number' | 'textarea';
label: string;
placeholder?: string | ((t: (key: string) => string, protocol: any) => string);
options?: readonly string[];
defaultValue?: any;
min?: number;
max?: number;
step?: number;
suffix?: string;
generate?: {
function: () => Promise<string | Record<string, string>> | string | Record<string, string>;
updateFields?: Record<string, string>;
};
condition?: (protocol: any, values: any) => boolean;
group?: 'basic' | 'transport' | 'security' | 'reality' | 'obfs' | 'encryption';
gridSpan?: 1 | 2;
};
export type ServerFormValues = z.infer<typeof formSchema>;
export type ProtocolType = (typeof protocols)[number];

View File

@ -102,8 +102,8 @@ function DynamicField({
<Button <Button
type='button' type='button'
variant='ghost' variant='ghost'
onClick={() => { onClick={async () => {
const result = field.generate!.function(); const result = await field.generate!.function();
if (typeof result === 'string') { if (typeof result === 'string') {
fieldProps.onChange(result); fieldProps.onChange(result);
} else if (field.generate!.updateFields) { } else if (field.generate!.updateFields) {