Commit Graph
49 Commits
Author SHA1 Message Date
Lemon-miaow 9910c78709 fix(limbo): 登录门聊天和 /link 回复里的网址可以直接点击打开 2026-09-27 15:13:08 +08:00
Lemon-miaow 1e42e9a87b fix(velocity): 休眠服 MOTD 按唤醒策略说明谁能唤醒,闲置回收期间加入给出回收提示 2026-09-27 15:02:16 +08:00
Lemon-miaow 32534e3d56 fix(velocity): 同一玩家同时只跑一个授权和唤醒,排队中再点同一服直接告知 2026-09-27 14:42:28 +08:00
Lemon-miaow 67028727ae fix(velocity): 传送没连上且后端没给理由时重新排队重拨,最多三次 2026-09-27 14:32:30 +08:00
Lemon-miaow 2d04e0e637 fix(rotate-token): 加确认步骤、velocity 热加载免踢人,补齐 registry/forwarding/db 轮换 2026-09-27 10:50:11 +08:00
Lemon-miaow 9a238780e5 test(plugins): 登录闸门、大厅菜单和 mod /link 抽成可测类并补行为测试 2026-09-27 05:55:58 +08:00
Lemon-miaow 4ae64cc2e8 fix(menu): 大厅菜单按玩家标出每台服能否启动及原因,自己的服排前面,状态改成中文 2026-09-27 05:02:33 +08:00
Lemon-miaow 82310d02ec fix(velocity): 路由开启时让出 Velocity 自带的 /server,大厅的 /server 菜单才能打开 2026-09-27 04:36:53 +08:00
Lemon-miaow 7ce1ba7174 fix(velocity): 新增 /felis lobby,/felis go lobby 也能回大厅 2026-09-27 04:27:39 +08:00
Lemon-miaow 0e08fa7ced fix(servers): 主人可放弃服务器、管理员可删除服务器,由 reaper 归档世界后释放或移除 2026-09-27 03:49:49 +08:00
Lemon-miaow d0ab983f0f test(velocity): 测试桩按真实 wake 的顺序从服的状态推导回复,运行中的服对谁都 202 就绪,403 和 start_failed 不再由用例手配,停着的服一律报 fallback 2026-09-27 00:36:41 +08:00
Lemon-miaow 4425060d3e fix(lobby): 大厅变成安全的中转大厅并提示 /menu,大厅上限 200、登录门不限人数 2026-09-26 23:11:30 +08:00
Lemon-miaow 22d1e834ad fix(felis): 启动失败的服可在面板重试或停止,玩家入服时直接告知启动失败 2026-09-26 22:08:29 +08:00
Lemon-miaow c1025274e1 fix(velocity): 等待队列跟随服的启动进度,自动重试期间一直等并每分钟报进度,放弃或被停止时说明原因 2026-09-26 21:41:01 +08:00
Lemon-miaow df8b09788c fix(velocity): 目标服拒绝玩家时带原因提示并直接放行到大厅,不再在 login 里循环 2026-09-26 21:28:19 +08:00
Lemon-miaow 4afabc390d fix(velocity): 菜单加入和 /felis go 先查实时状态,运行中的服直接进入,内部 wake 对已运行服不再做启动权限校验 2026-09-26 21:21:59 +08:00
Lemon-miaow 3843082153 fix(velocity): 停服的 fallback 名不再注册成后端,唤醒就绪时按轮询到的地址立即注册再传送 2026-09-26 21:18:08 +08:00
Lemon-miaow 5099b2501f feat(velocity): legacy forwarding 列表跟随 CR 的 forwarding=legacy 标签实时更新 (#15) 2026-09-26 08:09:49 +08:00
Lemon-miaow c15eec6c2d test(velocity): 路由核心类接真 velocity-api 自测,修正子域名改名后旧域名仍路由 2026-09-26 00:42:05 +08:00
Lemon-miaow b65c2c00b3 feat(plugins): 插件侧计数器与周期健康日志,Limbo/刷新失败按故障周期升级日志 2026-09-26 00:33:04 +08:00
Lemon-miaow fcf5c305ea feat(velocity): felis-api 调用改走有界线程池、定时任务防重叠,超时可配置,幂等 GET 带抖动重试一次 2026-09-26 00:03:19 +08:00
Lemon-miaow 001f060027 docs(plugins): README 同步版本表与 MC 支持矩阵,防伪造描述限定到 Velocity 路径 2026-09-25 23:45:33 +08:00
Lemon-miaow d9453a6488 build(plugins): 插件构建统一钉到 gradle 9.8 镜像与带 sha256 的 wrapper,paper-api/Limbo 对齐锁文件并启用依赖校验 2026-09-25 23:45:33 +08:00
Lemon-miaow 2d1592bf01 fix(mods): 加载器 mod 仅在正版验证的独立服务器上签发绑定码,README 标明只用 limbo token 及其风险 2026-09-25 23:06:35 +08:00
Lemon-miaow f1414b5cc8 docs(link): 写明内部面明文 HTTP 的单节点前提与放行范围,修正配置模板里的内部面地址 2026-09-25 23:03:02 +08:00
Lemon-miaow 0fb43232b5 fix(velocity): 保存最近一次服务器列表,控制面不可用时重启代理仍能路由登录 2026-09-25 22:58:53 +08:00
Lemon-miaow d93c1b6913 feat(api): op-login 游戏内审批先展示目标账号、邮箱与发起来源,须输入账户名确认,velocity 显示审批卡片 2026-09-25 17:02:43 +08:00
Lemon-miaow a883c1fe07 feat(api): 内部面 token 按调用方拆分为 velocity/limbo/build/ops 并按路由限定调用方,审计来源区分调用方,安装器生成并下发各自 Secret,新增 felis rotate-token 轮换命令 2026-09-25 15:21:41 +08:00
Lemon-miaow aace66e8d3 fix(bootstrap): 重跑只重启有变化的 Velocity、系统服与 PostgreSQL,无 firewalld 时用 nftables 收口 5432,PG 大版本不符拒绝启动 2026-09-25 00:06:47 +08:00
Lemon-miaow abfe60d62d fix(api): wake 与回档/备份/改文件按服务器互斥 2026-09-24 14:39:02 +08:00
Lemon-miaow 8f684cedc0 feat(paper): 大厅菜单从代理动态拉取服务器列表并分页 2026-09-24 13:39:38 +08:00
Lemon-miaow 955433ba81 fix(limbo): 经 felis:control 放行玩家并在 felis-api 故障时退避重试 2026-09-24 13:39:38 +08:00
Lemon-miaow 4648175773 fix(velocity): felis:control 按来源服务器限权并关闭 bungeecord 通道 2026-09-24 13:39:38 +08:00
Lemon-miaow aa5abfa911 ci(plugins): gate the three loader mods (JDK 17 wrapper builds) in CI and release
Nothing ever compiled these modules — no CI job, no install path — which is
why the gradlew exec-bit bug (previous commit) shipped unnoticed.  Add
plugins/test-mods.sh: runs each module's vendored wrapper under JDK 17
(they target the Java-17 Minecraft lines; paper/limbo stay on the JDK 21
gate).  ci.yml gains a `mods` job (temurin 17 + setup-gradle, wrapper
pinned per module), release.yml gates both plugin gates before shipping.
README: status now records the server-boot verification, the Building
section pins limbo's `-PlimboVersion=<release>` (the `+` default is
unresolvable from the LOOHP repo) and documents both gates.
2026-09-24 00:59:24 +08:00
Lemon-miaow c2fe6a6bf4 fix(plugins): loader mod metadata — AGPL-3.0-only license, real issue tracker
The three loader mods declared `license = "MIT"` (and Forge/NeoForge a
placeholder `issueTrackerURL = https://example.invalid/felis`) while the
repository is AGPL-3.0-only (README.md:73, LICENSE).  The mods were added
2026-06-26, the LICENSE landed 2026-07-12 — stale leftovers that nothing
ever read back: no CI job, no install path.  Fabric loader prints the
license from fabric.mod.json at boot and both mods.toml files are parsed
by their loaders, so the wrong claim is user-visible.  Align both with
reality — boot-verified on real fabric/forge/neoforge dedicated servers
(AUDIT-2026-09-22.md, batch 41).
2026-09-24 00:59:19 +08:00
Lemon-miaow f6048f268b fix(plugins): commit the gradlew exec bit for the loader mods
All three vendored wrappers were tracked as 100644, so the README's documented
'plugins/<loader>/gradlew -p plugins/<loader> build' failed on every fresh
clone with 'Permission denied' (exit 126) — and since no install path or CI job
ever ran them, nothing caught it.

git update-index --chmod=+x for the three files; the VM then built all three
modules for the first time (results in the gate commit and the audit).
2026-09-24 00:47:32 +08:00
Lemon-miaow c59b38775b ci(plugins): run the plugin self-tests and build the shipped plugin jars
The three framework-free test mains under plugins/*/test were never run by
anything — not CI, not the plugin builds — and the velocity/paper/limbo jars
were only ever compiled by deploy/bootstrap.sh on a live host. CI gains a
'plugins' job (JDK 21 plus the Gradle 8.14 the plugin Dockerfiles pin) running
plugins/test.sh: the three mains (InviteCardTest's jars fetched from Maven
Central, pinned and digest-checked) and the three production builds.

The first real run surfaced and fixed two untested assumptions: InviteCardTest's
documented javac line omitted examination-api (adventure-api's Component
signatures reference Examinable, so javac needs it too), and limbo's '+' version
default cannot resolve — LOOHP's repository serves no maven-metadata — so the
script resolves the current release off the Limbo CI artifact name (the same
source bootstrap reads) and plugins/README.md stops advertising a bare
'gradle -p plugins/limbo build' that can never work.

Verified in gradle:8.14-jdk21 on the VM: mains OK (32/36/48 checks);
velocity/paper/limbo BUILD SUCCESSFUL.
2026-09-24 00:19:56 +08:00
flyemoji 2180e77cf5 chore: drop tool-name markers from source comments
Seventeen comments opened with a tag naming the tool that wrote them.
The tag goes and each comment keeps its reasoning, now starting as a
plain sentence. None of the reasoning changes.

The AGENTS.md note in .gitignore drops the story of how the file got
into the tree and keeps the one fact a reader needs: its advice to run
go fmt is destructive on this CRLF working tree.

Comments only; no code, build or test changes.
2026-09-22 12:57:19 +09:00
flyemoji 82a1275fcf docs(plugins): say which plugin jars an install actually produces
The module table listed felis-fabric, felis-forge and felis-neoforge next to the
two jars a finished install really has, with nothing distinguishing them. Neither
deploy/bootstrap.sh nor the embed set in bootstrap_asset.go builds a loader mod,
so someone reading the table expected three jars that are not there after setup
and had no way to tell from this file. The mods do build -- the wrapper commands
under Building work -- they are just never installed for you, which is what the
new column says.

Two further disagreements with the code, in the same table:

  limbo/ was missing entirely. It is embedded, built by bootstrap.sh and running
  on the login gate, so the one module the table omitted was a shipped one. It is
  also the only module that reaches the account-link endpoint without a command:
  it mints the code on join for anyone unlinked and holds them until they redeem
  it, so the opening claim that every module except the lobby ships /link named
  the wrong exception.

  velocity was listed as felis-velocity-0.2.0.jar; plugins/velocity/build.gradle:6
  says 0.1.0, as does every other module. Nothing breaks on this because
  bootstrap.sh globs felis-velocity-*.jar and installs it under a fixed name, but
  the version in the table was not a version anything produces.

The Gradle table and the JDK note now carry limbo's Java-21 toolchain, which it
needs for the same reason paper does and for a different cause: LOOHP/Limbo
releases are class-file major 65, so the compiler JDK must be able to read them.
It still emits release 17 bytecode.
2026-07-28 14:33:37 +09:00
flyemoji c9f3aa68f6 fix(velocity): stop the lobby tile failing for players already in the lobby
Asking for the server you are standing on is a no-op, but it went through the
whole wake-and-queue path and ended in a Connect to the current server, which
Velocity answers ALREADY_CONNECTED. The player saw a failure for a move that
was never real.

The guard belongs in authorizeAndWait rather than at each entry point: the
menu tile, /felis go and an accepted invite all funnel through it. The lobby
is where it shows up most, since the tile for the lobby itself sits in front
of every player already standing in it.
2026-07-23 00:00:59 +09:00
flyemoji 60b0ec97ac feat(invite): let a player bring a friend to the server they're on
/invite <player> posts a chat card to the invitee with a green [Accept] and a
red [Deny] button, and Accept walks them to the server the inviter is standing
on. It is a UX wrapper over `/felis go` and nothing more: the accept runs the
same doGo path on the ACCEPTING player's own verified uuid, so a stored invite
carries a server name and never an identity to act as, and the prompt needs no
unguessable token.

Why it can be this simple: an invite can only name the server its sender is
currently on, so the target is running by construction, and a running felis
server already admits any linked player through <name>.<root-domain> on the
link check alone (WaitingRouter.onServerPreConnect) — no wake, no
autostartPolicy consultation. Hence enqueueFromInvite: a READY backend is
joined directly, and only the not-ready case falls through to the policy-gated
wake path unchanged. Routing an accept through wakeAndWaitLinked would have
asked the API to wake a server that needs no waking, and autostartPolicy
defaults to ownerOnly, so the API would answer 403 and the green button would
do nothing for exactly the people you would invite.

It is not consequence-free, and the inviter is told so at send time rather
than in a comment only we read. Landing on a felis server records the player
in its allowlist (onServerConnected -> join-event -> RecordJoin); on an
autostartPolicy=allowlist server that row is what lets them come back and
START the thing later. The same row they would earn by walking in unaided —
the invite shortened the walk, it did not widen the door — but it outlives the
invite, so accessNotice says so. The wording follows the policy: only under
allowlist does it claim they will be able to start the server themselves,
because under the ownerOnly default (and the empty string the API reports for
an unset field) that row grants no waking and the claim would be a lie.

InviteBook also holds a 30s per-sender cooldown, because the one capability
/invite genuinely adds is "make a chat card appear on any online player", and
unrated that is a way to follow someone around their own chat log. It is
charged in put() rather than at the top of the command, so an invite refused
for an offline name or a player already on the server costs the sender
nothing, and the gate sits after every other validation for the same reason.
The stamp is global per sender on purpose: a per-(sender, invitee) key would
wave through one player papering the whole proxy, which is the thing being
limited.

The card lives in InviteCard as a pure function so the buttons — the whole
point of the feature — can be asserted without a live proxy, and the button
clicks are pinned to the server the card named, so a stale card cannot answer
a newer invite (checked with peek before the invite is spent, so refusing a
superseded card leaves the live one answerable).

Verified: production gradle 8.14 + JDK 21 build; jar carries the plugin classes
and no test classes; InviteBookTest (36 checks) and InviteCardTest (48 checks);
and a live Velocity 3.5.1 that loads the jar, registers /invite <player> and
answers /invite accept <server>, with a malformed subcommand as a negative
control.
2026-07-22 14:32:51 +09:00
flyemoji 7860152f57 feat(auth)!: go fully passwordless and fix cross-check review findings
Remove password authentication everywhere; the only session doors are
passkey (WebAuthn), email OTP, in-game bind codes, QR scan-login, and
op-login vouching. Remediates the 33-finding cross-check review across
backend, CLI, panel, plugins, and docs.

Backend/CLI:
- Drop password routes and fields from account/user/onboard/auth
  handlers; align tests (new account subtests, naming reserves
  "console", op-login/onboard/qr-login test updates).
- Add migrations 0016_op_login.sql and 0017_drop_password.sql.
- Thread panel/admin hostnames from hostcfg through api.go,
  setup_panel.go, tui_root.go and tui_preflight.go instead of
  hardcoding; bootstrap.sh writes panel-hostname/admin-hostname
  into felis.toml.
- Reword breakglass and TUI copy for passwordless flows.

Panel:
- Delete the ChangePassword page and all password UI; align
  login/auth/api/types with the passwordless contract; add the
  migration and op-login approval flows.
- i18n: convert ImageBuildPage durations/status badges and
  ServerLuckPerms strings to translation keys; drop 72 orphan keys
  per locale; unify the title as "Felis - Console".

Plugins (all six rebuilt):
- Velocity waiting router returns 503 at_capacity during wake;
  MOTD/control-channel copy and config comments.
- Paper zh menu title; Limbo bind-code TTL 600s with panel_url
  preference; unified /link lines in fabric/forge/neoforge; shared
  link-client javadoc contract fixes.

Docs: openapi.yaml, sequence-diagrams.md, deploy/limbo/README.md and
plugins/README.md aligned with the implementation.

BREAKING CHANGE: migration 0017 irreversibly drops
users.password_hash and users.must_change_password; password login
cannot be restored after migrating.
2026-07-20 04:47:32 +09:00
flyemoji 9ea35304e3 fix(setup): source the console host from the panel hostname, not op.console
The owner setup URL and the limbo login link were built from the admin host
(op.console.<root>, with an op.console.localhost fallback) and a hardcoded
console.<root>, so an operator who set a custom panel_hostname got an unreachable setup
link and a wrong login target. Thread the resolved panel host (defaultPanelHostname)
through performSetupMCBind, the MC-bind TUI, and the login system-server env
(new FELIS_PANEL_HOSTNAME); the limbo plugin prefers it and keeps console.<root> only as
the fallback for an older operator whose env predates it. This also matters for security:
the only wired WebAuthn verifier is scoped to the panel host, so passkey enrollment must
land on the panel face, never op.console.

While here, the limbo login handler checks link status before minting a bind code: an
already-linked player is sent straight to the lobby instead of being shown a useless code.
2026-07-16 13:27:02 +09:00
flyemoji 688c86da18 feat(proxy): enforce login-first routing 2026-07-14 02:55:11 +09:00
flyemoji 16b7ad2756 feat(runtime): add authenticated system backends 2026-07-14 02:54:13 +09:00
flyemoji c1aa38bac1 feat(velocity): add /felis migrate to open an account migration (§B3 inherit)
Add the in-game /felis migrate command that a player runs to open an
account migration, the entry point for handing their owned servers to
another account (§B3 inherit, scenario A). The command posts the player's
Mojang-verified UUID to the existing handleMigrateStart backend, which puts
the account into migrate mode; the player then finishes on the web console
(prove identity, name the receiving account, redeem a one-time code).

Mirrors the existing /felis claim path: requires a real player past login
limbo, acts on the caller's account rather than the current server, expects
201 Created affirming started=true (a 201 without it is a contract breach,
not a refusal), and maps the handler refusals (404 not_linked, 409
account_retired) to player-facing guidance. On success it points the player
at https://console.<root_domain>, derived from config, never hardcoded.
Compile-verified against velocity-api:3.3.0-SNAPSHOT via the podman gradle
toolchain. Closes the code-only gap named in handlers_account_migrate.go.
2026-07-06 23:50:57 +09:00
flyemoji 0c1cc598c1 feat(auth): migrate console login to passwordless
Replace console password auth with a passwordless surface — the pre-session
login doors plus an identifier-first discovery endpoint — and remove the
password paths.

- Login doors (Public, pre-session): email-OTP, passkey assertion, op.console
  login with in-game approval, and setup-token redeem.
- /api/v1/auth/options: identifier-first discovery reporting which console
  methods an email can use. The single sanctioned existence oracle; methods
  are computed with no role branch, so staff and player accounts in the same
  credential state return byte-identical bodies (staffness invisible by
  construction).
- Remove password auth: drop StaffUser.PasswordHash and the /auth/login,
  /auth/change-password and /users/{id}/reset-password endpoints (and test).
- Data layer: UserByEmail, verified-email uniqueness, setup-token store
  (migration 0012).
- Reconcile docs/openapi.yaml with the served surface; the method/path/face/
  tier parity gate (TestOpenAPIMatchesServedRoutes) passes.
- felis TUI: in-game MC bind, owner/break-glass OP provisioning, version.
- Velocity /felis command suite.

Consolidates the accumulated backend migration work; the frontend (panel/)
is left untouched. Full Go tree green on WSL (go build ./... && go test ./...).
2026-07-04 21:47:12 +09:00
flyemoji 241fe21f8a feat(limbo): felis-limbo in-game login flow over the shared account-link client
The login limbo now performs the onboarding inside Limbo: on join it checks the collision blacklist, mints a bind code, opens a book linking the player to console.<root_domain> (guiding them to the system browser), polls link-status, and transfers to the lobby via BungeeCord Connect — fail-closed on blacklist, mint/transport error, or window elapse. FelisApiClient gains linkStatus/isBlacklisted on the existing internal transport.
2026-07-02 19:38:38 +09:00
flyemoji 93f143f5b6 feat(plugins): add Velocity proxy and Fabric/Forge/NeoForge/Paper integration mods
Server-side integration plugins: the Velocity proxy plugin plus Fabric, Forge, NeoForge, and Paper mods with a shared module. Gradle build output is not tracked.
2026-06-26 23:32:40 +09:00