mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-21 18:37:14 +00:00
58669f6146662b0cc42c528832df8cd291e26b9d
281 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
58669f6146 |
refactor(ci): replace the in-house review lanes with the official code-review skill
The four pull_request_target review jobs in claude-bot.yml (Senior Developer / QA / Tester / Arbiter and their shared rubric) are replaced by a single review job running the official code-review plugin - the same skill behind Anthropic's hosted Code Review and the review workflow /install-github-app generates. The hosted service needs a Team/Enterprise organisation, so the plugin runs in CI on the maintainer's subscription instead: inline findings on PR open and ready-for-review, plus manual (re-)review when the owner or a collaborator comments "@claude review". The official example triggers on pull_request, but GitHub withholds secrets from fork runs and essentially every 3x-ui pull request is from a fork, so the job keeps the lanes' pull_request_target posture: the workspace is the base revision and nothing from the pull request is checked out or executed. What the lanes uniquely knew is distilled into REVIEW.md, handed to the skill via --append-system-prompt and pinned by bot_context_test.go the way repo-context.md is: the runtime.Runtime dispatch rule, migration and upgrade safety, the four-step route contract chain including the unchecked docs copy, the i18n rule, the three link implementations, and the wire-format verification bar. The mention job now ignores "@claude review" comments on pull requests so the review trigger does not also wake the generic bot, and the lane-only rubric file goes with the lanes. The remaining prompts also lose their tone micro-rules (no emoji, no exclamation marks, no filler) and the workflow's comment banners are removed. |
||
|
|
19e71d9acc | refactor(ci): move the bot's repository briefing into versioned files a test pins | ||
|
|
92fb94d856 |
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.
|
||
|
|
f75ea08ab4 |
fix(frontend): keep the MSW worker in step with the lockfile (#6222)
The tracked frontend/public/mockServiceWorker.js is generated by msw and pinned at 2.14.7, while package-lock.json installs 2.15.0. msw rewrites the worker on postinstall, so npm ci leaves the tree dirty on a clean checkout and every contributor either commits an unrelated 30-line diff or discards it. Regenerate the worker for the locked version and add a check that compares it against the installed runtime, wired into make verify and CI so the pair cannot drift again. |
||
|
|
2b1fe1fd02 |
ci: actually run the PostgreSQL schema and migration tests (#6224)
* ci: actually run the PostgreSQL schema and migration tests TestHostAutoMigrateCreatesColumns_Postgres and TestMigrate_Postgres skip unless XUI_DB_TYPE and XUI_DB_DSN are set. CI sets them only for the durable-first step, so both tests have never run: a green pipeline says nothing about the PostgreSQL schema or the migration path. The job already has a PostgreSQL service. Point those two tests at it and fail if either skips, the same guard the durable-first step uses. * ci: make the PostgreSQL guard fail on a renamed test, and self-test the workflow The guard asserted the absence of `--- SKIP`, which only catches a test that ran and skipped. A renamed or deleted test makes `-run` match nothing, so `go test` prints "no tests to run" and exits 0 — the step stays green while testing nothing, which is the exact failure this PR set out to close. Both steps now count `--- PASS` lines and require the expected number: at least one for durable-first, exactly two for the schema tests. Also adds `.github/workflows/ci.yml` to both `paths` filters so a change to the workflow runs the workflow — without it this PR's own CI never fired and the new step would first execute on main after merge — and hoists the duplicated DSN to job-level `env`. --------- Co-authored-by: n0ctal <n0ctal@users.noreply.github.com> |
||
|
|
dc1979a14c |
ci(release): stamp released binaries with their source revision (#6223)
* ci(release): stamp released binaries with their source revision The release job builds `main.go`, which Go records as a file list rather than a package, so the binary carries no VCS metadata at all: `go version -m xui-release` reports command-line-arguments and nothing else. There is no way to tell which commit a published binary came from, which is exactly what you want when a user reports a bug against "the latest release". Build the package with -buildvcs=true instead. Same output, same flags, plus vcs.revision and vcs.time in the binary. * ci(release): give the Windows job the git it needs to stamp -buildvcs=true fails the build when it cannot read VCS state, and the MSYS2 shell the Windows job runs in has no git on PATH. Install it there so the Windows binary carries the same revision as the Linux ones instead of the flag turning into a build break. --------- Co-authored-by: n0ctal <n0ctal@users.noreply.github.com> |
||
|
|
8cec47a8a5 |
fix(ci): resync the bot prompts with the repo and close the gaps an audit found
The three prompts still enforced the comment ban CLAUDE.md replaced with
the 2-line cap on Aug 1 (
|
||
|
|
7c8a9a6909 | ci: attach provenance and SBOM attestations to the published images (#6130) | ||
|
|
1e2d6f6081 |
fix(ci): close the TOCTOU race in the conflict-resolution bot
The resolve-conflicts job runs on issue_comment, a privileged trigger: it holds GITHUB_TOKEN, the Claude OAuth token and the push PAT, and it checks out fork code with `gh pr checkout`. The only gate was that the commenter is the repository owner, which says nothing about the code that ends up in the workspace. A contributor could force-push to the pull request head between the owner asking for the merge and the runner fetching it, so the owner reviews one tree and the job runs another. Verify before anything is checked out that the head repository was last pushed to before the triggering comment was written, and refuse the run otherwise. Pin the head SHA reported by that check and abort if the commit `gh pr checkout` lands on differs, which closes the remaining window between the check and the fetch. Require author_association to be OWNER alongside the existing login comparison. This also clears CodeQL actions/untrusted-checkout/high, which for issue_comment triggers demands both an actor/association check and a comment-vs-head-date check dominating the checkout. |
||
|
|
b6473004ac |
fix(ci): harden the conflict resolver against the branch it checks out
resolve-conflicts is the one job that puts a pull request's own tree in the working directory while holding CLAUDE_CODE_OAUTH_TOKEN, CLAUDE_BOT_PAT and a write-scoped token, which is what CodeQL alert 101 (actions/untrusted-checkout) points at. Nothing in the job executes that tree and the trigger is gated on the repository owner, so the alert is not reachable as written, but two of its guards were weaker than they read. Git hooks were neutered only after gh pr checkout had already run, so the guard sat one step behind the checkout it exists to cover; it now precedes it. The conflicted paths are concatenated into the --allowedTools value handed to the model, so a path carrying a comma or a parenthesis would widen that allowlist. Only both-modified paths reach that code today, which means they already exist in the base repository, but the merge is now handed back to the maintainer unless every conflicted path is plain [A-Za-z0-9._/-]. The file's header comment block is dropped. |
||
|
|
7f7b7e16a4 |
feat(xray): update xray-core to v26.7.28 and adapt panel
Bump xtls/xray-core to 5ca6f4b7d4dc (v26.7.28) and move the three binary pins (DockerInit.sh, the Linux and Windows URLs in release.yml) in lockstep so the in-process conf.Build() validation and the child binary agree. XMC finalmask (#6487) is the breaking change. The mask's `usernames` string list is gone, replaced by a required `profiles` array whose entries each need a 3-16 character [A-Za-z0-9_] username, a parseable UUID and both Mojang texture fields; the "default to Dream when empty" fallback was removed, so an xmc mask saved by an older panel now fails to build and takes the whole config down with it rather than degrading one inbound. The textures are a signed blob only Mojang's session server can issue, so a legacy username cannot be upgraded automatically. The panel now: - rejects an incomplete xmc mask at save time (AddInbound/UpdateInbound), pointing at the specific field that is missing; - drops only the offending mask when generating the core config, for rows that never went through the form (upgrade, node sync, restored backup, direct DB edit), warning which inbound lost its obfuscation instead of leaving every inbound offline; - carries legacy usernames into profile stubs in the finalmask form so the operator keeps their player names and sees exactly what still needs filling in, and edits profiles through a list editor. No destructive DB migration: unlike the removed shadowsocks ciphers there is no valid replacement to rewrite to, and dropping the mask from stored rows would discard the operator's hostname and password for config they can still repair. The generation-time strip already prevents the startup failure. Also track the core's xmux maxConnections fallback, lowered from 6 to 3 for anti-TSPU, in the fresh-XMUX seed so a new panel config matches what the core would pick on its own. TUN gained a `desc` key and random utunN naming, but the Go validator no longer accepts TUN inbounds and the panel only renders legacy saved rows, so nothing there needs adapting. The remaining commits are REALITY log-warning wording, gRPC/XHTTP localAddr accuracy and a routing tweak, none of which change the JSON config surface. Tests cross-check the panel's profile predicate against conf.XMCProfile.Build() so a future core release that tightens or relaxes the rules fails loudly rather than silently emitting configs the core refuses to start on. |
||
|
|
5accd8a611 |
fix(ci): stop the conflict job trusting the branch it is merging
A second audit of the hardened workflow found the "no shell at all"
claim in resolve-conflicts was still false, by two routes that live
outside this file.
The job runs the model in the workspace right after `gh pr checkout`,
so for a fork pull request the working directory is attacker-controlled.
claude-code-action writes `enableAllProjectMcpServers = true` into
~/.claude/settings.json before starting Claude Code
(base-action/src/setup-claude-code-settings.ts), and the CLI honours a
project `.mcp.json` unless `strictMcpConfig` is set, which the action
never sets. A contributor branch carrying an `.mcp.json` therefore got
its command spawned at session start, with --allowedTools gating tool
calls but not server startup. The same tree also supplied CLAUDE.md and
.claude/ as project instructions. The job now passes
`--strict-mcp-config` and `--setting-sources user`, so nothing in the
merged tree configures the session.
The second route was `Edit` with no path scope, the only unscoped file
grant left. Editing `.git/config` to set `core.fsmonitor` or a
`credential.helper` gets a command run by the next step's git calls,
which hold CLAUDE_BOT_PAT, and the stray-file guard could never see it
because `git diff --name-only` lists tracked paths only. The merge step
now emits one `Edit(//<workspace>/<file>)` rule per conflicted path and
the model gets exactly those plus /tmp, with `.git/**` denied outright
and Bash, WebFetch, WebSearch and Task denied by name. Hooks are
disabled for the run (`core.hooksPath=/dev/null`, `commit --no-verify`).
Conflict handling gets three real gaps closed: modify/delete, rename and
both-added conflicts (git status DD/AU/UD/DU/AA/UA) leave no markers, so
they used to sail through the marker check and get committed unresolved
- they are now detected up front and handed back untouched; the marker
scan covers `=======` and `|||||||`, not just the outer pair; and after
staging, `git diff --diff-filter=U` must come back empty or nothing is
committed. A `=======` markdown underline of exactly seven characters in
a conflicted file will now hand the merge back rather than commit it,
which is the safe direction.
Smaller things the audit was right about:
- the mutating gh rules are prefix rules, so `Bash(gh issue close:*)`
reached every issue in the repository. They now carry the triggering
number: `Bash(gh issue close ${{ github.event.issue.number }}:*)`.
- `Write(//tmp/**)` is granted alongside `Edit(//tmp/**)`: the docs say a
Write(path) rule is never matched by the file checks, so the Edit rule
is what authorises it, but the tool has to be listed to exist at all.
Without this the model could not create /tmp/comment.md.
- the mention prompt lost its thread context when it moved to agent mode
and referred to "<number>" literally; it now gets repo, number, title
and whether the thread is a pull request.
- `git log`/`git show` are gone from mention: `--output=<file>` makes
them a file-write primitive.
- `@claude resolve pr conflicts` on a plain issue matched no job at all.
- the commit step gated on `skip != 'true'`, so it also ran when the
merge step died before writing any output; it now needs `skip ==
'false'`.
- bot-authored pull requests (dependabot opens three ecosystems' worth)
no longer start a review run that the action refuses to serve.
- resolve-conflicts drops to `contents: read`, since the push is the
PAT's job, and fails with a comment when that PAT is missing.
|
||
|
|
f46b1726cf |
fix(ci): close the write paths an audit found still open in the bot
Making the jobs read-only in the previous commit was not enough: two of the mechanisms that grant write access were invisible in the workflow file itself. Every job now passes a `prompt:` input. Without one, claude-code-action picks tag mode for a mention, and src/modes/tag/index.ts then appends `--permission-mode acceptEdits`, its own allowedTools including `Bash(git commit:*)` and a push wrapper, and calls setupBranch. So the mention job could edit files and commit them no matter what its own allowedTools said, and its system prompt claiming otherwise was simply wrong. A `prompt:` selects agent mode, which adds nothing. It also removes tag mode's hidden requirement that the comment contain the trigger phrase, which would have made resolve-conflicts a no-op for a comment that said only "resolve pr conflicts". resolve-conflicts no longer hands git to the model. `Bash(git:*)` is a prefix rule, so it permitted `git push origin HEAD:main`, `--force`, `git remote set-url`, and shell execution through `git config alias.x '!sh -c ...'` - the action ships scripts/git-push.sh precisely because `git push:*` allows `--receive-pack='sh -c ...'`. The job now splits in three: a step checks out the PR branch, merges the base and collects the conflicted paths; the model gets Read/Glob/Grep/Edit and no shell at all; a final step verifies and pushes. That step refuses to commit if a conflict marker survives, if the model wrote /tmp/ABORT, or if anything outside the conflicted set was touched, and it stages those paths individually instead of `git add -A`. The PAT is now written to the push URL only in that last step, after the model's session has ended, instead of sitting in .git/config while untrusted branch content is read. The bare `Write` grant in the three answering jobs becomes `Edit(//tmp/**)`, since only prose kept it out of the checkout and out of $GITHUB_ACTION_PATH, whose scripts run after the model step. Each prompt now says to fall back to an inline --body if the write is refused, so a denied write cannot silently cost a reply. mention gains the transcript upload and the no-reply guard the other jobs already have, keyed to the triggering comment's timestamp. Restores the header note about the 21000-character expression cap, with the current block sizes. |
||
|
|
acbb879f80 |
refactor(ci): make the bot read-only except for PR conflict resolution
The bot is meant to investigate and explain, not to write code. It could
do considerably more than that: handle-pr-fix applied fixes and pushed
them to any trusted author's PR, an @claude mention on a pull request
could edit files, and an @claude mention on an issue opened a pull
request against main. All of it is gone.
Now every job that answers automatically runs with a contents: read
token, so pushing is impossible rather than merely forbidden:
- handle-pr-fix is deleted. handle-pr-review takes every pull request
instead of only the ones from outside contributors, and it comments.
- mention drops contents: write, the push-URL routing step, and the
Edit tool. Its Bash allowlist is now an explicit read-only set - the
gh subcommands it needs plus git log/show/diff/blame - so gh api,
gh pr merge and gh pr create are no longer reachable. Asked for a
fix, it now writes the change out in full instead of applying it.
One narrow exception replaces all of that: resolve-conflicts. It runs
only when the repository owner comments "resolve pr conflicts" on a
pull request, and it may merge the base branch into that PR's head
branch and resolve the conflicts, nothing else. It keeps both sides of
every conflict, takes the base version of generated artifacts it cannot
regenerate here, and aborts the merge rather than guess when a hunk
needs a human. It never force-pushes, merges, or closes.
Also removes the pull-request-opening step whose guard never worked:
gh api prints the 404 body on stdout, so `ahead=$(gh api ... || echo 0)`
became `{"message":"Not Found",...}0`, never equal to "0", and every
reply-only mention run ended red on `gh pr create`. Uploads the
handle-pr-review transcript the way handle-issue already does, so a run
that dies inside the sandbox leaves evidence.
|
||
|
|
1358f65bec |
fix(ci): unbreak the issue-triage bot, which answered nothing
Since 2026-07-20 every `issues` run reported success while posting no comment at all - #6094 through #6103 carry zero replies. The cause is the sandbox, not the prompt or the model. `handle-issue` and `handle-pr-review` pass allowed_non_write_users, which is what lets the bot run for reporters who have no write access. claude-code-action reacts to that input by turning subprocess isolation on and installing bubblewrap, and that sandbox cannot start on the runner: every Bash call dies during setup, before the command itself runs, with bwrap: Can't create file at /home/.mcp.json: Permission denied `gh` is reachable only through Bash, so the triage investigated the issue, wrote its reply to /tmp/comment.md, and could never post it. The action itself did not crash, so the job stayed green. Opt both jobs out with CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=0. The scrub is a best-effort wipe of secrets from subprocess environments, not an access control; what actually bounds these jobs is unchanged - a contents: read token that cannot push, and a Bash allowlist holding only specific `gh issue`, `gh label`, `gh search` and `gh release` subcommands. Code changes stay confined to handle-pr-fix and mention, which only trusted actors and the owner can trigger. Add a step to each job that fails the run when no bot comment landed on the issue or pull request, so the next silent breakage shows up red instead of green, and lower retention-days to the repository maximum of 7 so the artifact upload stops warning. |
||
|
|
edb487a005 |
chore(deps): migrate to react-router 8 and refresh frontend dependencies
react-router-dom 7 is superseded by react-router 8, which folds the DOM bindings back into the core package. RouterProvider now comes from `react-router/dom`, while the hooks and `createBrowserRouter` move to `react-router`. Updates the nine importing modules and the router line in docs/architecture.md to match. Also refreshes antd, react-i18next, storybook, eslint, lint-staged and playwright to current patch/minor releases, and restores alphabetical order in devDependencies for the @vitest/browser-playwright and playwright entries. Bumps brace-expansion to 5.0.8, the only release outside the affected range of GHSA-mh99-v99m-4gvg (unbounded expansion length causing an OOM crash). `npm audit fix` could not apply this on its own: the lockfile pinned 5.0.7 and npm will not re-resolve a transitive-only dependency in place, so the entry was updated directly and reinstalled. |
||
|
|
35cf6be6f9 |
fix(ci): keep the triage prompt under the 21000-char expression cap
The previous commit pushed handle-issue's prompt to 21587 characters and
GitHub stopped parsing the file: "(Line: 39, Col: 19): Exceeded max
expression length 21000". Because the prompt interpolates ${{ }}, GitHub
treats the whole block scalar as a single expression, and the cap applies
per expression. The failure mode is quiet and total - no job fails,
the workflow itself disappears, its registered name reverts from "Claude
Bot" to the file path, and the only signal is a run attributed to the
push with no jobs in it.
Drop the hand-written stack description, repository map and runtime-fact
list from that prompt and point at CLAUDE.md and docs/architecture.md
instead. Both are maintained, both are already in the checkout, and the
copy in the prompt had drifted from them anyway - it still described the
mtg worker, omitted internal/tunnelmonitor/ and memory.high, and filed
internal/web/runtime/ under "wiring". Only the support-facing facts that
live in neither file are kept: the install one-liner, the random initial
credentials, the distro-dependent env file, the Docker image and the
capabilities fail2ban needs.
handle-issue is now 15069 characters, and a header comment records the
limit so the next edit does not rediscover it in production.
|
||
|
|
0f7329c3ce |
fix(ci): repair the Claude bot and narrow what it can reach
Three problems, all in .github/workflows/claude-bot.yml. It was silently dead. No comment had been posted since 2026-07-20 while every run reported success: roughly twenty issues and pull requests each burned 18-56 turns and up to $2.59, ended with permission denials, and published nothing. Comment bodies are markdown, markdown is full of backticks, and inside a quoted `--body "..."` backticks are command substitution, so the write was rejected and a failed triage looked exactly like a clean one. The body now goes to /tmp through Write and out through --body-file, in every branch of both jobs, and each job re-reads the thread afterwards so a rejected write fails loudly instead of reporting success. The run transcript is kept as an artifact. It could reach much further than it claimed. Both jobs that any GitHub user can trigger declared themselves READ-ONLY in prose while holding Bash(gh:*), which is not a GitHub-scoped allowlist: `gh alias set --shell` runs its argument through sh -c and `gh extension install` fetches and executes code, both as single commands whose first token is gh. That is a general shell on a runner holding CLAUDE_CODE_OAUTH_TOKEN, which does not expire with the job. `gh api` accepted any method, issues: write is repo-scoped rather than issue-scoped, and `gh pr review --approve`, `gh pr close` and `gh pr checkout` were forbidden in prose only. Those two jobs now list the subcommands they actually run. The untrusted title and body are fenced in tags carrying github.run_id, unguessable at the time the issue is written, and the invariants an allowlist cannot express - one issue number, labels and title only, /tmp as the sole writable path, never $GITHUB_ENV - are stated explicitly. Both checkouts get persist-credentials: false. handle-pr-fix and mention keep their wildcards: only owners, members and collaborators can trigger them, and narrowing the maintainer's own path risks more than it protects. Its review hid findings and its triage quoted stale facts. "Prefer a few high-signal findings over many low-value ones" is read literally by Opus - it finds the bug, judges it below the stated bar and says nothing - while the Severity and Confidence tiers already existed to do that filtering. The review also never said that the working directory is the base revision, so it could assert that a case was unhandled in code the pull request had already rewritten, and label it confirmed, on an outside contributor's first patch. Four CLAUDE.md conventions were missing, each a guaranteed miss: openapigen's StructAllow allowlist, the layering rules including the runtime.Runtime dispatch requirement that silently breaks multi-node when bypassed, the assertion standard, and golden share-link fixtures regenerated to turn a red test green. On the triage side the invalid and duplicate branches were gated three times over and so never fired, leaving spam to collect a full investigation and a courteous reply; /etc/default/x-ui was given as the env file when it is distro-dependent, making the PostgreSQL migration advice a silent no-op on RHEL and Arch; an env list labelled "full" omitted XUI_PORT and the XUI_TUNNEL_HEALTH_* family; XTLS was offered as a security option the panel does not have. docs/architecture.md was invisible to both prompts despite being maintained and already in the checkout. From the bot's own output: it published a trigger only the maintainer can use, retitled issues without saying so, asked for screenshots it cannot open, and once invented a reason for a number it had miscounted. All four jobs move to Opus 5, at xhigh effort rather than max - the recommended tier for agentic work, and one below the overthinking that max invites on routine triage. |
||
|
|
2f156c8eb0 |
fix(ci): publish dev-latest edit-first instead of probing for existence
The dev-latest publish step probed for the release with gh release view before choosing edit or create. During the api.github.com 503 storm the probe itself failed, mis-routing an existing release into the create path, which then died on a permanent 422 already-exists error that no amount of retrying can fix. The release exists on every run but the very first, so edit first and fall back to create only when the edit fails. The retry log line now names the gh subcommand instead of just the binary. |
||
|
|
455d1cd0f7 |
fix(ci): resolve the mtg-multi tag from the release-page redirect
The api.github.com outage that failed the previous release run outlasted the retry window, while asset downloads from github.com kept working the whole time. The latest-release lookup was the only api.github.com dependency left in the build jobs, so resolve the tag from the release page redirect on github.com instead: tag resolution now shares exactly the failure domain of the downloads it feeds, and an API-only outage can no longer fail a build that could otherwise finish. No token needed for the redirect, on either platform. |
||
|
|
ab1a922806 |
fix(ci): survive transient GitHub 5xx outages in the release workflow
A GitHub API 503 storm failed the release run four attempts in a row: the mtg-multi latest-release lookup died on every platform (even s390x, which never packages the sidecar), and the matrix default fail-fast then cancelled the six healthy builds alongside the one that hit the outage. Retry every external fetch with backoff (curl --retry-all-errors, wget --retry-on-http-error, Invoke-* -MaximumRetryCount), scope the mtg-multi tag lookup to the platforms that actually package the sidecar, disable fail-fast on the build matrix, and retry the idempotent dev-latest publish commands. The workflow file itself now triggers the run's path filters, so editing it exercises the release build immediately instead of failing silently at the next code push. |
||
|
|
444e1e5917 |
chore(deps): bump actions/setup-go from 6 to 7 (#5995)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6 to 7. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
73b479e5a0 |
chore(deps): bump actions/setup-node from 6 to 7 (#5992)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6 to 7. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
b11ceac18e |
fix(ci): install the docs-pinned pnpm instead of floating on 11.x
pnpm/action-setup resolved 'version: 11' to the newest 11.x, and its self-installer crashes upgrading to 11.12.0 (Cannot use 'in' operator to search for 'integrity'), failing both docs workflows at setup. Reading docs/package.json instead installs the exact packageManager pin (pnpm@11.9.0), which also keeps the workflows and the lockfile toolchain on a single source of truth. |
||
|
|
bbc4163768 |
chore: standardize the toolchain on Node 24 LTS
The repo now pins Node 24 everywhere instead of mixing 22 and hardcoded workflow versions. The docs workflows read .nvmrc like the main CI already did, so the Storybook bundle in the Pages deploy builds on the same runtime as the PR gate. The docs gen:api script runs its TypeScript entry natively, dropping the experimental type-stripping flag that Node 24 makes default; the matching frontend cleanup (engines and gen:api) landed with the Storybook commit. |
||
|
|
df3ba568d1 |
feat(docs): publish the component Storybook on the docs site
The docs site and the component workbench were entirely disconnected. The Pages deploy now builds the frontend Storybook and bundles it into the artifact under /storybook, so the live component reference ships with the documentation, and the navbar links to it. Story changes trigger a redeploy so the published workbench cannot go stale. |
||
|
|
7078abc14a |
feat(frontend): make Storybook a validated, fully covered component workbench
Storybook existed only as an undocumented local tool: 9 of 24 reusable components had stories, autodocs pages were bare prop tables, nothing built or tested the stories, and no contributor doc mentioned the workbench existed. Every reusable component under src/components/ now has a co-located story with enriched autodocs (component descriptions plus per-prop argTypes, kept as string metadata since the repo bans line comments). Stories double as headless Chromium tests through the Storybook vitest addon, with axe accessibility checks enforced as errors and play-function interaction tests covering the modals, the RHF field bridge, the config block, and the select-all buttons. The preview now mirrors the panel's real theme DOM (body class, shared AntD theme config, seeded theme storage) so what stories render matches production. CI and make verify gain a static Storybook build as a compile gate, and the frontend test job installs Chromium so story tests run on every PR. Contributor docs (frontend README, CONTRIBUTING, agent guides) document the workbench, the story conventions, and the Controls setup. Node engines move to 24 LTS and gen:api drops the type-stripping flags that Node 24 makes default. |
||
|
|
814cda3fb4 |
feat(xray): update xray-core to v26.7.11 and adapt panel
Bump xtls/xray-core to 50231eaf (v26.7.11) and the three binary pins (DockerInit.sh, release.yml x2) in lockstep. Adapt the panel to the upstream changes: - Shadowsocks "none"/"plain" and VMess "none"/"zero" were removed from the core. A migration rewrites stored none/plain SS methods to a supported cipher and none/zero VMess security to "auto" (on both the clients column and inbound settings JSON); the SS build-time heal does the same so a row injected after boot cannot brick startup. The removed values are dropped from every frontend option list, schema and adapter, and coerced to "auto" at the Go link/sub/Clash emit sites and both link importers. Fix the CipherType_NONE sentinel that no longer compiles. - Unencrypted vless/trojan outbounds to a public address are now refused by the core. Validate outbounds through the vendored config loader when saving the xray template and when storing/merging outbound subscriptions, so one such outbound cannot keep the core from starting. - New TCP finalmask type "xmc" (Minecraft mimicry): add it to the sub link allowlist, the frontend enum and the FinalMask form (hostname, usernames, required password), and document it. - streamSettings gained a "method" alias for "network"; canonicalize it to "network" at inbound save time and in the form adapters/schema so a method-keyed config keeps its transport. - New root "env" config key is passed through xray.Config, compared in Equals, and forces a restart in the hot diff. - REALITY now defaults minClientVer to 26.3.27; update the form placeholder. |
||
|
|
c62e8c6bbe |
ci(claude-bot): structure PR review and issue triage prompts
Rework the handle-pr-review, handle-pr-fix, and handle-issue prompts to produce professional, structured output. The review job now rates findings by severity and confidence across explicit review areas and reports a Summary, Findings, and a text-only verdict in one plain comment; the fix job reuses the same lens to prioritize what it applies versus leaves for the author; issue triage gains a structured bug-confirmation format and explicit outcomes for mislabeled and not-a-bug reports, closing conservatively. Severity uses text labels to respect the no-emoji house style, and the adapted ignore-list keeps i18n and generated files flaggable. |
||
|
|
d33b6865a9 |
ci(claude-bot): auto-open the PR after an owner @claude fix on an issue
claude-code-action only pushes a branch and posts a Create PR link by design; it never opens the PR itself. Add a post-step to the mention job that opens a PR from the action's branch_name output when the trigger was an issue (guarded against no-op branches and against an existing PR). Simplify the mention prompt so the agent just makes edits with Edit/Write and lets the workflow commit and open the PR, instead of running git/gh pr create itself (which fought the action's built-in flow and left only a link). |
||
|
|
de5b130095 |
ci(claude-bot): gate write capability to trusted actors
Make every automatic, untrusted trigger read-only and require an explicit trusted actor for any code change. - handle-issue (issue opened): read-only triage; confirm bugs and tag the maintainer, never edit code or open a PR. Authenticates as GITHUB_TOKEN so replies post as github-actions[bot], not a personal account. - handle-pr-fix (PR opened): applies fixes only for owner/member/collaborator authors; dropped allowed_non_write_users so the default write gate also applies. - handle-pr-review (PR opened, external authors): read-only review comment only. - mention (@claude comment): runs only for the repository owner; may open a PR from an issue or commit to a PR on explicit request. No job authenticates as the static PAT anymore; the PAT is used only to route git pushes for the trusted PR-fix and owner-mention paths. |
||
|
|
7780ab0e23 |
ci(claude-bot): auto-fix trusted PRs and easy issue bugs
Split the review-only handle-pr job into handle-pr-fix (owner/member/collaborator PRs: apply refactors and bug fixes directly, commit to the PR branch, no suggestion blocks) and handle-pr-review (external/fork PRs: one review-only comment, no suggestions, no code checkout). Upgrade handle-issue to open a fix PR for easy bugs (pushed via CLAUDE_BOT_PAT so pull_request CI runs on it), confirm the root cause and tag the maintainer for big bugs, and never open a PR for feature or enhancement requests. |
||
|
|
f431e9cc03 |
fix(inbounds): apply runtime changes after the DB commit (#5768)
* fix(inbounds): apply runtime changes after commit * ci: fix staticcheck findings |
||
|
|
328d920e98 |
feat(mtproto): enforce per-client quota & expiry via mtg-multi limits
Map each mtproto client's totalGB and expiryTime onto mtg-multi's new
[secret-limits] (quota/expires): emit them into the generated config and
hot-apply through PUT /secrets so live connections survive. Quota is
written as an exact "<n>B" byte count that round-trips through both the
config and API parsers without the precision loss of a base-2 unit.
The sidecar's quota counter is not pruned when a secret is dropped, so a
panel-side traffic reset re-pushes the client's secret and then calls
POST /secrets/{name}/reset-quota (wired into every reset path) so a
renewed client is not immediately re-blocked.
Resolve the mtg-multi binary from the fork's latest release tag in
DockerInit.sh and release.yml instead of a hardcoded version pin, so the
panel no longer needs a manual bump per fork release.
|
||
|
|
61e12e4c29 |
Frontend dev tooling (Husky, lint-staged, MSW, Storybook) + full React Hook Form migration (#5859)
* chore(frontend): add husky + lint-staged pre-commit gate Wire a local pre-commit gate that runs eslint --fix on staged frontend TypeScript via lint-staged. Because the only package.json lives in frontend/ while the git root is one level up, the prepare script installs husky hooks at frontend/.husky from the repo root (cd .. && husky frontend/.husky), and the pre-commit hook cd's into frontend/ before invoking lint-staged so node_modules resolves. * test(frontend): add MSW request mocking Add Mock Service Worker so tests can exercise the real http-init.ts request pipeline (CSRF acquisition, 403 refetch-and-retry, body parsing) instead of only stubbing HttpUtil. A node setupServer is started for the vitest unit project with onUnhandledRequest bypass so the existing HttpUtil spies and 55 component tests are untouched; the browser worker is copied to public/ for Storybook and dev use. * chore(frontend): add Storybook + component stories Set up Storybook 10 on the React-Vite builder (compatible with the pinned Vite 8.1.3 and React 19). The preview decorator mirrors the vitest component harness: an Ant Design ConfigProvider with a light/dark toolbar toggle and an en-US i18next instance. main.ts neutralizes the app vite config bits that do not belong in a component workshop (the three-entry rollup input, renderBuiltUrl, and the shared dist outDir) so build-storybook can never clobber internal/web/dist. Seeds stories across the presentational library (viz, ui, clients, feedback). build-storybook is a local tool and is not wired into the CI gate. * feat(frontend): add React Hook Form primitives Introduce the shared RHF layer that AntD inputs bind through, ahead of migrating the forms off Ant Design's Form store: - FormField wraps a Controller in an Ant Design Form.Item shell, reconciling the value/onChange shapes of Input, Switch, InputNumber, Select and friends via normalizeAntdOnChange, with input/output transforms and Zod-issue-key error messages resolved through t(). - useZodForm wires zodResolver (Zod 4) with the AntD-matching modes (validate on submit, then live) and shouldUnregister false so hidden and unmounted-tab fields keep their values. - rhfZodValidate covers the rare per-field rule sites. Covered by a FormField test exercising normalization, transforms, and resolver error surfacing. * refactor(frontend): migrate Pattern-B leaf forms to React Hook Form Move the controlled-useState leaf forms onto RHF via the FormField primitive, keeping Ant Design components and each form's exact submit behaviour (same safeParse, same toast on the first Zod issue, same payload building): - clients: ClientBulkAdjustModal, BulkAddToGroupModal, ClientBulkAddModal - xray: RuleFormModal, BalancerFormModal, WarpModal, NordModal Multi-control widgets that don't fit a single input (inbound dual select, subId regen, expiry branches, the balancer tag warning) stay as explicit Controller/setValue. Derived visibility now reads live values through useWatch. FormField gains a required prop so migrated fields keep their required-asterisk affordance. Settings tabs are intentionally excluded: they are control-panel components that live-patch a parent AllSetting via SettingListItem, not Ant Design Form submit-forms. * refactor(frontend): migrate LoginPage to React Hook Form Replace the Ant Design Form store + antdRule per-field validation with useForm + FormField. The AntD Form stays as the layout/submit wrapper, now driving methods.handleSubmit(onSubmit) via onFinish. Username and password validate through rhfZodValidate(LoginFormSchema.shape.*); the two-factor field keeps its conditional required rule (only registered when 2FA is enabled). Submit posts the same values to /login. * refactor(frontend): migrate ClientFormModal to React Hook Form Move the client add/edit form off controlled useState onto RHF while preserving exact submit behaviour (same ClientFormSchema / ClientCreateFormSchema safeParse, same toast, same payload + attach/ detach diff + external-links build). expiryDate is stored as an epoch number (never a Dayjs) to survive RHF's value cloning, converted at the DateTimePicker boundary. externalLinks uses useFieldArray with stable ids. inboundIds and the derived show*/ss2022 visibility read live via useWatch. Space.Compact button-group widgets stay manual Controllers so the joined borders keep working. * refactor(frontend): migrate Node and DNS modals to React Hook Form Both are self-contained Pattern-A forms (no shared fragments). Replace Form.useForm with useForm + FormProvider, Form.useWatch with useWatch, setFieldValue with setValue, and partial validateFields([...]) with methods.trigger([...]). Per-field antdRule becomes rhfZodValidate rules; the Node scheme->tlsVerify cascade moves to FormField onAfterChange; the DNS domains/expectIPs/unexpectIPs string arrays are driven by useWatch + setValue. Submit runs through handleSubmit on the modal OK button, preserving each form's exact validation, payload build, and save/onConfirm behaviour. * refactor(frontend): migrate HostFormModal to React Hook Form The host external-proxy editor's outer form moves to useForm + FormProvider. Security/tab visibility reads via useWatch; the three json-form editors (HostMuxForm/HostSockoptForm/HostFinalMaskForm) are bound as value/onChange black boxes through a Controller (their own internal forms are unchanged). remark/inboundId keep their validation via rhfZodValidate; submit runs through handleSubmit and builds the same payload (isDisabled = !enable) and save call. * refactor(frontend): migrate OutboundFormModal + fragments to React Hook Form Move the outbound form cluster off Ant Design's Form store onto RHF. The parent uses useForm + FormProvider with a watch() subscription for the protocol reseed cascade and setValue-based network/security/xmux cascades; the JSON<->Basic bridge and the formValuesToWirePayload submit are preserved exactly. Every outbound transport/protocol/security fragment now binds through FormField/useWatch via context. The shared config editors stay untouched and are bound through small value/onChange adapters (src/lib/xray/forms/fields: FinalMaskField, SniffingField, SockoptCustomField) via Controller; HeaderMapEditor binds directly. The host json-form wrappers that reuse the outbound MuxForm/ SockoptForm (HostMuxForm, HostSockoptForm, OutboundSubtreeJsonForm) move to a local RHF provider to match. Outbound render/link tests pass unchanged. * refactor(frontend): migrate InboundFormModal + fragments to React Hook Form Move the inbound add/edit form (the largest form in the panel) and its transport/protocol/security fragments off Ant Design's Form store onto RHF, mirroring the outbound migration. The parent uses useForm + FormProvider with a watch() subscription for the protocol reseed cascade (type==='change' guard so programmatic resets don't reseed) and setValue-based network/security cascades; useSecurityActions drives the TLS/Reality keypair + scan through setValue. Hidden pass-through Form.Items are dropped (their values ride in the reset object and survive via shouldUnregister:false), so getValues() still returns the settings.clients subtree untouched. accounts / certificates / tun lists use useFieldArray; the shared FinalMask/Sniffing/Sockopt editors bind through the value/onChange adapters. Submit keeps the manual InboundFormSchema.safeParse + formatInboundValidation toast + formValuesToWirePayload exactly. The golden link/full fixtures pass byte-for-byte, confirming identical wire output. inbound-form-blocks test harness rewritten from a Form.useForm harness to an RHF provider. * refactor(frontend): retire antdRule; document the RHF form pattern All forms now build on React Hook Form, so the AntD-Form Zod adapter antdRule (src/utils/zodForm.ts) has no remaining callers — remove it. Update frontend/CLAUDE.md: forms use useZodForm + FormField from components/form/rhf with zodResolver/rhfZodValidate validation; AntD <Form> is layout-only; the shared FinalMask/Sniffing/Sockopt editors stay AntD islands wrapped as value/onChange adapters bound via a Controller. * chore(frontend): cover esbuild in the allowScripts allowlist esbuild (pulled in transitively by Vite/Vitest/Storybook) ships a postinstall that npm's allow-scripts flags as uncovered on every install. Its platform binary is delivered through the @esbuild/<platform> optionalDependencies, so the postinstall isn't needed here; deny it like the other entries to silence the warning. * fix(frontend): restore label layout in Sniffing/FinalMask field adapters The value/onChange adapters that wrap the shared SniffingFields and FinalMaskForm editors put them in their own isolated AntD Form, but that Form was missing the label layout the fields used to inherit from the inbound/outbound parent form. Their labels rendered full-width instead of the compact right-aligned column, so the Sniffing tab and the TCP Masks / QUIC Params sections looked broken. Give both adapter forms the same colon=false, labelCol/wrapperCol span 8/14, labelWrap layout. * ci: add least-privilege permissions to Docs CI workflow The docs-ci workflow had no explicit permissions block, so it inherited the repository default for GITHUB_TOKEN. The build job only checks out and builds the docs, so restrict it to contents: read, resolving the CodeQL actions/missing-workflow-permissions alert. |
||
|
|
9b91f0f42e |
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. |
||
|
|
406ce54fb2 |
chore(mtproto): bump the mtg-multi binary pin to v1.14.0
Same asset layout and platform coverage as v1.13.3; picks up the sidecar's Docker-style environment variable support. |
||
|
|
659f0f404c |
fix(ci): stop executing tag-checkout code in the release smoke test
CodeQL alert 99 (actions/cache-poisoning/poisonable-step): the workflow_run job runs in the default branch's cache scope, so checking out workflow_run.head_sha and executing a script from it is a cache-poisoning surface. The if-guard (event == 'push') already kept fork PRs out, but the checkout pin was never the load-bearing part of the release verification — the version argument is, since install.sh downloads that exact release binary. Run the smoke script from the default branch instead, which also matches what real users execute. |
||
|
|
6214ff4edc |
fix(mtproto): stop dropping connections on client/inbound edits; add live updates + ad-tag (#5838)
* fix(mtproto): split the mtg fingerprint into structural and secrets parts A reordered clients array in the stored settings used to read as a config change because the fingerprint concatenated secrets in array order, and one opaque fingerprint could not tell a restart-worthy change (bind address, fronting, throttle) from a secret-set change a reload-capable mtg can absorb in place. Sort the secret pairs so order stops mattering, and split the value so the upcoming hot-reload path can decide between keeping, reloading, and restarting the process. * fix(mtproto): stop restarting mtg on every inbound edit Saving an mtproto inbound tore down and respawned its mtg sidecar even when nothing material changed, dropping every live Telegram connection: the update path pushed DelInbound+AddInbound, and Remove deletes the manager's map entry, so Ensure's fingerprint no-op gate could never fire. Route mtproto updates through a single Ensure call so an edit that leaves the generated TOML alone keeps the process, and only real config changes restart it. Capturing the pre-edit protocol also fixes a latent leak: changing an inbound's protocol away from mtproto never stopped the sidecar, because the snapshot handed to the runtime already carried the new protocol and the removal took the xray branch, leaving an orphaned mtg holding the port. An mtproto push failure no longer requests an xray restart - xray cannot fix the sidecar, and the 10s reconcile job self-heals it. The regression test fakes mtg by re-executing the test binary, counting spawns through a pid file: an unchanged save and a remark-only edit must keep the process, a re-keyed secret must restart it. * fix(mtproto): exclude depleted clients from the reconcile job to match the sync push The 10s reconcile job derived mtg secret sets from raw inbound settings while the interactive push filtered clients through buildRuntimeInboundForAPI, which drops client_traffics-disabled (depleted or expired) clients. The two paths therefore disagreed on the fingerprint - each disagreement one needless mtg restart dropping live connections - and worse, the job kept serving depleted clients' secrets indefinitely, so running out of traffic never actually cut an mtproto client's access. DesiredMtprotoInstances now builds the job's desired state with the same depletion overlay the push uses (one bulk client_traffics query), drops inbounds whose every secret is filtered away so their sidecar stops, and AddInbound pushes the filtered payload too so an imported inbound carrying disabled stats does not seed a fingerprint the next reconcile disagrees with. * feat(mtproto): hot-reload mtg secrets in place instead of restarting A client add, removal, re-key, or enable-toggle changes only the [secrets] section of the generated config, yet the panel could apply it only by killing and respawning the mtg sidecar, dropping every Telegram connection on that inbound. Split the ensure decision three ways: an identical config is a no-op, a secrets-only change rewrites the TOML on the same api port and asks mtg to hot-swap it via POST /reload, and a structural change (or a failed reload) falls back to the full stop-and-start. The reload endpoint is served by the mhsanaei/mtg-multi fork; against an older binary the POST 404s and the manager restarts exactly as before, so panel and binary upgrades stay order-independent. * feat(mtproto): apply single-client edits to the sidecar immediately Client CRUD on an mtproto inbound was a runtime no-op, so an add, delete, re-key, or enable-toggle only reached mtg on the next 10s reconcile. With the sidecar now able to hot-reload, push the change straight after the edit commits: applyLocalMtproto rebuilds the inbound's filtered client set and re-applies it, so a new client works within a moment (and, on a reload-capable binary, without disturbing the others) and deleting the last client stops the process. The three interactive single-client paths (add, update, delete) call it; bulk operations still ride the reconcile job, which converges to the same state. * chore(mtproto): pin mtg-multi to the mhsanaei fork v1.13.3 The reload endpoint the panel now uses lives in the mhsanaei/mtg-multi fork, so point the source-build pin (DockerInit.sh + both release.yml matrices) at it and bump to v1.13.3. The install still produces the same mtg-multi binary name, so the mtg-<os>-<arch> rename and everything downstream are unchanged. Docs and the package comment note the hot-reload path and its restart fallback. * feat(mtproto): apply live secret updates via the management API and add ad-tag Two capabilities the mhsanaei/mtg-multi v1.13.3 fork exposes are now surfaced by the sidecar manager. Live updates go through PUT /secrets on the fork's management API instead of POST /reload: the panel already holds the whole desired set per inbound, so it sends secrets and the advertising tag as one JSON call that mtg applies atomically, keeping every unchanged connection and closing only removed or re-keyed ones. The config file is still written first so a restart or crash recovery reproduces the state, and any non-200 (an older binary, a refused connection) still falls back to a full restart. Per-inbound ad-tag adds an optional 32-hex Telegram advertising tag plus public-ipv4/public-ipv6 overrides. The ad-tag rides the reloadable secrets fingerprint, so changing it hot-applies without dropping connections; the public IPs are proxy-construction parameters and sit in the structural fingerprint, so a change there restarts the process. Empty public IPs are omitted so mtg auto-detects the reachable address. * feat(inbounds): expose the mtproto ad-tag and public IP in the inbound form Adds an Ad-tag field (validated as 32 hex characters) plus optional Public IPv4 and Public IPv6 overrides to the MTProto inbound form, backed by the same-named settings the sidecar writes into the mtg config. The public IPs are optional — left blank, mtg auto-detects the reachable address the ad-tag middle proxy needs. English strings are added to every locale; the non-English ones carry the English text until translated and fall back to it meanwhile. * ci(mtproto): install mtg-multi from prebuilt release binaries The fork now publishes release archives for every platform we package, so download and unpack the matching mtg-multi-<ver>-<os>-<arch> binary instead of compiling it from source with go install. Faster builds and no toolchain step, and the archive's platform labels line up with our matrix; the produced mtg-<os>-<arch> filenames are unchanged. * i18n(mtproto): localize the ad-tag and public IP strings The six mtgAdTag*/mtgPublicIp* keys shipped with English text in every locale as a placeholder. Translate them into the twelve non-English locales (Arabic, Spanish, Persian, Indonesian, Japanese, Portuguese-BR, Russian, Turkish, Ukrainian, Vietnamese, and Simplified/Traditional Chinese); en-US is unchanged. * retired goreportcard.com |
||
|
|
977fe4b4ea |
fix(ci): install mtg-multi without GOBIN for cross-compiled release builds
go install refuses to run with GOBIN set when GOOS/GOARCH differ from the host, which failed the linux release build for every non-amd64 platform (386, arm64, armv7, armv6). Let it install into GOPATH/bin instead, where cross-compiled binaries land in a GOOS_GOARCH subdirectory, and locate the binary there. DockerInit.sh keeps GOBIN because buildx runs it under emulation for the target platform, making the install native. |
||
|
|
d97bd8643e |
feat(mtproto): adopt dolonet/mtg-multi and make MTProto inbounds multi-client
Replace the upstream 9seconds/mtg sidecar with the dolonet/mtg-multi fork so a single MTProto inbound can serve many per-user secrets. Each panel client is now one named FakeTLS secret in the fork's [secrets] section: clients are first-class (attach/detach, limits, expiry, per-client tg:// links) exactly like every other protocol, mirroring the WireGuard multi-client model. Per-client traffic and online status come from the fork's /stats JSON API (its Prometheus output has no per-user label), fed into the existing email-keyed client_traffics accumulator; an optional throttle caps concurrent connections. A one-time seeder converts each legacy single-secret inbound into a one-client inbound. The fork ships only linux/darwin amd64/arm64 binaries but is pure Go, so provisioning builds it from source for every supported platform (release.yml, DockerInit.sh) while keeping the panel-expected mtg-<os>-<arch> filename and the 'run' verb, so process.go is untouched. Also fixes a pre-existing update.sh gap that never renamed the mtg binary for armv6/armv7 updates. |
||
|
|
5c725df702 |
fix(ci): pin the tag smoke test to the release under test
The v3.4.2 tag push triggered the smoke workflow immediately, but install.sh with no arguments resolves releases/latest, which still pointed at v3.4.1 while release.yml was uploading the new assets. The green smoke run therefore validated the previous release (#5756). A paths filter alone cannot exclude tag pushes because a brand-new tag ref has no diff base. Restrict the push trigger to branches so tag pushes no longer start the unpinned job, and add a workflow_run job that fires after the release workflow completes for a v* tag: it checks out the tagged commit, passes the tag through smoke-noninteractive.sh into install.sh's explicit-version path, and asserts the installed binary reports exactly that version. Closes #5756 |
||
|
|
427613b308 |
chore(ci): upgrade claude-bot to Sonnet 5 and set explicit effort levels
Sonnet 5 reaches near-Opus quality on coding/agentic work at lower cost; pin effort explicitly (xhigh/max) instead of relying on model defaults. |
||
|
|
e44075a6e0 |
chore(deps): bump xray-core to v26.6.27
Update the xray-core Go module (infra/conf builders + gRPC command clients) and the bundled binary pin in DockerInit.sh and the release workflow from v26.6.22 to v26.6.27. No gRPC command-API breaking changes. The release's other inbound work rides along with the bump: TUN autoSystemRoutingTable/autoOutboundsInterface are already modeled in the frontend tun schema, while Hysteria vlessRoute (UUID-derived) and the TUN traffic counters are internal to xray-core and need no panel changes. |
||
|
|
d1c0d77023 |
chore(ci): bump golangci-lint action to v9
Update the GitHub Actions CI workflow to use golangci/golangci-lint-action@v9 instead of v8. This keeps the lint job aligned with the latest major version and ongoing action maintenance. |
||
|
|
fa1a19c03c |
style: adopt golangci-lint v2 and resolve all findings
Add .golangci.yml (v2): the standard linters plus bodyclose, errorlint, noctx, misspell, rowserrcheck, sqlclosecheck, unconvert, usestdlibvars, with gofumpt + goimports formatters. Enable the std-error-handling exclusion preset for idiomatic Close/Remove/Setenv ignores; scope-exclude SA1019 (parser.ParseDir in tools/openapigen) and ST1005 (intentional capitalized user-facing error copy that tests assert verbatim). No inline nolint directives were introduced. Resolve all 217 findings behavior-preserving: gofumpt/goimports formatting, explicit blank assignment on intentionally ignored errors, errors.Is/errors.As and %w wrapping, context-aware stdlib calls (CommandContext/QueryContext/NewRequestWithContext/Dialer), staticcheck simplifications, removed redundant conversions, http.StatusOK and http.MethodGet, inlined the go:fix intPtr helper, and deferred sql rows Close. Add a golangci CI job mirroring the existing Go jobs. |
||
|
|
30796dc2ce |
chore(deploy): drop the AWS golden-image build stack
Remove the release-driven Packer AMI/qcow2 pipeline and everything that existed only to feed it: the image.yml workflow, deploy/packer, deploy/lightsail, deploy/firstboot, the AWS Marketplace checklist, and the first-boot smoke test/job. Keep the cloud-agnostic unattended-install path (cloud-init + install.sh non-interactive) and the Hetzner notes, which never depended on the workflow. Hetzner's snapshot path is dropped too since it relied on firstboot to avoid admin/admin on clones; cloud-init regenerates per-instance credentials on its own. Update deploy/README, the cloud-init and Hetzner docs, the root README plus its six translations, and .gitattributes to match. |
||
|
|
dc6d13b58f |
chore: bump deps and modernize test loops
- release.yml: download-artifact v7 -> v8 - frontend: i18next 26.3.1 -> 26.3.2, qs 6.15.2 -> 6.15.3 - go.mod: consolidate indirect requires (go mod tidy) - tests: adopt Go 1.22 range-over-int loops |
||
|
|
aad2b3eb1e |
feat(update): add rolling dev update channel for per-commit builds
Adds an opt-in Dev channel so panels running CI per-commit builds can self-update to the latest commit, mirroring the stable online-update flow. CI publishes/overwrites a single fixed-tag pre-release (dev-latest), force-moved to the newest main commit and marked --latest=false so releases/latest stays the stable tag. Builds stamp the short commit via -ldflags; the panel compares the running commit to the dev release commit to detect an update, and update.sh honors XUI_UPDATE_TAG to install from that tag. Linux/systemd only. |
||
|
|
a2961fd046 |
Update Xray to v26.6.22
Point CI workflow and DockerInit.sh to Xray v26.6.22 (update download URLs for Linux and Windows). Update go.mod to the matching github.com/xtls/xray-core pseudo-version and bump github.com/pion/stun to v3.1.6; refresh corresponding go.sum entries. |