* fix(api-docs): generate request bodies for all encodings
The OpenAPI generator only recognized generic body parameters, so JSON, form, and multipart declarations disappeared into empty application/json objects. Generate the declared media type and schema, preserve optionality and conditional requirements, and encode repeated form arrays the way Gin expects. Correct the request metadata exposed by the complete schemas and keep the panel and docs specifications synchronized.
* fix(api-docs): align alternative request schemas
Keep non-empty constraints on the selected request-body alternative without rejecting empty values for the alternatives that panel requests also include. Allow null client IP lists because model serialization emits them while cleared rows await pruning.
* fix(api-docs): send object urlencoded fields as JSON, document the inbound update body
Four defects the request-body rework exposed or left behind:
- An object-typed field in an x-www-form-urlencoded body got no encoding
entry, so OpenAPI 3.0 serialized it form-style. Swagger "Try it out"
and generated clients sent memberWeights=3&memberWeights=0.2 to
/panel/api/sub-balancers, and parseSubBalancerForm json.Unmarshals the
raw field, so every such call failed with "invalid memberWeights".
Emit encoding.<name>.contentType = application/json instead.
- bodyRequiredOneOf names were never checked against the declared body
params: a typo emitted an anyOf branch requiring a property that does
not exist — unsatisfiable — and make gen still passed. Throw now, and
extend the requestSchema guard to reject bodyRequiredOneOf as well.
- /panel/api/inbounds/update/:id advertised no request body although its
own summary says the shape mirrors /add and updateInbound binds one.
Both entries now share an inboundBody const so they cannot drift.
- The mixed-locations error was the only buildOperation throw without
the method and path, aborting make gen without naming the offender.
Regenerated frontend/public/openapi.json and copied it to
docs/public/openapi.json. No MDX regeneration: no summary changed.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
* feat(release): publish SHA-256 sums and verify them in install.sh/update.sh
The installer and updater fetched the release archive and extracted it
after checking only that the file is not empty, and the release workflow
published no checksums. TLS protects the transport, not the bytes: a
truncated or swapped asset, a bad mirror or a TLS-terminating proxy was
installed as root. #5396 added this verification for the Xray archive;
the panel's own archive was the remaining unverified download.
Publish <asset>.sha256 next to every release archive (Linux and Windows)
and verify it before extracting. A mismatch aborts the install; a missing
sidecar, which every release before this change has, only warns, so
installing older tags keeps working.
Assisted-by: Claude Code:claude-fable-5-1
* fix(install): fail closed when the checksum sidecar cannot be fetched
Review follow-up. Any curl failure on the sidecar (5xx, reset, DNS) was
treated as "no checksum published", so whoever can swap the archive
could also drop the 90-byte sidecar request and skip the check. Only a
404, which every release before the sidecar existed returns, is still
tolerated with a warning; every other outcome aborts and removes the
downloaded archive.
Assisted-by: Claude Code:claude-fable-5-1
* fix(install): restore the closing brace lost in the main merge
* fix(x-ui.sh): put the fail2ban backend override in jail.d, not jail.conf
create_iplimit_jails switched the global fail2ban backend to systemd on
Debian 12+ and Ubuntu 22.04+ with sed on /etc/fail2ban/jail.conf. That
file is the package's conffile: the next fail2ban upgrade either drops
the edit or keeps a stale jail.conf, depending on the conffile prompt.
Write the same override to /etc/fail2ban/jail.d/3x-ipl-backend.conf,
which fail2ban reads after jail.conf and which upgrades leave alone, and
remove it together with the other 3x-ipl files on uninstall. The 3x-ipl
jail itself keeps its explicit backend=auto.
Assisted-by: Claude Code:claude-fable-5-1
* fix(x-ui.sh): only override the stock fail2ban backend, keep it on partial removal
Review follow-ups. The old sed only rewrote a literal 'backend = auto'
in jail.conf's [DEFAULT], so an operator's own backend survived it; the
override file was written unconditionally. Write it only when jail.conf
still carries the stock value. And keep the file when only the IP-limit
jail is removed: the sed was never reverted either, and deleting a
[DEFAULT] override there would flip every inheriting jail back to auto
on the restart in the same branch. The full /etc/fail2ban removal path
still deletes it.
Assisted-by: Claude Code:claude-fable-5-1
* fix(install): fetch x-ui.sh and unit files from the installed release tag
install.sh and update.sh pin the panel archive to a release tag but always
took x-ui.sh, x-ui.rc and the service units from main, so the management
script and the binary of one installation came from different commits:
the fail2ban templates and setting flags the script writes drift silently
against an older binary, two installs of the same tag differ, and a
reviewed or digest-pinned installer still runs unreviewed code from main.
Use the same ref as the archive, keeping main only for the rolling
dev-latest build. The menu's "update menu" and update_shell paths now
fetch the script matching the installed version and fall back to main
with a visible notice when no script is published for it.
Assisted-by: Claude Code:claude-fable-5-1
* fix(install): fall back to main for files a pinned tag does not publish
Review follow-ups. install.sh accepts tags down to v2.3.5, but x-ui.rc
only exists from v2.8.4 and the split x-ui.service.* files are newer
still, so pinning those to the tag made an Alpine install of an old tag
404 after the previous install was already removed. Probe the tag for
each file and fall back to main with a notice when it is missing, as
the menu already does for x-ui.sh.
The fail2ban auto-setup probe also trusted the exit status of
'x-ui setup-fail2ban', but scripts before v3.4.0 have no such
subcommand and exit 0 from the usage banner, so the installer reported
a setup that never ran. Skip with a notice when the installed script
does not know the subcommand.
Assisted-by: Claude Code:claude-fable-5-1
* fix(install): refuse a tag that does not publish a needed script
Falling back to main reintroduced the binary/script mismatch the tag
pinning exists to remove, and it fired at points where install.sh and
update.sh have already stopped and removed the previous installation --
so the quiet path was also the one that could not be undone.
Probe the tag instead, before anything is touched, for every file that is
always fetched from GitHub (x-ui.sh, plus x-ui.rc on Alpine), and abort
with the HTTP status when one is missing. The unit files stay unprobed:
they are only fetched when the release tarball omits them, so an old tag
that ships x-ui.service inside its tarball still installs. Their existing
failure message now names the ref it tried.
Also tighten the setup-fail2ban probe to the dispatcher's case arm rather
than any mention of the string, which also matches a comment.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(database): keep the SQLite store owner-only
InitDB created the data directory 0755 and let SQLite create x-ui.db
and its -wal/-shm side files under the default umask, so on a stock
install they are world-readable. The store holds client UUIDs, Reality
private keys and the admin password hash, so any local account could
read them.
Create the directory 0700 and chmod the database files to 0600 right
after opening. SQLite gives -wal/-shm the mode of the main file, so
files created later inherit it; existing installs are tightened on the
next start. PostgreSQL deployments are untouched.
Assisted-by: Claude Code:claude-fable-5-1
* fix(database): tolerate chmod failures, keep the dump and install dir owner-only
Review follow-ups. A store the panel cannot chmod (root_squash NFS, a
foreign uid in a container) refused to start, which is worse than the
0644 it had before; log and continue instead, as the backup-directory
cleanup above already does. install.sh reset /etc/x-ui to 0755 right
after the binary created it 0700, so the directory hunk was inert on
real installs; create it 0700 there too. The migrate-db dump in the same
directory is a plaintext copy of the same secrets and was written 0644.
Assisted-by: Claude Code:claude-fable-5-1
* fix(sub): randomize fresh panel subscription paths
Seed distinct cryptographically random paths for base64, JSON, and Clash subscriptions when a panel database is first created. Persist them so restarts keep published URLs stable while upgrades preserve existing settings.
Generated-by: OpenCode:gpt-5.6-sol
* fix(sub): regenerate paths on settings reset
Keep subscription paths unpredictable after a factory reset, close the test database on failure, and update the builder, OpenAPI, and localized docs to describe panel-specific paths instead of obsolete fixed defaults.
Generated-by: OpenCode:gpt-5.6-sol