mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-28 22:17:13 +00:00
Move to TypeScript 7 and the oxc toolchain (oxlint + oxfmt) (#6262)
* chore(frontend,docs): move to TypeScript 7 and replace ESLint with oxlint
TypeScript 7 is the native Go port and ships no programmatic compiler
API, so typescript-eslint cannot run at all: it peer-pins
typescript >=4.8.4 <6.1.0 (canary too) and hard-crashes with
"typescript-eslint does not support TS 7.0". Upstream support is
tracked in typescript-eslint#10940 and targets TS >=7.1.
Rather than wait, or carry Microsoft's side-by-side alias (which keeps
a second TS 6 install alive purely to feed the linter), both projects
move to oxlint, which never depended on the TypeScript API.
Typecheck drops from ~9.7s to ~2.2s and 167 packages leave frontend/.
oxlint has no no-restricted-syntax, so the #6121/#6127 cleared-
InputNumber guard is reimplemented as a JS plugin in
frontend/tools/oxlint/. It was verified to still fire in
pages/settings/** and pages/xray/** and to stay exempt in *Modal.tsx.
The type-aware @deprecated sweep survives too, as
`npm run lint:deprecated`: oxlint's type-aware mode runs on
oxlint-tsgolint, which drives the TS 7 typescript-go checker, so the
TS 7 move is what makes it possible.
Behaviour is preserved rather than tightened. jsx-a11y/prefer-tag-over-role
is off in both configs because it was never part of the recommended sets
ESLint actually ran, and oxlint honours the existing eslint-disable
comments, so no source churn was needed.
Two real fixes fell out of the stricter linting:
- outbound-link-parser.test.ts used `out?.streamSettings` behind an `as`
cast, which hid the optional chain from ESLint and would throw on a
null parse; the rest of the file already used `out!`.
- InputAddon's conditional role/tabIndex/onKeyDown is genuinely
accessible but oxlint cannot evaluate it, so it gets a scoped disable.
* chore(docs): replace Prettier with oxfmt
oxfmt is the oxc project's Prettier-compatible formatter, so this pairs
with the oxlint move and drops the last JS-based tool from the docs
toolchain.
The swap is behaviour-preserving. Running Prettier and oxfmt over the
same files, with the existing .prettierrc.json settings migrated via
`oxfmt --migrate=prettier`, produces byte-identical output on every
file. (Comparing them outside the project directory is misleading:
Prettier silently falls back to its defaults when it cannot find its
config, which looks like a mismatch but is not one.)
The 18 files reformatted here were already failing `pnpm format:check`
before this change — Prettier wanted the exact same edits. The check is
not part of docs-ci.yml, which is why the drift went unnoticed.
.prettierignore becomes ignorePatterns in .oxfmtrc.json, keeping the
deliberate MDX exclusion: reflowing MDX prose merges headings into
paragraphs and collapses lists inside Steps/Callout components. Both
that and the generated fumadocs-openapi reference output were verified
untouched.
oxfmt is pinned to 0.63.0 rather than latest. pnpm 11's built-in
minimumReleaseAge policy rejects same-day releases, and 0.64.0 would
have made pnpm silently append 20 waiver lines to pnpm-workspace.yaml.
* style(frontend): adopt oxfmt and format src
frontend/ has never had a formatter, so this reformats 344 of 497 files
in src/. The change is purely whitespace, quoting and line wrapping —
no logic is touched. It is kept in its own commit so it does not bury
the TypeScript 7 / oxlint migration or the git blame for the code
itself.
Settings match docs/ and the code as it was already written: single
quotes, semicolons, trailing commas, 2-space indent, 100 columns. That
was measured rather than assumed — src/ was already uniformly
single-quoted and 2-space indented, with p90 line length at 75.
Formatting is scoped to src/ (mirroring `oxlint src`) and
.oxfmtrc.json ignores src/generated. Both matter: `make gen-check`
compares src/generated and public/openapi.json, and
`make msw-worker-check` byte-compares public/mockServiceWorker.js
against the installed MSW runtime, so reformatting any of them breaks
the gate.
Reflowing also moves `eslint-disable-next-line` comments off the line
they guard, which broke two suppressions that had been silently
correct before:
- clone-inbound-modal.test.tsx: the object literal became multi-line,
leaving `} as any;` four lines below its no-explicit-any disable.
- ClientsPage.tsx: the useMemo dependency array moved onto its own
line, out from under its exhaustive-deps disable.
Both comments were relocated onto the line they actually guard, and
verified to still suppress by removing them and watching the errors
return.
* ci: enforce formatting in CI and make verify
Adding oxfmt in the previous two commits gave both projects a formatter
but nothing that checks it, which is how docs/ had already drifted to 18
unformatted files: docs-ci.yml runs typecheck, lint, test and build, but
never format:check, so Prettier's complaints were only ever visible to
whoever ran it by hand.
Wire `format:check` into the frontend job in ci.yml and the docs job in
docs-ci.yml, and add a `format-check` target to `make verify` so the
local gate keeps mirroring CI as the Makefile header promises.
Verified the step actually bites rather than passing vacuously: adding
a badly formatted line to a source file in each project makes both
`make format-check` and `pnpm format:check` fail, and reverting it makes
them pass again.
No workflow referenced ESLint or Prettier by name — they all invoke the
package scripts — so the tooling swap needed no other CI changes.
* ci: trigger CI on Makefile changes
The path filters listed **.go, go.mod, go.sum, frontend/**, .nvmrc and
ci.yml itself, but not the Makefile — so a change to the canonical task
runner that ci.yml is meant to mirror could land without any job
running. The previous commit, which edits both, only triggers because
it happens to touch ci.yml too.
* fix(frontend): replace deprecated Ant Design 6 APIs in the geo components
`npm run lint:deprecated` reported five uses of props Ant Design 6 has
deprecated. All five are gone, and the matching runtime warnings no
longer appear in the test output.
Tag `bordered={false}` becomes `variant="filled"` and Space `direction`
becomes `orientation`; both are the one-to-one replacements named in
antd's own deprecation messages, and `direction`/`orientation` share the
same Orientation type.
Input `addonAfter` is the one that is not a rename. It becomes a
`Space.Compact block` wrapping the Input and the browse Button, which is
antd's documented migration. `block` keeps the field filling its form
row as the addon did. Note this is a deliberate visual change: the
button used to be a borderless `type="text"` icon sitting inside the
addon's grey box, and is now a regular button whose border joins the
input. The tooltip, aria-label, ref, id and onBlur wiring are unchanged,
so the react-hook-form binding in RuleFormModal and the existing tests
still address it the same way.
Only these five were deprecated. The other `bordered` props in the tree
sit on QRCode, Table, Descriptions and Alert, where the prop is not
deprecated, and these were the only two Space `direction` uses in the
codebase.
* fix(frontend): restore lint rules lost in the oxlint migration, and test the guard
Addresses the review on #6262.
The frontend config re-enabled only no-explicit-any and no-unused-vars
and left the rest of tseslint's recommended set to oxlint's correctness
category. It does not cover all of it. Confirmed by linting one probe
file against both configs: docs/ (which enumerates the rules) reports
all nine, frontend/ reported four. So ban-ts-comment,
no-empty-object-type, no-namespace, no-require-imports and
no-unsafe-function-type had silently stopped being enforced — a `//
@ts-ignore` or a `namespace` block would have landed unflagged. The ten
rules are now mirrored from docs/.oxlintrc.json, and src/ still passes.
The #6121/#6127 guard was 57 lines of hand-written AST walking with no
test. It now has one: fixtures for the three banned shapes plus an
onNumber()-wrapped control, asserting the rule fires three times and
that .oxlintrc.json still wires it to the right paths. Verified it fails
for the right reason by making walk() enumerate nothing, which is the
silent-death mode the review described — the traversal depends on
Object.keys() seeing AST children as own enumerable properties.
The fixtures deliberately violate the rule, so their oxlint config is
named guard.oxlintrc.json rather than .oxlintrc.json: oxlint discovers
nested configs by directory, which would otherwise turn the fixtures
into three lint errors. The test passes it explicitly with -c.
Also from the review:
- lint and format now cover tools/ as well as src/, so the one piece of
hand-written lint logic in the repo is no longer the least covered
file in it.
- lint-staged runs oxfmt before oxlint --fix. Formatting became a hard
CI gate in this PR while the hook only ran the linter, so a commit
could pass the hook and fail CI on formatting alone.
- .oxfmtrc.json ignores public/, so the artefacts that make gen-check
and make msw-worker-check byte-compare stay safe even if oxfmt is
invoked without a path argument.
- The MDX and generated-reference rationales that .prettierignore
carried are back as comments in docs/.oxfmtrc.json — oxlint and oxfmt
both accept JSONC, so relocating them was unnecessary.
Not applied: the review also suggested restoring ../internal/web/dist to
the ignore lists. Both tools reject `..` patterns outright ("patterns
are resolved within the config file's directory"), and being outside
frontend/ it is unreachable anyway.
This commit is contained in:
@@ -8,7 +8,10 @@ export type HostSecurity = z.infer<typeof HostSecuritySchema>;
|
||||
export const MihomoIpVersionSchema = z.enum(['dual', 'ipv4', 'ipv6', 'ipv4-prefer', 'ipv6-prefer']);
|
||||
export const SubTypeSchema = z.enum(['raw', 'json', 'clash']);
|
||||
|
||||
const HostTagSchema = z.string().regex(/^[A-Z0-9_:]+$/, 'pages.hosts.toasts.badTag').max(36);
|
||||
const HostTagSchema = z
|
||||
.string()
|
||||
.regex(/^[A-Z0-9_:]+$/, 'pages.hosts.toasts.badTag')
|
||||
.max(36);
|
||||
|
||||
export const HostFormSchema = z.object({
|
||||
id: z.number().optional(),
|
||||
@@ -66,42 +69,44 @@ export const HostFormSchema = z.object({
|
||||
});
|
||||
export type HostFormValues = z.infer<typeof HostFormSchema>;
|
||||
|
||||
export const HostRecordSchema = z.object({
|
||||
groupId: z.string(),
|
||||
inboundIds: z.array(z.number()),
|
||||
hosts: z.array(z.string()),
|
||||
sortOrder: z.number().optional(),
|
||||
remark: z.string().optional(),
|
||||
serverDescription: z.string().optional(),
|
||||
isDisabled: z.boolean().optional(),
|
||||
isHidden: z.boolean().optional(),
|
||||
tags: z.array(z.string()).nullish(),
|
||||
port: z.number().optional(),
|
||||
security: z.string().optional(),
|
||||
sni: z.string().optional(),
|
||||
hostHeader: z.string().optional(),
|
||||
path: z.string().optional(),
|
||||
alpn: z.array(z.string()).nullish(),
|
||||
fingerprint: z.string().optional(),
|
||||
overrideSniFromAddress: z.boolean().optional(),
|
||||
keepSniBlank: z.boolean().optional(),
|
||||
pinnedPeerCertSha256: z.array(z.string()).nullish(),
|
||||
verifyPeerCertByName: z.preprocess(
|
||||
(v) => (typeof v === 'boolean' ? '' : v),
|
||||
z.string().optional(),
|
||||
),
|
||||
allowInsecure: z.boolean().optional(),
|
||||
echConfigList: z.string().optional(),
|
||||
muxParams: z.unknown().optional(),
|
||||
sockoptParams: z.unknown().optional(),
|
||||
finalMask: z.string().optional(),
|
||||
vlessRoute: z.string().optional(),
|
||||
excludeFromSubTypes: z.array(z.string()).nullish(),
|
||||
nodeGuids: z.array(z.string()).nullish(),
|
||||
mihomoIpVersion: z.string().optional(),
|
||||
mihomoX25519: z.boolean().optional(),
|
||||
shuffleHost: z.boolean().optional(),
|
||||
}).loose();
|
||||
export const HostRecordSchema = z
|
||||
.object({
|
||||
groupId: z.string(),
|
||||
inboundIds: z.array(z.number()),
|
||||
hosts: z.array(z.string()),
|
||||
sortOrder: z.number().optional(),
|
||||
remark: z.string().optional(),
|
||||
serverDescription: z.string().optional(),
|
||||
isDisabled: z.boolean().optional(),
|
||||
isHidden: z.boolean().optional(),
|
||||
tags: z.array(z.string()).nullish(),
|
||||
port: z.number().optional(),
|
||||
security: z.string().optional(),
|
||||
sni: z.string().optional(),
|
||||
hostHeader: z.string().optional(),
|
||||
path: z.string().optional(),
|
||||
alpn: z.array(z.string()).nullish(),
|
||||
fingerprint: z.string().optional(),
|
||||
overrideSniFromAddress: z.boolean().optional(),
|
||||
keepSniBlank: z.boolean().optional(),
|
||||
pinnedPeerCertSha256: z.array(z.string()).nullish(),
|
||||
verifyPeerCertByName: z.preprocess(
|
||||
(v) => (typeof v === 'boolean' ? '' : v),
|
||||
z.string().optional(),
|
||||
),
|
||||
allowInsecure: z.boolean().optional(),
|
||||
echConfigList: z.string().optional(),
|
||||
muxParams: z.unknown().optional(),
|
||||
sockoptParams: z.unknown().optional(),
|
||||
finalMask: z.string().optional(),
|
||||
vlessRoute: z.string().optional(),
|
||||
excludeFromSubTypes: z.array(z.string()).nullish(),
|
||||
nodeGuids: z.array(z.string()).nullish(),
|
||||
mihomoIpVersion: z.string().optional(),
|
||||
mihomoX25519: z.boolean().optional(),
|
||||
shuffleHost: z.boolean().optional(),
|
||||
})
|
||||
.loose();
|
||||
export type HostRecord = z.infer<typeof HostRecordSchema>;
|
||||
|
||||
export const HostListSchema = z.array(HostRecordSchema);
|
||||
|
||||
@@ -18,9 +18,8 @@ import { NetworkSettingsSchema, StreamExtrasSchema } from '@/schemas/protocols/s
|
||||
// (~9e15) lose precision; the panel works around this for the traffic
|
||||
// counters by stringifying them at the API edge. Not modeled here.
|
||||
|
||||
export const StreamSettingsSchema = NetworkSettingsSchema
|
||||
.and(SecuritySettingsSchema)
|
||||
.and(StreamExtrasSchema);
|
||||
export const StreamSettingsSchema =
|
||||
NetworkSettingsSchema.and(SecuritySettingsSchema).and(StreamExtrasSchema);
|
||||
export type StreamSettings = z.infer<typeof StreamSettingsSchema>;
|
||||
|
||||
export const InboundCoreSchema = z.object({
|
||||
|
||||
+134
-94
@@ -1,7 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const nullableStringArray = z.array(z.string()).nullable().transform((v) => v ?? []);
|
||||
const nullableNumberArray = z.array(z.number()).nullable().transform((v) => v ?? []);
|
||||
const nullableStringArray = z
|
||||
.array(z.string())
|
||||
.nullable()
|
||||
.transform((v) => v ?? []);
|
||||
const nullableNumberArray = z
|
||||
.array(z.number())
|
||||
.nullable()
|
||||
.transform((v) => v ?? []);
|
||||
|
||||
export const ClientTrafficSchema = z.object({
|
||||
up: z.number().optional(),
|
||||
@@ -15,64 +21,68 @@ export const ClientTrafficSchema = z.object({
|
||||
resetCount: z.number().optional(),
|
||||
});
|
||||
|
||||
export const ClientRecordSchema = z.object({
|
||||
id: z.number().optional(),
|
||||
email: z.string(),
|
||||
subId: z.string().optional(),
|
||||
uuid: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
auth: z.string().optional(),
|
||||
flow: z.string().optional(),
|
||||
security: z.string().optional(),
|
||||
totalGB: z.number().optional(),
|
||||
expiryTime: z.number().optional(),
|
||||
limitIp: z.number().optional(),
|
||||
limitHwid: z.number().optional(),
|
||||
tgId: z.union([z.number(), z.string()]).optional(),
|
||||
group: z.string().optional(),
|
||||
comment: z.string().optional(),
|
||||
enable: z.boolean().optional(),
|
||||
reset: z.number().optional(),
|
||||
resetDay: z.number().optional(),
|
||||
resetMax: z.number().optional(),
|
||||
trafficReset: z.string().optional(),
|
||||
trafficResetDay: z.number().optional(),
|
||||
inboundIds: nullableNumberArray.optional(),
|
||||
traffic: ClientTrafficSchema.nullable().optional(),
|
||||
reverse: z.object({ tag: z.string().optional() }).loose().nullable().optional(),
|
||||
privateKey: z.string().optional(),
|
||||
publicKey: z.string().optional(),
|
||||
allowedIPs: z.string().optional(),
|
||||
preSharedKey: z.string().optional(),
|
||||
keepAlive: z.number().optional(),
|
||||
secret: z.string().optional(),
|
||||
adTag: z.string().optional(),
|
||||
createdAt: z.number().optional(),
|
||||
updatedAt: z.number().optional(),
|
||||
}).loose();
|
||||
export const ClientRecordSchema = z
|
||||
.object({
|
||||
id: z.number().optional(),
|
||||
email: z.string(),
|
||||
subId: z.string().optional(),
|
||||
uuid: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
auth: z.string().optional(),
|
||||
flow: z.string().optional(),
|
||||
security: z.string().optional(),
|
||||
totalGB: z.number().optional(),
|
||||
expiryTime: z.number().optional(),
|
||||
limitIp: z.number().optional(),
|
||||
limitHwid: z.number().optional(),
|
||||
tgId: z.union([z.number(), z.string()]).optional(),
|
||||
group: z.string().optional(),
|
||||
comment: z.string().optional(),
|
||||
enable: z.boolean().optional(),
|
||||
reset: z.number().optional(),
|
||||
resetDay: z.number().optional(),
|
||||
resetMax: z.number().optional(),
|
||||
trafficReset: z.string().optional(),
|
||||
trafficResetDay: z.number().optional(),
|
||||
inboundIds: nullableNumberArray.optional(),
|
||||
traffic: ClientTrafficSchema.nullable().optional(),
|
||||
reverse: z.object({ tag: z.string().optional() }).loose().nullable().optional(),
|
||||
privateKey: z.string().optional(),
|
||||
publicKey: z.string().optional(),
|
||||
allowedIPs: z.string().optional(),
|
||||
preSharedKey: z.string().optional(),
|
||||
keepAlive: z.number().optional(),
|
||||
secret: z.string().optional(),
|
||||
adTag: z.string().optional(),
|
||||
createdAt: z.number().optional(),
|
||||
updatedAt: z.number().optional(),
|
||||
})
|
||||
.loose();
|
||||
|
||||
export const InboundOptionSchema = z.object({
|
||||
id: z.number(),
|
||||
remark: z.string().optional(),
|
||||
tag: z.string().optional(),
|
||||
protocol: z.string().optional(),
|
||||
port: z.number().optional(),
|
||||
tlsFlowCapable: z.boolean().optional(),
|
||||
ssMethod: z.string().optional(),
|
||||
wgPublicKey: z.string().optional(),
|
||||
wgMtu: z.number().optional(),
|
||||
wgDns: z.string().optional(),
|
||||
mtprotoDomain: z.string().optional(),
|
||||
// Hosting node id; absent/null for this panel's own inbounds (#4997).
|
||||
nodeId: z.number().nullable().optional(),
|
||||
// Share-host resolution inputs, mirroring the backend resolveInboundAddress so
|
||||
// the clients page picks the same WireGuard endpoint host as the subscription:
|
||||
// the hosting node address, the inbound listen, and its share-address strategy.
|
||||
nodeAddress: z.string().optional(),
|
||||
listen: z.string().optional(),
|
||||
shareAddr: z.string().optional(),
|
||||
shareAddrStrategy: z.string().optional(),
|
||||
}).loose();
|
||||
export const InboundOptionSchema = z
|
||||
.object({
|
||||
id: z.number(),
|
||||
remark: z.string().optional(),
|
||||
tag: z.string().optional(),
|
||||
protocol: z.string().optional(),
|
||||
port: z.number().optional(),
|
||||
tlsFlowCapable: z.boolean().optional(),
|
||||
ssMethod: z.string().optional(),
|
||||
wgPublicKey: z.string().optional(),
|
||||
wgMtu: z.number().optional(),
|
||||
wgDns: z.string().optional(),
|
||||
mtprotoDomain: z.string().optional(),
|
||||
// Hosting node id; absent/null for this panel's own inbounds (#4997).
|
||||
nodeId: z.number().nullable().optional(),
|
||||
// Share-host resolution inputs, mirroring the backend resolveInboundAddress so
|
||||
// the clients page picks the same WireGuard endpoint host as the subscription:
|
||||
// the hosting node address, the inbound listen, and its share-address strategy.
|
||||
nodeAddress: z.string().optional(),
|
||||
listen: z.string().optional(),
|
||||
shareAddr: z.string().optional(),
|
||||
shareAddrStrategy: z.string().optional(),
|
||||
})
|
||||
.loose();
|
||||
|
||||
export const InboundOptionsSchema = z.array(InboundOptionSchema);
|
||||
|
||||
@@ -91,7 +101,10 @@ export const ClientsSummarySchema = z.object({
|
||||
deactive: nullableStringArray,
|
||||
});
|
||||
|
||||
const nullableClientArray = z.array(ClientRecordSchema).nullable().transform((v) => v ?? []);
|
||||
const nullableClientArray = z
|
||||
.array(ClientRecordSchema)
|
||||
.nullable()
|
||||
.transform((v) => v ?? []);
|
||||
|
||||
export const ClientPageResponseSchema = z.object({
|
||||
items: nullableClientArray,
|
||||
@@ -105,19 +118,24 @@ export const ClientPageResponseSchema = z.object({
|
||||
|
||||
// A per-client external link surfaced in the client's subscription:
|
||||
// kind=link is a single share link, kind=subscription is a remote sub URL.
|
||||
export const ExternalLinkSchema = z.object({
|
||||
id: z.number().int().optional().default(0),
|
||||
kind: z.enum(['link', 'subscription']).default('link'),
|
||||
value: z.string(),
|
||||
remark: z.string().optional().default(''),
|
||||
enable: z.preprocess((v) => (v == null ? true : v), z.boolean()).default(true),
|
||||
expiryTime: z.number().int().optional().default(0),
|
||||
namePrefix: z.string().optional().default(''),
|
||||
lastFetchAt: z.number().int().optional().default(0),
|
||||
lastFetchError: z.string().optional().default(''),
|
||||
}).loose();
|
||||
export const ExternalLinkSchema = z
|
||||
.object({
|
||||
id: z.number().int().optional().default(0),
|
||||
kind: z.enum(['link', 'subscription']).default('link'),
|
||||
value: z.string(),
|
||||
remark: z.string().optional().default(''),
|
||||
enable: z.preprocess((v) => (v == null ? true : v), z.boolean()).default(true),
|
||||
expiryTime: z.number().int().optional().default(0),
|
||||
namePrefix: z.string().optional().default(''),
|
||||
lastFetchAt: z.number().int().optional().default(0),
|
||||
lastFetchError: z.string().optional().default(''),
|
||||
})
|
||||
.loose();
|
||||
|
||||
export const ExternalLinkListSchema = z.array(ExternalLinkSchema).nullable().transform((v) => v ?? []);
|
||||
export const ExternalLinkListSchema = z
|
||||
.array(ExternalLinkSchema)
|
||||
.nullable()
|
||||
.transform((v) => v ?? []);
|
||||
|
||||
export const ClientHydrateSchema = z.object({
|
||||
client: ClientRecordSchema,
|
||||
@@ -127,30 +145,22 @@ export const ClientHydrateSchema = z.object({
|
||||
|
||||
export const BulkAdjustResultSchema = z.object({
|
||||
adjusted: z.number(),
|
||||
skipped: z
|
||||
.array(z.object({ email: z.string(), reason: z.string() }))
|
||||
.optional(),
|
||||
skipped: z.array(z.object({ email: z.string(), reason: z.string() })).optional(),
|
||||
});
|
||||
|
||||
export const BulkDeleteResultSchema = z.object({
|
||||
deleted: z.number(),
|
||||
skipped: z
|
||||
.array(z.object({ email: z.string(), reason: z.string() }))
|
||||
.optional(),
|
||||
skipped: z.array(z.object({ email: z.string(), reason: z.string() })).optional(),
|
||||
});
|
||||
|
||||
export const BulkSetEnableResultSchema = z.object({
|
||||
changed: z.number(),
|
||||
skipped: z
|
||||
.array(z.object({ email: z.string(), reason: z.string() }))
|
||||
.optional(),
|
||||
skipped: z.array(z.object({ email: z.string(), reason: z.string() })).optional(),
|
||||
});
|
||||
|
||||
export const BulkCreateResultSchema = z.object({
|
||||
created: z.number(),
|
||||
skipped: z
|
||||
.array(z.object({ email: z.string(), reason: z.string() }))
|
||||
.optional(),
|
||||
skipped: z.array(z.object({ email: z.string(), reason: z.string() })).optional(),
|
||||
});
|
||||
|
||||
export const DelDepletedResultSchema = z.object({
|
||||
@@ -158,15 +168,33 @@ export const DelDepletedResultSchema = z.object({
|
||||
});
|
||||
|
||||
export const BulkAttachResultSchema = z.object({
|
||||
attached: z.array(z.string()).nullable().transform((v) => v ?? []),
|
||||
skipped: z.array(z.string()).nullable().transform((v) => v ?? []),
|
||||
errors: z.array(z.string()).nullable().transform((v) => v ?? []),
|
||||
attached: z
|
||||
.array(z.string())
|
||||
.nullable()
|
||||
.transform((v) => v ?? []),
|
||||
skipped: z
|
||||
.array(z.string())
|
||||
.nullable()
|
||||
.transform((v) => v ?? []),
|
||||
errors: z
|
||||
.array(z.string())
|
||||
.nullable()
|
||||
.transform((v) => v ?? []),
|
||||
});
|
||||
|
||||
export const BulkDetachResultSchema = z.object({
|
||||
detached: z.array(z.string()).nullable().transform((v) => v ?? []),
|
||||
skipped: z.array(z.string()).nullable().transform((v) => v ?? []),
|
||||
errors: z.array(z.string()).nullable().transform((v) => v ?? []),
|
||||
detached: z
|
||||
.array(z.string())
|
||||
.nullable()
|
||||
.transform((v) => v ?? []),
|
||||
skipped: z
|
||||
.array(z.string())
|
||||
.nullable()
|
||||
.transform((v) => v ?? []),
|
||||
errors: z
|
||||
.array(z.string())
|
||||
.nullable()
|
||||
.transform((v) => v ?? []),
|
||||
});
|
||||
|
||||
export const OnlinesSchema = nullableStringArray;
|
||||
@@ -184,12 +212,24 @@ export const ActiveInboundsByNodeSchema = z
|
||||
export const GroupSummarySchema = z.object({
|
||||
name: z.string(),
|
||||
clientCount: z.number(),
|
||||
trafficUsed: z.number().nullable().transform((v) => v ?? 0),
|
||||
up: z.number().nullable().transform((v) => v ?? 0),
|
||||
down: z.number().nullable().transform((v) => v ?? 0),
|
||||
trafficUsed: z
|
||||
.number()
|
||||
.nullable()
|
||||
.transform((v) => v ?? 0),
|
||||
up: z
|
||||
.number()
|
||||
.nullable()
|
||||
.transform((v) => v ?? 0),
|
||||
down: z
|
||||
.number()
|
||||
.nullable()
|
||||
.transform((v) => v ?? 0),
|
||||
});
|
||||
|
||||
export const GroupSummaryListSchema = z.array(GroupSummarySchema).nullable().transform((v) => v ?? []);
|
||||
export const GroupSummaryListSchema = z
|
||||
.array(GroupSummarySchema)
|
||||
.nullable()
|
||||
.transform((v) => v ?? []);
|
||||
|
||||
export function hasForbiddenClientChars(value: string): boolean {
|
||||
if (value.includes('/') || value.includes('\\') || value.includes(' ')) return true;
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const DefaultsPayloadSchema = z.object({
|
||||
expireDiff: z.number().optional(),
|
||||
trafficDiff: z.number().optional(),
|
||||
tgBotEnable: z.boolean().optional(),
|
||||
subEnable: z.boolean().optional(),
|
||||
subTitle: z.string().optional(),
|
||||
subURI: z.string().optional(),
|
||||
subJsonURI: z.string().optional(),
|
||||
subJsonEnable: z.boolean().optional(),
|
||||
subClashURI: z.string().optional(),
|
||||
subClashEnable: z.boolean().optional(),
|
||||
pageSize: z.number().optional(),
|
||||
datepicker: z.enum(['gregorian', 'jalalian']).optional(),
|
||||
ipLimitEnable: z.boolean().optional(),
|
||||
accessLogEnable: z.boolean().optional(),
|
||||
webDomain: z.string().optional(),
|
||||
subDomain: z.string().optional(),
|
||||
}).loose();
|
||||
export const DefaultsPayloadSchema = z
|
||||
.object({
|
||||
expireDiff: z.number().optional(),
|
||||
trafficDiff: z.number().optional(),
|
||||
tgBotEnable: z.boolean().optional(),
|
||||
subEnable: z.boolean().optional(),
|
||||
subTitle: z.string().optional(),
|
||||
subURI: z.string().optional(),
|
||||
subJsonURI: z.string().optional(),
|
||||
subJsonEnable: z.boolean().optional(),
|
||||
subClashURI: z.string().optional(),
|
||||
subClashEnable: z.boolean().optional(),
|
||||
pageSize: z.number().optional(),
|
||||
datepicker: z.enum(['gregorian', 'jalalian']).optional(),
|
||||
ipLimitEnable: z.boolean().optional(),
|
||||
accessLogEnable: z.boolean().optional(),
|
||||
webDomain: z.string().optional(),
|
||||
subDomain: z.string().optional(),
|
||||
})
|
||||
.loose();
|
||||
|
||||
export type DefaultsPayload = z.infer<typeof DefaultsPayloadSchema>;
|
||||
|
||||
+10
-16
@@ -2,12 +2,7 @@ import { z } from 'zod';
|
||||
|
||||
import { PortSchema } from '@/schemas/primitives';
|
||||
|
||||
export const DnsQueryStrategySchema = z.enum([
|
||||
'UseIP',
|
||||
'UseIPv4',
|
||||
'UseIPv6',
|
||||
'UseSystem',
|
||||
]);
|
||||
export const DnsQueryStrategySchema = z.enum(['UseIP', 'UseIPv4', 'UseIPv6', 'UseSystem']);
|
||||
export type DnsQueryStrategy = z.infer<typeof DnsQueryStrategySchema>;
|
||||
|
||||
const DnsHostValueSchema = z.union([z.string(), z.array(z.string())]);
|
||||
@@ -35,22 +30,21 @@ export const DnsServerObjectInnerSchema = z.object({
|
||||
serveExpiredTTL: z.number().int().min(0).optional(),
|
||||
});
|
||||
|
||||
export const DnsServerObjectSchema = z.preprocess(
|
||||
(val) => {
|
||||
export const DnsServerObjectSchema = z
|
||||
.preprocess((val) => {
|
||||
if (typeof val !== 'object' || val === null || Array.isArray(val)) return val;
|
||||
const v = val as Record<string, unknown>;
|
||||
if (v.expectIPs && !v.expectedIPs) {
|
||||
return { ...v, expectedIPs: v.expectIPs };
|
||||
}
|
||||
return val;
|
||||
},
|
||||
DnsServerObjectInnerSchema,
|
||||
).transform((v) => {
|
||||
if (v.port === undefined && !isEncryptedDnsAddress(v.address)) {
|
||||
return { ...v, port: 53 };
|
||||
}
|
||||
return v;
|
||||
});
|
||||
}, DnsServerObjectInnerSchema)
|
||||
.transform((v) => {
|
||||
if (v.port === undefined && !isEncryptedDnsAddress(v.address)) {
|
||||
return { ...v, port: 53 };
|
||||
}
|
||||
return v;
|
||||
});
|
||||
export type DnsServerObject = z.infer<typeof DnsServerObjectSchema>;
|
||||
|
||||
export const DnsServerEntrySchema = z.union([z.string(), DnsServerObjectSchema]);
|
||||
|
||||
@@ -5,9 +5,8 @@ import { InboundSettingsSchema } from '@/schemas/protocols/inbound';
|
||||
import { SecuritySettingsSchema } from '@/schemas/protocols/security';
|
||||
import { NetworkSettingsSchema, StreamExtrasSchema } from '@/schemas/protocols/stream';
|
||||
|
||||
export const InboundStreamFormSchema = NetworkSettingsSchema
|
||||
.and(SecuritySettingsSchema)
|
||||
.and(StreamExtrasSchema);
|
||||
export const InboundStreamFormSchema =
|
||||
NetworkSettingsSchema.and(SecuritySettingsSchema).and(StreamExtrasSchema);
|
||||
export type InboundStreamFormValues = z.infer<typeof InboundStreamFormSchema>;
|
||||
|
||||
export const TrafficResetSchema = z.enum(['never', 'hourly', 'daily', 'weekly', 'monthly']);
|
||||
@@ -54,9 +53,8 @@ export type InboundFormBase = z.infer<typeof InboundFormBaseSchema>;
|
||||
|
||||
// Full form values = base + db fields + protocol-discriminated settings.
|
||||
// Consumers narrow on `.protocol` to access the matching settings branch.
|
||||
export const InboundFormSchema = InboundFormBaseSchema
|
||||
.and(InboundDbFieldsSchema)
|
||||
.and(InboundSettingsSchema);
|
||||
export const InboundFormSchema =
|
||||
InboundFormBaseSchema.and(InboundDbFieldsSchema).and(InboundSettingsSchema);
|
||||
export type InboundFormValues = z.infer<typeof InboundFormSchema>;
|
||||
|
||||
export const FallbackRowSchema = z.object({
|
||||
|
||||
@@ -214,9 +214,8 @@ export type MuxForm = z.infer<typeof MuxFormSchema>;
|
||||
// DU + extras (sockopt). Hysteria gets a side-channel branch in the modal
|
||||
// (legacy ob.stream.hysteria) — keeping the DU strict for now and routing
|
||||
// hysteria transport knobs through the Advanced JSON tab if needed.
|
||||
export const OutboundStreamFormSchema = NetworkSettingsSchema
|
||||
.and(SecuritySettingsSchema)
|
||||
.and(StreamExtrasSchema);
|
||||
export const OutboundStreamFormSchema =
|
||||
NetworkSettingsSchema.and(SecuritySettingsSchema).and(StreamExtrasSchema);
|
||||
export type OutboundStreamFormValues = z.infer<typeof OutboundStreamFormSchema>;
|
||||
|
||||
// Top-level form base: identity (tag, sendThrough, targetStrategy), then
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const SlimInboundSchema = z.object({
|
||||
id: z.number(),
|
||||
protocol: z.string(),
|
||||
}).loose();
|
||||
export const SlimInboundSchema = z
|
||||
.object({
|
||||
id: z.number(),
|
||||
protocol: z.string(),
|
||||
})
|
||||
.loose();
|
||||
|
||||
export const SlimInboundListSchema = z.array(SlimInboundSchema);
|
||||
|
||||
export const InboundDetailSchema = z.object({
|
||||
id: z.number(),
|
||||
protocol: z.string(),
|
||||
}).loose();
|
||||
export const InboundDetailSchema = z
|
||||
.object({
|
||||
id: z.number(),
|
||||
protocol: z.string(),
|
||||
})
|
||||
.loose();
|
||||
|
||||
export const LastOnlineMapSchema = z.record(z.string(), z.number());
|
||||
|
||||
|
||||
@@ -1,92 +1,101 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const NodeRecordSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string().optional(),
|
||||
remark: z.string().optional(),
|
||||
scheme: z.string().optional(),
|
||||
address: z.string().optional(),
|
||||
port: z.number().optional(),
|
||||
basePath: z.string().optional(),
|
||||
apiToken: z.string().optional(),
|
||||
hasApiToken: z.boolean().optional(),
|
||||
enable: z.boolean().optional(),
|
||||
status: z.string().optional(),
|
||||
latencyMs: z.number().optional(),
|
||||
cpuPct: z.number().optional(),
|
||||
memPct: z.number().optional(),
|
||||
xrayVersion: z.string().optional(),
|
||||
panelVersion: z.string().optional(),
|
||||
uptimeSecs: z.number().optional(),
|
||||
inboundCount: z.number().optional(),
|
||||
clientCount: z.number().optional(),
|
||||
onlineCount: z.number().optional(),
|
||||
activeCount: z.number().optional(),
|
||||
disabledCount: z.number().optional(),
|
||||
depletedCount: z.number().optional(),
|
||||
lastHeartbeat: z.number().optional(),
|
||||
lastError: z.string().optional(),
|
||||
// Xray state captured from the remote node's own /panel/api/server/status.
|
||||
// Lets the nodes list show a distinct indicator when the panel API is reachable
|
||||
// (status=online) but the Xray core on that node has failed.
|
||||
xrayState: z.string().optional(),
|
||||
xrayError: z.string().optional(),
|
||||
allowPrivateAddress: z.boolean().optional(),
|
||||
tlsVerifyMode: z.enum(['verify', 'skip', 'pin', 'mtls']).optional(),
|
||||
pinnedCertSha256: z.string().optional(),
|
||||
inboundSyncMode: z.enum(['all', 'selected']).optional(),
|
||||
// Backend serializes a nil []string as null for nodes saved before #5178.
|
||||
inboundTags: z.array(z.string()).nullish(),
|
||||
outboundTag: z.string().optional(),
|
||||
// Multi-hop node tree (#4983): a node's stable GUID, its parent's GUID, and
|
||||
// whether it's a read-only transitive sub-node surfaced from a downstream node.
|
||||
guid: z.string().optional(),
|
||||
parentGuid: z.string().optional(),
|
||||
transitive: z.boolean().optional(),
|
||||
}).loose();
|
||||
export const NodeRecordSchema = z
|
||||
.object({
|
||||
id: z.number(),
|
||||
name: z.string().optional(),
|
||||
remark: z.string().optional(),
|
||||
scheme: z.string().optional(),
|
||||
address: z.string().optional(),
|
||||
port: z.number().optional(),
|
||||
basePath: z.string().optional(),
|
||||
apiToken: z.string().optional(),
|
||||
hasApiToken: z.boolean().optional(),
|
||||
enable: z.boolean().optional(),
|
||||
status: z.string().optional(),
|
||||
latencyMs: z.number().optional(),
|
||||
cpuPct: z.number().optional(),
|
||||
memPct: z.number().optional(),
|
||||
xrayVersion: z.string().optional(),
|
||||
panelVersion: z.string().optional(),
|
||||
uptimeSecs: z.number().optional(),
|
||||
inboundCount: z.number().optional(),
|
||||
clientCount: z.number().optional(),
|
||||
onlineCount: z.number().optional(),
|
||||
activeCount: z.number().optional(),
|
||||
disabledCount: z.number().optional(),
|
||||
depletedCount: z.number().optional(),
|
||||
lastHeartbeat: z.number().optional(),
|
||||
lastError: z.string().optional(),
|
||||
// Xray state captured from the remote node's own /panel/api/server/status.
|
||||
// Lets the nodes list show a distinct indicator when the panel API is reachable
|
||||
// (status=online) but the Xray core on that node has failed.
|
||||
xrayState: z.string().optional(),
|
||||
xrayError: z.string().optional(),
|
||||
allowPrivateAddress: z.boolean().optional(),
|
||||
tlsVerifyMode: z.enum(['verify', 'skip', 'pin', 'mtls']).optional(),
|
||||
pinnedCertSha256: z.string().optional(),
|
||||
inboundSyncMode: z.enum(['all', 'selected']).optional(),
|
||||
// Backend serializes a nil []string as null for nodes saved before #5178.
|
||||
inboundTags: z.array(z.string()).nullish(),
|
||||
outboundTag: z.string().optional(),
|
||||
// Multi-hop node tree (#4983): a node's stable GUID, its parent's GUID, and
|
||||
// whether it's a read-only transitive sub-node surfaced from a downstream node.
|
||||
guid: z.string().optional(),
|
||||
parentGuid: z.string().optional(),
|
||||
transitive: z.boolean().optional(),
|
||||
})
|
||||
.loose();
|
||||
|
||||
export const NodeListSchema = z.array(NodeRecordSchema);
|
||||
|
||||
export const ProbeResultSchema = z.object({
|
||||
status: z.string(),
|
||||
latencyMs: z.number().optional(),
|
||||
xrayVersion: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
// Present on successful probe; used to surface "connected to panel, but xray failed on node".
|
||||
xrayState: z.string().optional(),
|
||||
xrayError: z.string().optional(),
|
||||
}).loose();
|
||||
export const ProbeResultSchema = z
|
||||
.object({
|
||||
status: z.string(),
|
||||
latencyMs: z.number().optional(),
|
||||
xrayVersion: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
// Present on successful probe; used to surface "connected to panel, but xray failed on node".
|
||||
xrayState: z.string().optional(),
|
||||
xrayError: z.string().optional(),
|
||||
})
|
||||
.loose();
|
||||
|
||||
export const NodeFormSchema = z.object({
|
||||
id: z.number().optional(),
|
||||
name: z.string().trim().min(1, 'pages.nodes.toasts.fillRequired'),
|
||||
remark: z.string().optional(),
|
||||
scheme: z.enum(['http', 'https']),
|
||||
address: z.string().trim().min(1, 'pages.nodes.toasts.fillRequired'),
|
||||
port: z.number().int().min(1).max(65535),
|
||||
basePath: z.string(),
|
||||
// mTLS nodes authenticate via the client certificate, so the token is optional
|
||||
// there; every other verify mode still requires one (matches remote.do()).
|
||||
apiToken: z.string().trim(),
|
||||
hasStoredToken: z.boolean().optional().default(false),
|
||||
enable: z.boolean(),
|
||||
allowPrivateAddress: z.boolean(),
|
||||
tlsVerifyMode: z.enum(['verify', 'skip', 'pin', 'mtls']),
|
||||
pinnedCertSha256: z.string().optional().default(''),
|
||||
inboundSyncMode: z.enum(['all', 'selected']).optional().default('all'),
|
||||
// Unmounted when sync mode is "all" (absent from antd onFinish values) and
|
||||
// serialized as null by the backend for a nil slice — tolerate both.
|
||||
inboundTags: z.array(z.string()).nullish().transform((tags) => tags ?? []),
|
||||
outboundTag: z.string().optional(),
|
||||
}).superRefine((val, ctx) => {
|
||||
if (val.tlsVerifyMode !== 'mtls' && val.apiToken.length === 0 && !val.hasStoredToken) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['apiToken'],
|
||||
message: 'pages.nodes.toasts.fillRequired',
|
||||
});
|
||||
}
|
||||
});
|
||||
export const NodeFormSchema = z
|
||||
.object({
|
||||
id: z.number().optional(),
|
||||
name: z.string().trim().min(1, 'pages.nodes.toasts.fillRequired'),
|
||||
remark: z.string().optional(),
|
||||
scheme: z.enum(['http', 'https']),
|
||||
address: z.string().trim().min(1, 'pages.nodes.toasts.fillRequired'),
|
||||
port: z.number().int().min(1).max(65535),
|
||||
basePath: z.string(),
|
||||
// mTLS nodes authenticate via the client certificate, so the token is optional
|
||||
// there; every other verify mode still requires one (matches remote.do()).
|
||||
apiToken: z.string().trim(),
|
||||
hasStoredToken: z.boolean().optional().default(false),
|
||||
enable: z.boolean(),
|
||||
allowPrivateAddress: z.boolean(),
|
||||
tlsVerifyMode: z.enum(['verify', 'skip', 'pin', 'mtls']),
|
||||
pinnedCertSha256: z.string().optional().default(''),
|
||||
inboundSyncMode: z.enum(['all', 'selected']).optional().default('all'),
|
||||
// Unmounted when sync mode is "all" (absent from antd onFinish values) and
|
||||
// serialized as null by the backend for a nil slice — tolerate both.
|
||||
inboundTags: z
|
||||
.array(z.string())
|
||||
.nullish()
|
||||
.transform((tags) => tags ?? []),
|
||||
outboundTag: z.string().optional(),
|
||||
})
|
||||
.superRefine((val, ctx) => {
|
||||
if (val.tlsVerifyMode !== 'mtls' && val.apiToken.length === 0 && !val.hasStoredToken) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['apiToken'],
|
||||
message: 'pages.nodes.toasts.fillRequired',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type NodeRecord = z.infer<typeof NodeRecordSchema>;
|
||||
export type ProbeResult = z.infer<typeof ProbeResultSchema>;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const FlowSchema = z.enum([
|
||||
'',
|
||||
'xtls-rprx-vision',
|
||||
'xtls-rprx-vision-udp443',
|
||||
]);
|
||||
export const FlowSchema = z.enum(['', 'xtls-rprx-vision', 'xtls-rprx-vision-udp443']);
|
||||
export type Flow = z.infer<typeof FlowSchema>;
|
||||
|
||||
// Const map matching the legacy models/inbound.ts `TLS_FLOW_CONTROL`
|
||||
|
||||
@@ -5,9 +5,7 @@ export type SniffingDest = z.infer<typeof SniffingDestSchema>;
|
||||
|
||||
export const SniffingSchema = z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
destOverride: z
|
||||
.array(SniffingDestSchema)
|
||||
.default(['http', 'tls', 'quic', 'fakedns']),
|
||||
destOverride: z.array(SniffingDestSchema).default(['http', 'tls', 'quic', 'fakedns']),
|
||||
metadataOnly: z.boolean().default(false),
|
||||
routeOnly: z.boolean().default(false),
|
||||
ipsExcluded: z.array(z.string()).default([]),
|
||||
|
||||
@@ -11,7 +11,10 @@ export const HysteriaClientSchema = z.object({
|
||||
totalGB: z.number().int().min(0).default(0),
|
||||
expiryTime: z.number().int().default(0),
|
||||
enable: z.boolean().default(true),
|
||||
tgId: z.union([z.number(), z.string()]).transform((v) => Number(v) || 0).default(0),
|
||||
tgId: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v) || 0)
|
||||
.default(0),
|
||||
subId: z.string().default(''),
|
||||
comment: z.string().default(''),
|
||||
reset: z.number().int().min(0).default(0),
|
||||
|
||||
@@ -30,16 +30,16 @@ export * from './wireguard';
|
||||
// Consumers narrow on `.protocol` and TypeScript narrows `.settings` to the
|
||||
// matching leaf type.
|
||||
export const InboundSettingsSchema = z.discriminatedUnion('protocol', [
|
||||
z.object({ protocol: z.literal('vmess'), settings: VmessInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('vless'), settings: VlessInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('trojan'), settings: TrojanInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('vmess'), settings: VmessInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('vless'), settings: VlessInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('trojan'), settings: TrojanInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('shadowsocks'), settings: ShadowsocksInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('wireguard'), settings: WireguardInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('hysteria'), settings: HysteriaInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('http'), settings: HttpInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('mixed'), settings: MixedInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('tunnel'), settings: TunnelInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('tun'), settings: TunInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('mtproto'), settings: MtprotoInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('wireguard'), settings: WireguardInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('hysteria'), settings: HysteriaInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('http'), settings: HttpInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('mixed'), settings: MixedInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('tunnel'), settings: TunnelInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('tun'), settings: TunInboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('mtproto'), settings: MtprotoInboundSettingsSchema }),
|
||||
]);
|
||||
export type InboundSettings = z.infer<typeof InboundSettingsSchema>;
|
||||
|
||||
@@ -27,7 +27,10 @@ export const MtprotoClientSchema = z.object({
|
||||
totalGB: z.number().int().min(0).default(0),
|
||||
expiryTime: z.number().int().default(0),
|
||||
enable: z.boolean().default(true),
|
||||
tgId: z.union([z.number(), z.string()]).transform((v) => Number(v) || 0).default(0),
|
||||
tgId: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v) || 0)
|
||||
.default(0),
|
||||
subId: z.string().default(''),
|
||||
comment: z.string().default(''),
|
||||
reset: z.number().int().min(0).default(0),
|
||||
|
||||
@@ -17,7 +17,10 @@ export const ShadowsocksClientSchema = z.object({
|
||||
totalGB: z.number().int().min(0).default(0),
|
||||
expiryTime: z.number().int().default(0),
|
||||
enable: z.boolean().default(true),
|
||||
tgId: z.union([z.number(), z.string()]).transform((v) => Number(v) || 0).default(0),
|
||||
tgId: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v) || 0)
|
||||
.default(0),
|
||||
subId: z.string().default(''),
|
||||
comment: z.string().default(''),
|
||||
reset: z.number().int().min(0).default(0),
|
||||
|
||||
@@ -16,7 +16,10 @@ export const TrojanClientSchema = z.object({
|
||||
totalGB: z.number().int().min(0).default(0),
|
||||
expiryTime: z.number().int().default(0),
|
||||
enable: z.boolean().default(true),
|
||||
tgId: z.union([z.number(), z.string()]).transform((v) => Number(v) || 0).default(0),
|
||||
tgId: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v) || 0)
|
||||
.default(0),
|
||||
subId: z.string().default(''),
|
||||
comment: z.string().default(''),
|
||||
reset: z.number().int().min(0).default(0),
|
||||
|
||||
@@ -16,7 +16,9 @@ export const TunnelInboundSettingsSchema = z.object({
|
||||
// validation with "Invalid input" (issue #5516). The trailing .optional()
|
||||
// keeps the key optional in the inferred type (a bare .transform() would
|
||||
// make it required).
|
||||
rewritePort: PortSchema.nullable().transform((v) => v ?? undefined).optional(),
|
||||
rewritePort: PortSchema.nullable()
|
||||
.transform((v) => v ?? undefined)
|
||||
.optional(),
|
||||
portMap: z.record(z.string(), z.string()).default({}),
|
||||
allowedNetwork: TunnelNetworkSchema.default('tcp,udp'),
|
||||
followRedirect: z.boolean().default(false),
|
||||
|
||||
@@ -19,7 +19,10 @@ export const VlessClientSchema = z.object({
|
||||
totalGB: z.number().int().min(0).default(0),
|
||||
expiryTime: z.number().int().default(0),
|
||||
enable: z.boolean().default(true),
|
||||
tgId: z.union([z.number(), z.string()]).transform((v) => Number(v) || 0).default(0),
|
||||
tgId: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v) || 0)
|
||||
.default(0),
|
||||
subId: z.string().default(''),
|
||||
comment: z.string().default(''),
|
||||
reset: z.number().int().min(0).default(0),
|
||||
|
||||
@@ -11,7 +11,10 @@ export const VmessClientSchema = z.object({
|
||||
totalGB: z.number().int().min(0).default(0),
|
||||
expiryTime: z.number().int().default(0),
|
||||
enable: z.boolean().default(true),
|
||||
tgId: z.union([z.number(), z.string()]).transform((v) => Number(v) || 0).default(0),
|
||||
tgId: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v) || 0)
|
||||
.default(0),
|
||||
subId: z.string().default(''),
|
||||
comment: z.string().default(''),
|
||||
reset: z.number().int().min(0).default(0),
|
||||
|
||||
@@ -49,7 +49,10 @@ export const WireguardClientSchema = z.object({
|
||||
totalGB: z.number().int().min(0).default(0),
|
||||
expiryTime: z.number().int().default(0),
|
||||
enable: z.boolean().default(true),
|
||||
tgId: z.union([z.number(), z.string()]).transform((v) => Number(v) || 0).default(0),
|
||||
tgId: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform((v) => Number(v) || 0)
|
||||
.default(0),
|
||||
subId: z.string().default(''),
|
||||
comment: z.string().default(''),
|
||||
reset: z.number().int().min(0).default(0),
|
||||
|
||||
@@ -27,17 +27,17 @@ export * from './vmess';
|
||||
export * from './wireguard';
|
||||
|
||||
export const OutboundSettingsSchema = z.discriminatedUnion('protocol', [
|
||||
z.object({ protocol: z.literal('vmess'), settings: VmessOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('vless'), settings: VlessOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('trojan'), settings: TrojanOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('vmess'), settings: VmessOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('vless'), settings: VlessOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('trojan'), settings: TrojanOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('shadowsocks'), settings: ShadowsocksOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('wireguard'), settings: WireguardOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('hysteria'), settings: HysteriaOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('http'), settings: HttpOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('socks'), settings: SocksOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('freedom'), settings: FreedomOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('blackhole'), settings: BlackholeOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('dns'), settings: DNSOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('loopback'), settings: LoopbackOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('wireguard'), settings: WireguardOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('hysteria'), settings: HysteriaOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('http'), settings: HttpOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('socks'), settings: SocksOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('freedom'), settings: FreedomOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('blackhole'), settings: BlackholeOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('dns'), settings: DNSOutboundSettingsSchema }),
|
||||
z.object({ protocol: z.literal('loopback'), settings: LoopbackOutboundSettingsSchema }),
|
||||
]);
|
||||
export type OutboundSettings = z.infer<typeof OutboundSettingsSchema>;
|
||||
|
||||
@@ -24,7 +24,7 @@ export type Security = z.infer<typeof SecuritySchema>;
|
||||
export const SecuritySettingsSchema = z.union([
|
||||
z.discriminatedUnion('security', [
|
||||
z.object({ security: z.literal('none') }),
|
||||
z.object({ security: z.literal('tls'), tlsSettings: TlsStreamSettingsSchema }),
|
||||
z.object({ security: z.literal('tls'), tlsSettings: TlsStreamSettingsSchema }),
|
||||
z.object({ security: z.literal('reality'), realitySettings: RealityStreamSettingsSchema }),
|
||||
]),
|
||||
z.object({ security: z.never().optional() }),
|
||||
|
||||
@@ -86,6 +86,11 @@ export const TlsStreamSettingsSchema = z.object({
|
||||
curvePreferences: z.array(z.string()).optional(),
|
||||
masterKeyLog: z.string().optional(),
|
||||
echSockopt: SockoptStreamSettingsSchema.optional(),
|
||||
settings: TlsClientSettingsSchema.default({ fingerprint: 'chrome', echConfigList: '', pinnedPeerCertSha256: [], verifyPeerCertByName: '' }),
|
||||
settings: TlsClientSettingsSchema.default({
|
||||
fingerprint: 'chrome',
|
||||
echConfigList: '',
|
||||
pinnedPeerCertSha256: [],
|
||||
verifyPeerCertByName: '',
|
||||
}),
|
||||
});
|
||||
export type TlsStreamSettings = z.infer<typeof TlsStreamSettingsSchema>;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const VmessSecurityEnum = z.enum([
|
||||
'aes-128-gcm',
|
||||
'chacha20-poly1305',
|
||||
'auto',
|
||||
]);
|
||||
const VmessSecurityEnum = z.enum(['aes-128-gcm', 'chacha20-poly1305', 'auto']);
|
||||
|
||||
// Legacy rows persisted `security: ""` (especially on VMess inbounds
|
||||
// created before the enum was nailed down), and rows predating xray-core
|
||||
|
||||
@@ -47,10 +47,7 @@ export type BbrProfile = z.infer<typeof BbrProfileSchema>;
|
||||
// to dodge port-based blocking. Both fields are dash-range strings on the
|
||||
// wire (e.g. '20000-50000', '5-10'). preprocess coerces legacy DB rows
|
||||
// where interval was stored as a number (UI bug — see B19 in commit history).
|
||||
const StringRangeSchema = z.preprocess(
|
||||
(v) => (typeof v === 'number' ? String(v) : v),
|
||||
z.string(),
|
||||
);
|
||||
const StringRangeSchema = z.preprocess((v) => (typeof v === 'number' ? String(v) : v), z.string());
|
||||
|
||||
export const QuicUdpHopSchema = z.object({
|
||||
ports: StringRangeSchema.default('20000-50000'),
|
||||
|
||||
@@ -23,7 +23,13 @@ export * from './ws';
|
||||
export * from './xhttp';
|
||||
|
||||
export const NetworkSchema = z.enum([
|
||||
'tcp', 'kcp', 'ws', 'grpc', 'httpupgrade', 'xhttp', 'hysteria',
|
||||
'tcp',
|
||||
'kcp',
|
||||
'ws',
|
||||
'grpc',
|
||||
'httpupgrade',
|
||||
'xhttp',
|
||||
'hysteria',
|
||||
]);
|
||||
export type Network = z.infer<typeof NetworkSchema>;
|
||||
|
||||
@@ -37,13 +43,16 @@ export type Network = z.infer<typeof NetworkSchema>;
|
||||
// network selector hides it for other protocols. xray-core enforces
|
||||
// the constraint server-side too.
|
||||
const TransportNetworkSettingsSchema = z.discriminatedUnion('network', [
|
||||
z.object({ network: z.literal('tcp'), tcpSettings: TcpStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('kcp'), kcpSettings: KcpStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('ws'), wsSettings: WsStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('grpc'), grpcSettings: GrpcStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('httpupgrade'), httpupgradeSettings: HttpUpgradeStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('xhttp'), xhttpSettings: XHttpStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('hysteria'), hysteriaSettings: HysteriaStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('tcp'), tcpSettings: TcpStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('kcp'), kcpSettings: KcpStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('ws'), wsSettings: WsStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('grpc'), grpcSettings: GrpcStreamSettingsSchema }),
|
||||
z.object({
|
||||
network: z.literal('httpupgrade'),
|
||||
httpupgradeSettings: HttpUpgradeStreamSettingsSchema,
|
||||
}),
|
||||
z.object({ network: z.literal('xhttp'), xhttpSettings: XHttpStreamSettingsSchema }),
|
||||
z.object({ network: z.literal('hysteria'), hysteriaSettings: HysteriaStreamSettingsSchema }),
|
||||
]);
|
||||
|
||||
// Wireguard (always a UDP listener) and Tunnel (dokodemo-door) expose no
|
||||
@@ -69,10 +78,7 @@ export const NetworkSettingsSchema = z.preprocess(
|
||||
}
|
||||
return val;
|
||||
},
|
||||
z.union([
|
||||
TransportNetworkSettingsSchema,
|
||||
z.object({ network: z.never().optional() }),
|
||||
]),
|
||||
z.union([TransportNetworkSettingsSchema, z.object({ network: z.never().optional() })]),
|
||||
);
|
||||
export type NetworkSettings = z.infer<typeof NetworkSettingsSchema>;
|
||||
|
||||
|
||||
@@ -45,8 +45,15 @@ export const XMUX_FRESH_DEFAULTS: XHttpXmux = {
|
||||
// charset (splithttp.PredefinedTable, xray-core #6258). A literal ASCII
|
||||
// charset string is also accepted.
|
||||
export const XHTTP_SESSION_ID_TABLES = [
|
||||
'ALPHABET', 'Alphabet', 'BASE36', 'Base62', 'HEX',
|
||||
'alphabet', 'base36', 'hex', 'number',
|
||||
'ALPHABET',
|
||||
'Alphabet',
|
||||
'BASE36',
|
||||
'Base62',
|
||||
'HEX',
|
||||
'alphabet',
|
||||
'base36',
|
||||
'hex',
|
||||
'number',
|
||||
] as const;
|
||||
|
||||
// xray-core #6258 renamed sessionPlacement/sessionKey to
|
||||
@@ -68,50 +75,53 @@ function migrateLegacyXhttp(v: unknown): unknown {
|
||||
return o;
|
||||
}
|
||||
|
||||
export const XHttpStreamSettingsSchema = z.preprocess(migrateLegacyXhttp, z.object({
|
||||
path: z.string().default('/'),
|
||||
host: z.string().default(''),
|
||||
mode: XHttpModeSchema.default('auto'),
|
||||
xPaddingBytes: z.string().default('100-1000'),
|
||||
xPaddingObfsMode: z.boolean().default(false),
|
||||
xPaddingKey: z.string().default(''),
|
||||
xPaddingHeader: z.string().default(''),
|
||||
xPaddingPlacement: z.string().default(''),
|
||||
xPaddingMethod: z.string().default(''),
|
||||
sessionIDPlacement: z.string().default(''),
|
||||
sessionIDKey: z.string().default(''),
|
||||
// sessionIDTable: a predefined name (XHTTP_SESSION_ID_TABLES) or a literal
|
||||
// ASCII charset. sessionIDLength: dash-range string (e.g. '8-16'); only
|
||||
// honored when a table is set. xray-core enforces the room-size minimum.
|
||||
sessionIDTable: z.string().default(''),
|
||||
sessionIDLength: z.string().default(''),
|
||||
seqPlacement: z.string().default(''),
|
||||
seqKey: z.string().default(''),
|
||||
uplinkDataPlacement: z.string().default(''),
|
||||
uplinkDataKey: z.string().default(''),
|
||||
// Empty default on purpose: xray-core already defaults to 1MB/30ms, and
|
||||
// baking the literal values into every config and share link gives DPI a
|
||||
// stable fingerprint (#5141 — TSPU keys on scMinPostsIntervalMs=30).
|
||||
scMaxEachPostBytes: z.string().default(''),
|
||||
noSSEHeader: z.boolean().default(false),
|
||||
scMaxBufferedPosts: z.number().int().min(0).default(30),
|
||||
scStreamUpServerSecs: z.string().default('20-80'),
|
||||
serverMaxHeaderBytes: z.number().int().min(0).default(0),
|
||||
uplinkHTTPMethod: z.string().default(''),
|
||||
headers: WsHeaderMapSchema.default({}),
|
||||
// Client-side fields stored on inbound for subscription propagation.
|
||||
// The server listener ignores them at runtime, but the panel embeds
|
||||
// them in share-link `extra` blobs so the same xhttp config can
|
||||
// round-trip on both sides.
|
||||
// - scMinPostsIntervalMs: preserved when non-default (stripped at '' or '30')
|
||||
// - uplinkChunkSize & noGRPCHeader: outbound-only; stripped from inbound wire
|
||||
scMinPostsIntervalMs: z.string().default(''),
|
||||
uplinkChunkSize: z.number().int().min(0).default(0),
|
||||
noGRPCHeader: z.boolean().default(false),
|
||||
xmux: XHttpXmuxSchema.optional(),
|
||||
// UI-only toggle controlling whether the XMUX sub-form is expanded.
|
||||
// Never present on the wire — outbound modal strips it via the
|
||||
// form-to-wire adapter.
|
||||
enableXmux: z.boolean().default(false),
|
||||
}));
|
||||
export const XHttpStreamSettingsSchema = z.preprocess(
|
||||
migrateLegacyXhttp,
|
||||
z.object({
|
||||
path: z.string().default('/'),
|
||||
host: z.string().default(''),
|
||||
mode: XHttpModeSchema.default('auto'),
|
||||
xPaddingBytes: z.string().default('100-1000'),
|
||||
xPaddingObfsMode: z.boolean().default(false),
|
||||
xPaddingKey: z.string().default(''),
|
||||
xPaddingHeader: z.string().default(''),
|
||||
xPaddingPlacement: z.string().default(''),
|
||||
xPaddingMethod: z.string().default(''),
|
||||
sessionIDPlacement: z.string().default(''),
|
||||
sessionIDKey: z.string().default(''),
|
||||
// sessionIDTable: a predefined name (XHTTP_SESSION_ID_TABLES) or a literal
|
||||
// ASCII charset. sessionIDLength: dash-range string (e.g. '8-16'); only
|
||||
// honored when a table is set. xray-core enforces the room-size minimum.
|
||||
sessionIDTable: z.string().default(''),
|
||||
sessionIDLength: z.string().default(''),
|
||||
seqPlacement: z.string().default(''),
|
||||
seqKey: z.string().default(''),
|
||||
uplinkDataPlacement: z.string().default(''),
|
||||
uplinkDataKey: z.string().default(''),
|
||||
// Empty default on purpose: xray-core already defaults to 1MB/30ms, and
|
||||
// baking the literal values into every config and share link gives DPI a
|
||||
// stable fingerprint (#5141 — TSPU keys on scMinPostsIntervalMs=30).
|
||||
scMaxEachPostBytes: z.string().default(''),
|
||||
noSSEHeader: z.boolean().default(false),
|
||||
scMaxBufferedPosts: z.number().int().min(0).default(30),
|
||||
scStreamUpServerSecs: z.string().default('20-80'),
|
||||
serverMaxHeaderBytes: z.number().int().min(0).default(0),
|
||||
uplinkHTTPMethod: z.string().default(''),
|
||||
headers: WsHeaderMapSchema.default({}),
|
||||
// Client-side fields stored on inbound for subscription propagation.
|
||||
// The server listener ignores them at runtime, but the panel embeds
|
||||
// them in share-link `extra` blobs so the same xhttp config can
|
||||
// round-trip on both sides.
|
||||
// - scMinPostsIntervalMs: preserved when non-default (stripped at '' or '30')
|
||||
// - uplinkChunkSize & noGRPCHeader: outbound-only; stripped from inbound wire
|
||||
scMinPostsIntervalMs: z.string().default(''),
|
||||
uplinkChunkSize: z.number().int().min(0).default(0),
|
||||
noGRPCHeader: z.boolean().default(false),
|
||||
xmux: XHttpXmuxSchema.optional(),
|
||||
// UI-only toggle controlling whether the XMUX sub-form is expanded.
|
||||
// Never present on the wire — outbound modal strips it via the
|
||||
// form-to-wire adapter.
|
||||
enableXmux: z.boolean().default(false),
|
||||
}),
|
||||
);
|
||||
export type XHttpStreamSettings = z.infer<typeof XHttpStreamSettingsSchema>;
|
||||
|
||||
@@ -3,10 +3,7 @@ import { z } from 'zod';
|
||||
export const RuleProtocolSchema = z.enum(['http', 'tls', 'quic', 'bittorrent']);
|
||||
export type RuleProtocol = z.infer<typeof RuleProtocolSchema>;
|
||||
|
||||
const PortValueSchema = z.union([
|
||||
z.number().int().min(0).max(65535),
|
||||
z.string(),
|
||||
]);
|
||||
const PortValueSchema = z.union([z.number().int().min(0).max(65535), z.string()]);
|
||||
|
||||
export const RuleWebhookSchema = z.object({
|
||||
url: z.string(),
|
||||
|
||||
+100
-98
@@ -4,104 +4,106 @@ const port = z.number().int().min(1).max(65535);
|
||||
const nonNegativeInt = z.number().int().min(0);
|
||||
const absolutePath = z.string().regex(/^\//, 'pages.settings.validation.pathLeadingSlash');
|
||||
|
||||
export const AllSettingSchema = z.object({
|
||||
webListen: z.string().optional(),
|
||||
webDomain: z.string().optional(),
|
||||
webPort: port.optional(),
|
||||
webCertFile: z.string().optional(),
|
||||
webKeyFile: z.string().optional(),
|
||||
webBasePath: absolutePath.optional(),
|
||||
sessionMaxAge: z.number().int().min(1).max(525600).optional(),
|
||||
trustedProxyCIDRs: z.string().optional(),
|
||||
ipLimitAllowlist: z.string().optional(),
|
||||
panelOutbound: z.string().optional(),
|
||||
pageSize: z.number().int().min(0).max(1000).optional(),
|
||||
expireDiff: nonNegativeInt.optional(),
|
||||
trafficDiff: nonNegativeInt.max(100).optional(),
|
||||
remarkTemplate: z.string().optional(),
|
||||
subShowIdentityOnAllLinks: z.boolean().optional(),
|
||||
datepicker: z.enum(['gregorian', 'jalalian']).optional(),
|
||||
tgBotEnable: z.boolean().optional(),
|
||||
tgBotToken: z.string().optional(),
|
||||
tgBotProxy: z.string().optional(),
|
||||
tgBotAPIServer: z.string().optional(),
|
||||
tgBotChatId: z.string().optional(),
|
||||
tgRunTime: z.string().optional(),
|
||||
tgBotBackup: z.boolean().optional(),
|
||||
tgCpu: z.number().int().min(0).max(100).optional(),
|
||||
outboundDownThreshold: z.number().int().min(1).max(100).optional(),
|
||||
tgLang: z.string().optional(),
|
||||
twoFactorEnable: z.boolean().optional(),
|
||||
twoFactorToken: z.string().optional(),
|
||||
xrayTemplateConfig: z.string().optional(),
|
||||
subEnable: z.boolean().optional(),
|
||||
subJsonEnable: z.boolean().optional(),
|
||||
subJsonAutoDetect: z.boolean().optional(),
|
||||
subJsonAlwaysArray: z.boolean().optional(),
|
||||
subJsonUserAgentRegex: z.string().max(2048).optional(),
|
||||
subClashAutoDetect: z.boolean().optional(),
|
||||
subClashUserAgentRegex: z.string().max(2048).optional(),
|
||||
subTitle: z.string().optional(),
|
||||
subSupportUrl: z.string().optional(),
|
||||
subProfileUrl: z.string().optional(),
|
||||
subAnnounce: z.string().optional(),
|
||||
subEnableRouting: z.boolean().optional(),
|
||||
subRoutingRules: z.string().optional(),
|
||||
subIncyEnableRouting: z.boolean().optional(),
|
||||
subIncyRoutingRules: z.string().optional(),
|
||||
subListen: z.string().optional(),
|
||||
subPort: port.optional(),
|
||||
subPath: absolutePath.optional(),
|
||||
subJsonPath: absolutePath.optional(),
|
||||
subClashEnable: z.boolean().optional(),
|
||||
subClashPath: absolutePath.optional(),
|
||||
subDomain: z.string().optional(),
|
||||
externalTrafficInformEnable: z.boolean().optional(),
|
||||
externalTrafficInformURI: z.string().optional(),
|
||||
restartXrayOnClientDisable: z.boolean().optional(),
|
||||
subCertFile: z.string().optional(),
|
||||
subKeyFile: z.string().optional(),
|
||||
subUpdates: z.number().int().min(0).max(525600).optional(),
|
||||
subEncrypt: z.boolean().optional(),
|
||||
subURI: z.string().optional(),
|
||||
subJsonURI: z.string().optional(),
|
||||
subClashURI: z.string().optional(),
|
||||
subClashEnableRouting: z.boolean().optional(),
|
||||
subClashRules: z.string().optional(),
|
||||
subJsonMux: z.string().optional(),
|
||||
subJsonRules: z.string().optional(),
|
||||
subJsonFinalMask: z.string().optional(),
|
||||
subHideSettings: z.boolean().optional(),
|
||||
timeLocation: z.string().optional(),
|
||||
ldapEnable: z.boolean().optional(),
|
||||
ldapHost: z.string().optional(),
|
||||
ldapPort: port.optional(),
|
||||
ldapUseTLS: z.boolean().optional(),
|
||||
ldapInsecureSkipVerify: z.boolean().optional(),
|
||||
ldapBindDN: z.string().optional(),
|
||||
ldapPassword: z.string().optional(),
|
||||
ldapBaseDN: z.string().optional(),
|
||||
ldapUserFilter: z.string().optional(),
|
||||
ldapUserAttr: z.string().optional(),
|
||||
ldapVlessField: z.string().optional(),
|
||||
ldapSyncCron: z.string().optional(),
|
||||
ldapFlagField: z.string().optional(),
|
||||
ldapTruthyValues: z.string().optional(),
|
||||
ldapInvertFlag: z.boolean().optional(),
|
||||
ldapInboundTags: z.string().optional(),
|
||||
ldapAutoCreate: z.boolean().optional(),
|
||||
ldapAutoDelete: z.boolean().optional(),
|
||||
ldapDefaultTotalGB: nonNegativeInt.optional(),
|
||||
ldapDefaultExpiryDays: nonNegativeInt.optional(),
|
||||
ldapDefaultLimitIP: nonNegativeInt.optional(),
|
||||
hasTgBotToken: z.boolean().optional(),
|
||||
hasTwoFactorToken: z.boolean().optional(),
|
||||
hasLdapPassword: z.boolean().optional(),
|
||||
hasApiToken: z.boolean().optional(),
|
||||
hasWarpSecret: z.boolean().optional(),
|
||||
hasNordSecret: z.boolean().optional(),
|
||||
hasSmtpPassword: z.boolean().optional(),
|
||||
}).loose();
|
||||
export const AllSettingSchema = z
|
||||
.object({
|
||||
webListen: z.string().optional(),
|
||||
webDomain: z.string().optional(),
|
||||
webPort: port.optional(),
|
||||
webCertFile: z.string().optional(),
|
||||
webKeyFile: z.string().optional(),
|
||||
webBasePath: absolutePath.optional(),
|
||||
sessionMaxAge: z.number().int().min(1).max(525600).optional(),
|
||||
trustedProxyCIDRs: z.string().optional(),
|
||||
ipLimitAllowlist: z.string().optional(),
|
||||
panelOutbound: z.string().optional(),
|
||||
pageSize: z.number().int().min(0).max(1000).optional(),
|
||||
expireDiff: nonNegativeInt.optional(),
|
||||
trafficDiff: nonNegativeInt.max(100).optional(),
|
||||
remarkTemplate: z.string().optional(),
|
||||
subShowIdentityOnAllLinks: z.boolean().optional(),
|
||||
datepicker: z.enum(['gregorian', 'jalalian']).optional(),
|
||||
tgBotEnable: z.boolean().optional(),
|
||||
tgBotToken: z.string().optional(),
|
||||
tgBotProxy: z.string().optional(),
|
||||
tgBotAPIServer: z.string().optional(),
|
||||
tgBotChatId: z.string().optional(),
|
||||
tgRunTime: z.string().optional(),
|
||||
tgBotBackup: z.boolean().optional(),
|
||||
tgCpu: z.number().int().min(0).max(100).optional(),
|
||||
outboundDownThreshold: z.number().int().min(1).max(100).optional(),
|
||||
tgLang: z.string().optional(),
|
||||
twoFactorEnable: z.boolean().optional(),
|
||||
twoFactorToken: z.string().optional(),
|
||||
xrayTemplateConfig: z.string().optional(),
|
||||
subEnable: z.boolean().optional(),
|
||||
subJsonEnable: z.boolean().optional(),
|
||||
subJsonAutoDetect: z.boolean().optional(),
|
||||
subJsonAlwaysArray: z.boolean().optional(),
|
||||
subJsonUserAgentRegex: z.string().max(2048).optional(),
|
||||
subClashAutoDetect: z.boolean().optional(),
|
||||
subClashUserAgentRegex: z.string().max(2048).optional(),
|
||||
subTitle: z.string().optional(),
|
||||
subSupportUrl: z.string().optional(),
|
||||
subProfileUrl: z.string().optional(),
|
||||
subAnnounce: z.string().optional(),
|
||||
subEnableRouting: z.boolean().optional(),
|
||||
subRoutingRules: z.string().optional(),
|
||||
subIncyEnableRouting: z.boolean().optional(),
|
||||
subIncyRoutingRules: z.string().optional(),
|
||||
subListen: z.string().optional(),
|
||||
subPort: port.optional(),
|
||||
subPath: absolutePath.optional(),
|
||||
subJsonPath: absolutePath.optional(),
|
||||
subClashEnable: z.boolean().optional(),
|
||||
subClashPath: absolutePath.optional(),
|
||||
subDomain: z.string().optional(),
|
||||
externalTrafficInformEnable: z.boolean().optional(),
|
||||
externalTrafficInformURI: z.string().optional(),
|
||||
restartXrayOnClientDisable: z.boolean().optional(),
|
||||
subCertFile: z.string().optional(),
|
||||
subKeyFile: z.string().optional(),
|
||||
subUpdates: z.number().int().min(0).max(525600).optional(),
|
||||
subEncrypt: z.boolean().optional(),
|
||||
subURI: z.string().optional(),
|
||||
subJsonURI: z.string().optional(),
|
||||
subClashURI: z.string().optional(),
|
||||
subClashEnableRouting: z.boolean().optional(),
|
||||
subClashRules: z.string().optional(),
|
||||
subJsonMux: z.string().optional(),
|
||||
subJsonRules: z.string().optional(),
|
||||
subJsonFinalMask: z.string().optional(),
|
||||
subHideSettings: z.boolean().optional(),
|
||||
timeLocation: z.string().optional(),
|
||||
ldapEnable: z.boolean().optional(),
|
||||
ldapHost: z.string().optional(),
|
||||
ldapPort: port.optional(),
|
||||
ldapUseTLS: z.boolean().optional(),
|
||||
ldapInsecureSkipVerify: z.boolean().optional(),
|
||||
ldapBindDN: z.string().optional(),
|
||||
ldapPassword: z.string().optional(),
|
||||
ldapBaseDN: z.string().optional(),
|
||||
ldapUserFilter: z.string().optional(),
|
||||
ldapUserAttr: z.string().optional(),
|
||||
ldapVlessField: z.string().optional(),
|
||||
ldapSyncCron: z.string().optional(),
|
||||
ldapFlagField: z.string().optional(),
|
||||
ldapTruthyValues: z.string().optional(),
|
||||
ldapInvertFlag: z.boolean().optional(),
|
||||
ldapInboundTags: z.string().optional(),
|
||||
ldapAutoCreate: z.boolean().optional(),
|
||||
ldapAutoDelete: z.boolean().optional(),
|
||||
ldapDefaultTotalGB: nonNegativeInt.optional(),
|
||||
ldapDefaultExpiryDays: nonNegativeInt.optional(),
|
||||
ldapDefaultLimitIP: nonNegativeInt.optional(),
|
||||
hasTgBotToken: z.boolean().optional(),
|
||||
hasTwoFactorToken: z.boolean().optional(),
|
||||
hasLdapPassword: z.boolean().optional(),
|
||||
hasApiToken: z.boolean().optional(),
|
||||
hasWarpSecret: z.boolean().optional(),
|
||||
hasNordSecret: z.boolean().optional(),
|
||||
hasSmtpPassword: z.boolean().optional(),
|
||||
})
|
||||
.loose();
|
||||
|
||||
export type AllSettingInput = z.infer<typeof AllSettingSchema>;
|
||||
|
||||
|
||||
@@ -26,12 +26,14 @@ export const AppStatsSchema = z.object({
|
||||
uptime: z.number(),
|
||||
});
|
||||
|
||||
export const XrayInfoSchema = z.object({
|
||||
state: z.string(),
|
||||
errorMsg: z.string(),
|
||||
version: z.string(),
|
||||
color: z.string(),
|
||||
}).partial();
|
||||
export const XrayInfoSchema = z
|
||||
.object({
|
||||
state: z.string(),
|
||||
errorMsg: z.string(),
|
||||
version: z.string(),
|
||||
color: z.string(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
export const StatusSchema = z.object({
|
||||
cpu: z.number().optional(),
|
||||
|
||||
@@ -7,45 +7,57 @@ import {
|
||||
RuleObjectSchema,
|
||||
} from './routing';
|
||||
|
||||
export const XraySettingsValueSchema = z.object({
|
||||
inbounds: z.array(z.unknown()).optional(),
|
||||
outbounds: z
|
||||
.array(
|
||||
z.object({
|
||||
tag: z.string().optional(),
|
||||
protocol: z.string().optional(),
|
||||
settings: z.unknown().optional(),
|
||||
streamSettings: z.unknown().optional(),
|
||||
}).loose(),
|
||||
)
|
||||
.optional(),
|
||||
routing: z.object({
|
||||
rules: z.array(RuleObjectSchema).optional(),
|
||||
balancers: z.array(BalancerObjectSchema).optional(),
|
||||
domainStrategy: z.string().optional(),
|
||||
}).loose().optional(),
|
||||
dns: DnsObjectSchema.optional(),
|
||||
log: z.record(z.string(), z.unknown()).optional(),
|
||||
policy: z.object({
|
||||
system: z.record(z.string(), z.boolean()).optional(),
|
||||
levels: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
|
||||
}).loose().optional(),
|
||||
observatory: z.unknown().optional(),
|
||||
burstObservatory: z.unknown().optional(),
|
||||
fakedns: z.unknown().optional(),
|
||||
}).loose();
|
||||
export const XraySettingsValueSchema = z
|
||||
.object({
|
||||
inbounds: z.array(z.unknown()).optional(),
|
||||
outbounds: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
tag: z.string().optional(),
|
||||
protocol: z.string().optional(),
|
||||
settings: z.unknown().optional(),
|
||||
streamSettings: z.unknown().optional(),
|
||||
})
|
||||
.loose(),
|
||||
)
|
||||
.optional(),
|
||||
routing: z
|
||||
.object({
|
||||
rules: z.array(RuleObjectSchema).optional(),
|
||||
balancers: z.array(BalancerObjectSchema).optional(),
|
||||
domainStrategy: z.string().optional(),
|
||||
})
|
||||
.loose()
|
||||
.optional(),
|
||||
dns: DnsObjectSchema.optional(),
|
||||
log: z.record(z.string(), z.unknown()).optional(),
|
||||
policy: z
|
||||
.object({
|
||||
system: z.record(z.string(), z.boolean()).optional(),
|
||||
levels: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
|
||||
})
|
||||
.loose()
|
||||
.optional(),
|
||||
observatory: z.unknown().optional(),
|
||||
burstObservatory: z.unknown().optional(),
|
||||
fakedns: z.unknown().optional(),
|
||||
})
|
||||
.loose();
|
||||
|
||||
export const XrayConfigPayloadSchema = z.object({
|
||||
xraySetting: XraySettingsValueSchema,
|
||||
inboundTags: z.array(z.string()).optional(),
|
||||
clientReverseTags: z.array(z.string()).optional(),
|
||||
outboundTestUrl: z.string().optional(),
|
||||
// Subscription outbounds are injected at runtime (not persisted in xraySetting).
|
||||
// They are provided here so the UI can display them and use their tags in
|
||||
// balancers / routing rules.
|
||||
subscriptionOutbounds: z.array(z.unknown()).optional(),
|
||||
subscriptionOutboundTags: z.array(z.string()).optional(),
|
||||
}).loose();
|
||||
export const XrayConfigPayloadSchema = z
|
||||
.object({
|
||||
xraySetting: XraySettingsValueSchema,
|
||||
inboundTags: z.array(z.string()).optional(),
|
||||
clientReverseTags: z.array(z.string()).optional(),
|
||||
outboundTestUrl: z.string().optional(),
|
||||
// Subscription outbounds are injected at runtime (not persisted in xraySetting).
|
||||
// They are provided here so the UI can display them and use their tags in
|
||||
// balancers / routing rules.
|
||||
subscriptionOutbounds: z.array(z.unknown()).optional(),
|
||||
subscriptionOutboundTags: z.array(z.string()).optional(),
|
||||
})
|
||||
.loose();
|
||||
|
||||
export const OutboundTrafficRowSchema = z.object({
|
||||
tag: z.string(),
|
||||
@@ -55,39 +67,43 @@ export const OutboundTrafficRowSchema = z.object({
|
||||
|
||||
export const OutboundTrafficListSchema = z.array(OutboundTrafficRowSchema);
|
||||
|
||||
export const OutboundTestResultSchema = z.object({
|
||||
tag: z.string().optional(),
|
||||
success: z.boolean(),
|
||||
delay: z.number().optional(),
|
||||
error: z.string().optional(),
|
||||
mode: z.string().optional(),
|
||||
// HTTP-mode extras: status answered by the test URL plus the httptrace
|
||||
// timing breakdown (dial to local inbound / target TLS via the outbound /
|
||||
// time to first byte).
|
||||
httpStatus: z.number().optional(),
|
||||
connectMs: z.number().optional(),
|
||||
tlsMs: z.number().optional(),
|
||||
ttfbMs: z.number().optional(),
|
||||
endpoints: z
|
||||
.array(
|
||||
z.object({
|
||||
address: z.string(),
|
||||
delay: z.number().optional(),
|
||||
success: z.boolean(),
|
||||
error: z.string().optional(),
|
||||
}).loose(),
|
||||
)
|
||||
.optional(),
|
||||
egress: z
|
||||
.object({
|
||||
ipv4: z.string().optional(),
|
||||
ipv6: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
warp: z.string().optional(),
|
||||
})
|
||||
.loose()
|
||||
.optional(),
|
||||
}).loose();
|
||||
export const OutboundTestResultSchema = z
|
||||
.object({
|
||||
tag: z.string().optional(),
|
||||
success: z.boolean(),
|
||||
delay: z.number().optional(),
|
||||
error: z.string().optional(),
|
||||
mode: z.string().optional(),
|
||||
// HTTP-mode extras: status answered by the test URL plus the httptrace
|
||||
// timing breakdown (dial to local inbound / target TLS via the outbound /
|
||||
// time to first byte).
|
||||
httpStatus: z.number().optional(),
|
||||
connectMs: z.number().optional(),
|
||||
tlsMs: z.number().optional(),
|
||||
ttfbMs: z.number().optional(),
|
||||
endpoints: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
address: z.string(),
|
||||
delay: z.number().optional(),
|
||||
success: z.boolean(),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
.loose(),
|
||||
)
|
||||
.optional(),
|
||||
egress: z
|
||||
.object({
|
||||
ipv4: z.string().optional(),
|
||||
ipv6: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
warp: z.string().optional(),
|
||||
})
|
||||
.loose()
|
||||
.optional(),
|
||||
})
|
||||
.loose();
|
||||
|
||||
// Batch results from /xray/testOutbounds, aligned with the request order.
|
||||
export const OutboundTestResultListSchema = z.array(OutboundTestResultSchema);
|
||||
@@ -110,10 +126,11 @@ export const RuleFormSchema = z.object({
|
||||
});
|
||||
|
||||
export const BalancerFormSchema = z.object({
|
||||
tag: z.string().trim().min(1, 'pages.xray.balancerTagRequired').refine(
|
||||
(val) => !val.startsWith('_bl_'),
|
||||
{ message: 'pages.xray.balancer.reservedPrefix' },
|
||||
),
|
||||
tag: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'pages.xray.balancerTagRequired')
|
||||
.refine((val) => !val.startsWith('_bl_'), { message: 'pages.xray.balancer.reservedPrefix' }),
|
||||
strategy: BalancerStrategyTypeSchema.default('random'),
|
||||
selector: z.array(z.string()).min(1, 'pages.xray.balancerSelectorRequired'),
|
||||
fallbackTag: z.string().default(''),
|
||||
@@ -124,10 +141,7 @@ export const OutboundTagSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'pages.xray.outboundTagRequired')
|
||||
.refine(
|
||||
(val) => !val.startsWith('_bl_'),
|
||||
{ message: 'pages.xray.balancer.reservedPrefix' },
|
||||
);
|
||||
.refine((val) => !val.startsWith('_bl_'), { message: 'pages.xray.balancer.reservedPrefix' });
|
||||
|
||||
export type BalancerFormValues = z.infer<typeof BalancerFormSchema>;
|
||||
export type RuleFormValues = z.infer<typeof RuleFormSchema>;
|
||||
|
||||
Reference in New Issue
Block a user