refactor(frontend): extract createDefault*Client factories to lib/xray

Next Step 3d slice. Five plain-object factories — Vless, Vmess, Trojan,
Shadowsocks, Hysteria — replace the legacy
`new Inbound.<Protocol>Settings.<Protocol>(...)` constructor chain and the
ClientBase XrayCommonClass machinery. Each factory takes an optional
seed; missing random fields (id, password, auth, email, subId) fall
through to RandomUtil at call time. Forms can hand-pick a UUID; tests
pass deterministic seeds so the suite never touches window.crypto.

Tests double-verify each factory: a snapshot locks the exact shape, and
the matching Zod ClientSchema.parse(out) must equal `out` — no missing
defaults, no stray fields, type-narrowed end-to-end.

Discovered: VmessClientSchema and VlessClientSchema enforce z.uuid()
format, so the test seeds use real-shape UUIDs.

Suite: 49 tests across 6 files; typecheck + lint clean. Outbound and
inbound-settings factories follow in subsequent turns alongside the
toShareLink extraction.
This commit is contained in:
MHSanaei 2026-05-25 23:42:30 +02:00
parent 922a442264
commit 8d5d11cafc
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
3 changed files with 268 additions and 0 deletions

View file

@ -0,0 +1,122 @@
import { RandomUtil } from '@/utils';
import type { HysteriaClient } from '@/schemas/protocols/inbound/hysteria';
import type { ShadowsocksClient } from '@/schemas/protocols/inbound/shadowsocks';
import type { TrojanClient } from '@/schemas/protocols/inbound/trojan';
import type { VlessClient } from '@/schemas/protocols/inbound/vless';
import type { VmessClient } from '@/schemas/protocols/inbound/vmess';
// Plain-object factories for protocol clients. Each returns a Zod-parsable
// object matching the wire shape. Random fields (id, password, auth,
// email, subId) call RandomUtil at invocation time — pass them in
// `overrides` for deterministic tests or for forms that pre-seed values.
//
// These replace the legacy `new Inbound.<Settings>.<Client>()` constructors
// and the Inbound.ClientBase machinery. Callers no longer carry the
// XrayCommonClass dependency once the swap lands.
interface ClientBaseSeed {
email?: string;
subId?: string;
limitIp?: number;
totalGB?: number;
expiryTime?: number;
enable?: boolean;
tgId?: number;
comment?: string;
reset?: number;
}
interface ClientBase {
email: string;
limitIp: number;
totalGB: number;
expiryTime: number;
enable: boolean;
tgId: number;
subId: string;
comment: string;
reset: number;
}
function clientBase(seed: ClientBaseSeed = {}): ClientBase {
return {
email: seed.email ?? RandomUtil.randomLowerAndNum(8),
limitIp: seed.limitIp ?? 0,
totalGB: seed.totalGB ?? 0,
expiryTime: seed.expiryTime ?? 0,
enable: seed.enable ?? true,
tgId: seed.tgId ?? 0,
subId: seed.subId ?? RandomUtil.randomLowerAndNum(16),
comment: seed.comment ?? '',
reset: seed.reset ?? 0,
};
}
export interface VlessClientSeed extends ClientBaseSeed {
id?: string;
flow?: VlessClient['flow'];
}
export function createDefaultVlessClient(seed: VlessClientSeed = {}): VlessClient {
return {
id: seed.id ?? RandomUtil.randomUUID(),
flow: seed.flow ?? '',
...clientBase(seed),
};
}
export interface VmessClientSeed extends ClientBaseSeed {
id?: string;
security?: VmessClient['security'];
}
export function createDefaultVmessClient(seed: VmessClientSeed = {}): VmessClient {
return {
id: seed.id ?? RandomUtil.randomUUID(),
security: seed.security ?? 'auto',
...clientBase(seed),
};
}
export interface TrojanClientSeed extends ClientBaseSeed {
password?: string;
}
export function createDefaultTrojanClient(seed: TrojanClientSeed = {}): TrojanClient {
return {
password: seed.password ?? RandomUtil.randomSeq(10),
...clientBase(seed),
};
}
export interface ShadowsocksClientSeed extends ClientBaseSeed {
method?: string;
password?: string;
ssMethod?: string;
}
// Shadowsocks clients ship with an empty `method` on single-user inbounds
// (the parent inbound's method is authoritative); only 2022-blake3 multi-
// user inbounds use the per-client method. Callers pass `ssMethod` to seed
// a method-specific password length when creating a multi-user client.
export function createDefaultShadowsocksClient(seed: ShadowsocksClientSeed = {}): ShadowsocksClient {
const method = seed.method ?? '';
const password = seed.password ?? RandomUtil.randomShadowsocksPassword(seed.ssMethod ?? '2022-blake3-aes-256-gcm');
return {
method,
password,
...clientBase(seed),
};
}
export interface HysteriaClientSeed extends ClientBaseSeed {
auth?: string;
}
export function createDefaultHysteriaClient(seed: HysteriaClientSeed = {}): HysteriaClient {
return {
auth: seed.auth ?? RandomUtil.randomSeq(10),
...clientBase(seed),
};
}

View file

@ -0,0 +1,79 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`createDefaultHysteriaClient > produces a Zod-valid client 1`] = `
{
"auth": "fixed-hyst-auth",
"comment": "",
"email": "fixture@example.test",
"enable": true,
"expiryTime": 0,
"limitIp": 0,
"reset": 0,
"subId": "fixed-sub-id-1234",
"tgId": 0,
"totalGB": 0,
}
`;
exports[`createDefaultShadowsocksClient > produces a Zod-valid client 1`] = `
{
"comment": "",
"email": "fixture@example.test",
"enable": true,
"expiryTime": 0,
"limitIp": 0,
"method": "",
"password": "ZmFrZS1zcy1wYXNzd29yZA==",
"reset": 0,
"subId": "fixed-sub-id-1234",
"tgId": 0,
"totalGB": 0,
}
`;
exports[`createDefaultTrojanClient > produces a Zod-valid client 1`] = `
{
"comment": "",
"email": "fixture@example.test",
"enable": true,
"expiryTime": 0,
"limitIp": 0,
"password": "fixed-trojan-pw",
"reset": 0,
"subId": "fixed-sub-id-1234",
"tgId": 0,
"totalGB": 0,
}
`;
exports[`createDefaultVlessClient > produces a Zod-valid client 1`] = `
{
"comment": "",
"email": "fixture@example.test",
"enable": true,
"expiryTime": 0,
"flow": "",
"id": "11111111-2222-4333-8444-555555555555",
"limitIp": 0,
"reset": 0,
"subId": "fixed-sub-id-1234",
"tgId": 0,
"totalGB": 0,
}
`;
exports[`createDefaultVmessClient > produces a Zod-valid client 1`] = `
{
"comment": "",
"email": "fixture@example.test",
"enable": true,
"expiryTime": 0,
"id": "aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee",
"limitIp": 0,
"reset": 0,
"security": "auto",
"subId": "fixed-sub-id-1234",
"tgId": 0,
"totalGB": 0,
}
`;

View file

@ -0,0 +1,67 @@
import { describe, expect, it } from 'vitest';
import {
createDefaultHysteriaClient,
createDefaultShadowsocksClient,
createDefaultTrojanClient,
createDefaultVlessClient,
createDefaultVmessClient,
} from '@/lib/xray/inbound-defaults';
import { HysteriaClientSchema } from '@/schemas/protocols/inbound/hysteria';
import { ShadowsocksClientSchema } from '@/schemas/protocols/inbound/shadowsocks';
import { TrojanClientSchema } from '@/schemas/protocols/inbound/trojan';
import { VlessClientSchema } from '@/schemas/protocols/inbound/vless';
import { VmessClientSchema } from '@/schemas/protocols/inbound/vmess';
// Tests pass explicit seeds for every random field so the assertions don't
// depend on window.crypto (the node test env has no crypto.randomUUID).
// Each factory is verified two ways:
// 1. snapshot — locks the exact shape
// 2. Zod parse round-trip — confirms the factory output is a valid
// member of the protocol's client schema (no missing defaults, no
// stray fields)
const seed = {
email: 'fixture@example.test',
subId: 'fixed-sub-id-1234',
};
describe('createDefaultVlessClient', () => {
it('produces a Zod-valid client', () => {
const c = createDefaultVlessClient({ ...seed, id: '11111111-2222-4333-8444-555555555555' });
expect(c).toMatchSnapshot();
expect(VlessClientSchema.parse(c)).toEqual(c);
});
});
describe('createDefaultVmessClient', () => {
it('produces a Zod-valid client', () => {
const c = createDefaultVmessClient({ ...seed, id: 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee' });
expect(c).toMatchSnapshot();
expect(VmessClientSchema.parse(c)).toEqual(c);
});
});
describe('createDefaultTrojanClient', () => {
it('produces a Zod-valid client', () => {
const c = createDefaultTrojanClient({ ...seed, password: 'fixed-trojan-pw' });
expect(c).toMatchSnapshot();
expect(TrojanClientSchema.parse(c)).toEqual(c);
});
});
describe('createDefaultShadowsocksClient', () => {
it('produces a Zod-valid client', () => {
const c = createDefaultShadowsocksClient({ ...seed, password: 'ZmFrZS1zcy1wYXNzd29yZA==' });
expect(c).toMatchSnapshot();
expect(ShadowsocksClientSchema.parse(c)).toEqual(c);
});
});
describe('createDefaultHysteriaClient', () => {
it('produces a Zod-valid client', () => {
const c = createDefaultHysteriaClient({ ...seed, auth: 'fixed-hyst-auth' });
expect(c).toMatchSnapshot();
expect(HysteriaClientSchema.parse(c)).toEqual(c);
});
});