mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-24 20:07:13 +00:00
docs: vendor the documentation site into the monorepo
Fold the standalone 3x-ui-docs project (Next.js 16 + Fumadocs, deployed to docs.sanaei.dev) into docs/ so the panel and its documentation share a single source of truth, the way sing-box keeps its docs in-tree. The old repo becomes redundant and can be retired. - Import the full site under docs/ (app, components, content, lib, public, scripts, config). The self-contained pnpm project sits alongside the existing engineering notes with no filename collisions. - Re-point "Edit on GitHub" links from MHSanaei/3x-ui-docs to this repo's docs/content/docs path (docs/lib/shared.ts, docs/app/.../page.tsx). - Add docs-ci.yml and docs-deploy.yml under .github/workflows/, scoped to docs/** and run with working-directory: docs, since GitHub only runs workflows from the repo-root .github/. deploy-static.yml's GitHub Pages publish (CNAME docs.sanaei.dev) carries over unchanged. Follow-up (outside this commit): attach the docs.sanaei.dev custom domain to this repository's Pages (or set the Vercel project's root directory to docs), confirm the site is live from the monorepo, then delete MHSanaei/3x-ui-docs.
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
// Pure builders for Xray outbound objects, matching the wire shapes 3x-ui
|
||||
// emits (internal/util/link/outbound.go + the panel's outbound-defaults.ts):
|
||||
// - VLESS uses the FLAT settings form {address,port,id,flow,encryption}.
|
||||
// - VMess uses the vnext form {vnext:[{address,port,users:[...]}]}.
|
||||
// - trojan/ss/socks/http use {servers:[...]}.
|
||||
// - freedom/blackhole use empty settings.
|
||||
// - wireguard/warp use {secretKey,address,peers:[...],mtu}.
|
||||
// No React/DOM imports — unit-tested in Node.
|
||||
|
||||
export type OutboundKind =
|
||||
| 'freedom'
|
||||
| 'blackhole'
|
||||
| 'vless'
|
||||
| 'vmess'
|
||||
| 'trojan'
|
||||
| 'shadowsocks'
|
||||
| 'socks'
|
||||
| 'http'
|
||||
| 'wireguard'
|
||||
| 'warp';
|
||||
|
||||
export type Network = 'tcp' | 'kcp' | 'ws' | 'grpc' | 'httpupgrade' | 'xhttp';
|
||||
export type Security = 'none' | 'tls' | 'reality';
|
||||
|
||||
export interface StreamInput {
|
||||
network: Network;
|
||||
security: Security;
|
||||
host?: string;
|
||||
path?: string;
|
||||
serviceName?: string; // grpc
|
||||
sni?: string; // tls/reality serverName
|
||||
fingerprint?: string; // utls fingerprint
|
||||
publicKey?: string; // reality
|
||||
shortId?: string; // reality
|
||||
spiderX?: string; // reality
|
||||
}
|
||||
|
||||
export interface ProxyServerInput {
|
||||
address: string;
|
||||
port: number;
|
||||
id?: string; // vless / vmess uuid
|
||||
password?: string; // trojan / ss / socks / http
|
||||
method?: string; // ss cipher
|
||||
flow?: string; // vless xtls flow
|
||||
encryption?: string; // vless, default 'none'
|
||||
vmessSecurity?: string; // vmess scy, default 'auto'
|
||||
username?: string; // socks / http
|
||||
}
|
||||
|
||||
export interface WireguardInput {
|
||||
secretKey: string;
|
||||
address: string[]; // local interface addresses
|
||||
publicKey: string; // peer public key
|
||||
endpoint: string; // peer endpoint host:port
|
||||
preSharedKey?: string;
|
||||
reserved?: number[];
|
||||
mtu?: number;
|
||||
keepAlive?: number;
|
||||
}
|
||||
|
||||
export interface OutboundInput {
|
||||
kind: OutboundKind;
|
||||
tag: string;
|
||||
server?: ProxyServerInput;
|
||||
wireguard?: WireguardInput;
|
||||
stream?: StreamInput;
|
||||
domainStrategy?: string; // freedom
|
||||
}
|
||||
|
||||
// The well-known Cloudflare WARP WireGuard endpoint. The panel fills the real
|
||||
// keys/reserved bytes when you register WARP; this is the template default.
|
||||
const WARP_ENDPOINT = 'engage.cloudflareclient.com:2408';
|
||||
|
||||
// Protocols that carry a streamSettings transport in the panel.
|
||||
const STREAM_KINDS = new Set<OutboundKind>(['vless', 'vmess', 'trojan', 'shadowsocks']);
|
||||
|
||||
function toPort(port: number | undefined): number {
|
||||
const n = Number(port);
|
||||
return Number.isFinite(n) && n > 0 ? n : 443;
|
||||
}
|
||||
|
||||
export function buildStreamSettings(s: StreamInput): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = { network: s.network, security: s.security };
|
||||
|
||||
switch (s.network) {
|
||||
case 'tcp':
|
||||
out.tcpSettings = { header: { type: 'none' } };
|
||||
break;
|
||||
case 'kcp':
|
||||
out.kcpSettings = {
|
||||
mtu: 1350,
|
||||
tti: 20,
|
||||
uplinkCapacity: 5,
|
||||
downlinkCapacity: 20,
|
||||
cwndMultiplier: 1,
|
||||
maxSendingWindow: 2097152,
|
||||
};
|
||||
break;
|
||||
case 'ws': {
|
||||
const ws: Record<string, unknown> = { path: s.path || '/' };
|
||||
if (s.host) ws.host = s.host;
|
||||
out.wsSettings = ws;
|
||||
break;
|
||||
}
|
||||
case 'grpc':
|
||||
out.grpcSettings = { serviceName: s.serviceName || '', multiMode: false };
|
||||
break;
|
||||
case 'httpupgrade': {
|
||||
const hu: Record<string, unknown> = { path: s.path || '/' };
|
||||
if (s.host) hu.host = s.host;
|
||||
out.httpupgradeSettings = hu;
|
||||
break;
|
||||
}
|
||||
case 'xhttp': {
|
||||
const xh: Record<string, unknown> = { path: s.path || '/', mode: 'auto' };
|
||||
if (s.host) xh.host = s.host;
|
||||
out.xhttpSettings = xh;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (s.security === 'tls') {
|
||||
const tls: Record<string, unknown> = {};
|
||||
if (s.sni) tls.serverName = s.sni;
|
||||
if (s.fingerprint) tls.fingerprint = s.fingerprint;
|
||||
out.tlsSettings = tls;
|
||||
} else if (s.security === 'reality') {
|
||||
const reality: Record<string, unknown> = { fingerprint: s.fingerprint || 'chrome' };
|
||||
if (s.publicKey) reality.publicKey = s.publicKey;
|
||||
if (s.sni) reality.serverName = s.sni;
|
||||
if (s.shortId) reality.shortId = s.shortId;
|
||||
if (s.spiderX) reality.spiderX = s.spiderX;
|
||||
out.realitySettings = reality;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildSettings(o: OutboundInput): Record<string, unknown> {
|
||||
const s = o.server;
|
||||
switch (o.kind) {
|
||||
case 'freedom':
|
||||
return o.domainStrategy ? { domainStrategy: o.domainStrategy } : {};
|
||||
case 'blackhole':
|
||||
return {};
|
||||
case 'vless':
|
||||
return {
|
||||
address: s?.address ?? '',
|
||||
port: toPort(s?.port),
|
||||
id: s?.id ?? '',
|
||||
flow: s?.flow ?? '',
|
||||
encryption: s?.encryption || 'none',
|
||||
};
|
||||
case 'vmess':
|
||||
return {
|
||||
vnext: [
|
||||
{
|
||||
address: s?.address ?? '',
|
||||
port: toPort(s?.port),
|
||||
users: [{ id: s?.id ?? '', security: s?.vmessSecurity || 'auto' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
case 'trojan':
|
||||
return { servers: [{ address: s?.address ?? '', port: toPort(s?.port), password: s?.password ?? '' }] };
|
||||
case 'shadowsocks':
|
||||
return {
|
||||
servers: [
|
||||
{
|
||||
address: s?.address ?? '',
|
||||
port: toPort(s?.port),
|
||||
password: s?.password ?? '',
|
||||
method: s?.method || '2022-blake3-aes-128-gcm',
|
||||
},
|
||||
],
|
||||
};
|
||||
case 'socks':
|
||||
case 'http': {
|
||||
const server: Record<string, unknown> = { address: s?.address ?? '', port: toPort(s?.port) };
|
||||
server.users = s?.username ? [{ user: s.username, pass: s.password ?? '' }] : [];
|
||||
return { servers: [server] };
|
||||
}
|
||||
case 'wireguard':
|
||||
case 'warp':
|
||||
return buildWireguardSettings(o);
|
||||
}
|
||||
}
|
||||
|
||||
function buildWireguardSettings(o: OutboundInput): Record<string, unknown> {
|
||||
const w = o.wireguard;
|
||||
const isWarp = o.kind === 'warp';
|
||||
const peer: Record<string, unknown> = {
|
||||
publicKey: w?.publicKey ?? '',
|
||||
endpoint: w?.endpoint || (isWarp ? WARP_ENDPOINT : ''),
|
||||
allowedIPs: ['0.0.0.0/0', '::/0'],
|
||||
};
|
||||
if (w?.preSharedKey) peer.preSharedKey = w.preSharedKey;
|
||||
if (w?.keepAlive) peer.keepAlive = w.keepAlive;
|
||||
|
||||
const settings: Record<string, unknown> = {
|
||||
secretKey: w?.secretKey ?? '',
|
||||
address: w?.address ?? [],
|
||||
peers: [peer],
|
||||
mtu: w?.mtu ?? 1420,
|
||||
};
|
||||
if (w?.reserved && w.reserved.length > 0) settings.reserved = w.reserved;
|
||||
return settings;
|
||||
}
|
||||
|
||||
function protocolFor(kind: OutboundKind): string {
|
||||
return kind === 'warp' ? 'wireguard' : kind;
|
||||
}
|
||||
|
||||
export function buildOutbound(o: OutboundInput): Record<string, unknown> {
|
||||
const tag = o.kind === 'warp' ? 'warp' : o.tag;
|
||||
const ob: Record<string, unknown> = {
|
||||
tag,
|
||||
protocol: protocolFor(o.kind),
|
||||
settings: buildSettings(o),
|
||||
};
|
||||
if (o.stream && STREAM_KINDS.has(o.kind)) {
|
||||
ob.streamSettings = buildStreamSettings(o.stream);
|
||||
}
|
||||
return ob;
|
||||
}
|
||||
|
||||
export function buildOutboundJson(o: OutboundInput): string {
|
||||
return JSON.stringify(buildOutbound(o), null, 2);
|
||||
}
|
||||
Reference in New Issue
Block a user