docs(changes): backfill detail docs for pre-ledger functional commits
Retroactively author 15 grouped detail docs covering the backend functional (feat/fix) commits made before the change ledger was established (fad48ff), closing the ledger's detail-doc axis for the pre-convention history. Each doc groups a feature's constituent commits, lists their SHAs with subjects, and carries a backfill note stating it was reconstructed from git history on 2026-07-07 and not independently re-verified (current tree green at9911b8c). Add a Detail docs section to INDEX.md linking every detail doc (the 6 existing + 15 backfill) to the commit(s) it covers, so a doc is findable from the index without a column on the auto-generated ledger table. Catch the table up with the missing9911b8crow. Scope: backend (Go/Java/K8s) only, per the ledger's stated convention that frontend/panel commits are the collaborator's UI work; non-functional commits (docs/style/chore/refactor) keep their table row without a dedicated detail doc.
This commit is contained in:
16 files changed
+557
No files matched your search
@@ -0,0 +1,41 @@
|
||||
# Foundational subsystems: the initial Felis import (ledger backfill)
|
||||
|
||||
- **Type:** feature (initial import) — retroactive ledger entry
|
||||
- **Date:** 2026-06-26
|
||||
- **Area:** `apis/`, `internal/` (naming, rcon, store, config, build, backup, operator,
|
||||
submit, api, platform), `cmd/felis`, `plugins/`
|
||||
- **Commits:**
|
||||
- `7fbebfe` feat(apis): MinecraftServer CRD types (v1alpha1) — the lifecycle source of truth (§1)
|
||||
- `708cdfc` feat(core): naming, RCON, store (Postgres + embedded migrations), config, image-build libraries
|
||||
- `43ab921` feat(backup): archive-based world backup/restore + the retention/idle reaper
|
||||
- `78b8cf6` feat(operator): MinecraftServer controller and reconcilers
|
||||
- `d39605e` feat(submit): user modpack build + admin-approval pipeline (see [modpack-submission-lane](2026-06-26-modpack-submission-lane.md))
|
||||
- `b508fcc` feat(api): dual-faced felis-api — permissions/LuckPerms, modpack lane, admin fleet read
|
||||
- `47fcd90` feat(platform): node orchestration + the `cmd/felis` single-binary entrypoint
|
||||
- `93f143f` feat(plugins): Velocity proxy + Fabric/Forge/NeoForge/Paper integration mods
|
||||
- **Tasks:** #23 (permissions), #24 (modpack lane), #25 (fleet read)
|
||||
|
||||
## What it did
|
||||
|
||||
Stood up the whole backend spine in one build-order sweep: the Kubernetes CRD that is
|
||||
the lifecycle source of truth, the core libraries (deterministic resource naming, the
|
||||
RCON client, the Postgres store with embedded SQL migrations, config loading, container
|
||||
image-build helpers), the backup/restore/reaper subsystems, the operator controller
|
||||
that drives `MinecraftServer` resources, the user-modpack submit+approval pipeline, the
|
||||
dual-faced (internal/external) felis-api behind a Zero-Trust guard, the platform
|
||||
orchestrator that wires it all together under `cmd/felis`, and the server-side
|
||||
integration plugins.
|
||||
|
||||
## Why
|
||||
|
||||
This is the project's first functional import — the substrate every later change edits.
|
||||
It predates the change-ledger convention (established `fad48ff`, 2026-07-06), so it never
|
||||
got a contemporaneous detail doc; this entry backfills one.
|
||||
|
||||
> **Backfill note.** Reconstructed 2026-07-07 from the commit history to close the
|
||||
> change-ledger's detail-doc axis (§ Convention). This entry deliberately describes only
|
||||
> what these eight commits **introduced** on 2026-06-26 — the named subsystems have been
|
||||
> extended and reworked many times since (auth, passkey, metrics, quotas, updates), and
|
||||
> that later work lives in its own dated detail docs, not here. Not independently
|
||||
> re-verified for this doc; each subsystem was verified at its original commit and the
|
||||
> current tree builds green at `9911b8c` (WSL oracle, go1.26.4).
|
||||
@@ -0,0 +1,30 @@
|
||||
# Modpack submission lane: build/approval pipeline + storage backends (ledger backfill)
|
||||
|
||||
- **Type:** feature — retroactive ledger entry
|
||||
- **Date:** 2026-06-26 – 2026-07-02
|
||||
- **Area:** `internal/submit` (build/approval pipeline, storage backends), `internal/api` (submission endpoints)
|
||||
- **Commits:**
|
||||
- `d39605e` feat(submit): user modpack build + approval pipeline — an uploaded modpack stays `pending_review` and is never built until an admin approves; approval is a single-winner compare-and-swap handing off to the image-build Job, keeping the mandatory vulnerability scan in front of any push
|
||||
- `598f3d3` feat(submit): local + S3 backends for modpack upload contexts, installer-selectable
|
||||
- **Tasks:** #24 (§8 user-submitted modpack approval lane)
|
||||
|
||||
## What it did
|
||||
|
||||
Built the user-directed extension over the image-build subsystem: a player uploads a
|
||||
modpack context, it sits in `pending_review`, and an admin's approval is the single-winner
|
||||
gate that hands off to the build Job — with the vulnerability scan always ahead of any
|
||||
registry push. `598f3d3` makes the upload-context store pluggable (local filesystem or S3),
|
||||
selectable at install time.
|
||||
|
||||
## Why
|
||||
|
||||
Untrusted user content must never build or push unreviewed, and the compare-and-swap
|
||||
approval guarantees exactly one build per submission even under a double-click or retry.
|
||||
The storage-backend choice lets a single-node demo use local disk while a real deployment
|
||||
uses S3, without a code change.
|
||||
|
||||
> **Backfill note.** Reconstructed 2026-07-07 from the commit history. The approval
|
||||
> compare-and-swap and endpoints were unit-tested at their commits; the S3 path is
|
||||
> integration-configurable. The panel-side submission/approval UI is the collaborator's
|
||||
> frontend work and is tracked only by its INDEX rows. Not independently re-verified for
|
||||
> this doc; current tree green at `9911b8c`.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Cloudflare Tunnel + Access edge (cfsetup) + NodePort fencing (ledger backfill)
|
||||
|
||||
- **Type:** feature + fix — retroactive ledger entry
|
||||
- **Date:** 2026-06-27 – 2026-07-01
|
||||
- **Area:** `internal/cfsetup` (pure core + integration runner), `cmd/felis` (TUI edge flow), edge nftables fence
|
||||
- **Commits:**
|
||||
- `53a7664` feat(cfsetup): recommended Cloudflare Tunnel + Access edge (§14) — domain- and IdP-agnostic; the load-bearing `validateFailClosed` allowlist refuses any policy that could be public; fail-shut 404 catch-all; the raw game host is never proxied
|
||||
- `ba13839` feat(breakglass): optional Tunnel + Access setup in the sudo TUI, an independent peer of Owner provisioning
|
||||
- `a531f5e` fix(cfsetup): keep the connector install in the host apply layer only (drop the duplicate `StartConnector`)
|
||||
- `2810fe8` fix(cfsetup): repoint a stale DNS record when routing a tunnel hostname
|
||||
- `7d3be64` feat(cfsetup): start the tunnel connector as a setup step
|
||||
- `e058a64` feat(edge): close the panel NodePort to the public after the tunnel is up — nftables at prerouting `raw` (-300), before kube-proxy's NodePort DNAT, gated on the connector actually serving; loopback accepted first so the connector origin hop is untouched
|
||||
- **Tasks:** #37 (fence panel NodePort to public after tunnel)
|
||||
|
||||
## What it did
|
||||
|
||||
Stood up the optional one-click Zero-Trust edge: a Cloudflare Tunnel routing only the web
|
||||
hostnames plus a fail-closed Access application, provisioned from the sudo TUI against the
|
||||
operator's own Cloudflare account. `e058a64` then closes the Access-bypass hole where a
|
||||
direct `https://<node-ip>:<nodeport>/` with the right Host header reached the origin
|
||||
behind Access, by fencing the NodePort at the nftables raw hook so the packet is caught on
|
||||
its original destination port — but only once the connector is confirmed serving, so
|
||||
fencing never severs the only web path to a live origin.
|
||||
|
||||
## Why
|
||||
|
||||
Access is only a security boundary if the origin cannot be reached around it. The
|
||||
fail-closed policy guard (`validateFailClosed`) and the NodePort fence are the two
|
||||
load-bearing safety properties: a policy that could be public aborts the run with nothing
|
||||
created, and a routable-but-unfenced NodePort would defeat the whole edge.
|
||||
|
||||
> **Backfill note.** Reconstructed 2026-07-07 from the commit history. The policy guard,
|
||||
> ingress generation, request bodies, gating, and the nftables ruleset shape / conn-count
|
||||
> gate are unit-tested; the live cloudflared/Cloudflare-API and `nft` calls are
|
||||
> INTEGRATION-ONLY (need a real account). KNOWN-LIMITATION: the fence targets nftables;
|
||||
> firewalld-native coordination is deferred. Not independently re-verified for this doc;
|
||||
> current tree green at `9911b8c`.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Console auth: local-password login → passwordless migration (ledger backfill)
|
||||
|
||||
- **Type:** feature + refactor — retroactive ledger entry
|
||||
- **Date:** 2026-06-27 – 2026-07-04
|
||||
- **Area:** `internal/api` (auth handlers, sessions), `internal/store` (users schema)
|
||||
- **Commits:**
|
||||
- `af14f02` feat(api): local-password authentication backend — login/logout/change-password on `op.console`; HttpOnly+Secure+SameSite=Lax host-only server-side sessions (SHA-256, 12h TTL); anti-enumeration uniform bcrypt; JSON-only credential writes (415 otherwise); fails closed unless `local_auth_enabled`
|
||||
- `0c1cc59` feat(auth): migrate console login to passwordless
|
||||
- `3b43f05` refactor(api): drop the dead login concurrency limiter and reconcile passwordless comments
|
||||
- `c20b12c` refactor(api): drop the dead password-era `ResetMailer`, reconcile passkey-unbind docs
|
||||
- **Tasks:** #27 (B1 thin thread), #79/#80/#81 (residue sweep + primitive adjudication)
|
||||
|
||||
## What it did
|
||||
|
||||
Shipped the staff local-password door (`af14f02`) as the primary web login when
|
||||
Zero Trust is not in front of the API, then migrated the console to passwordless
|
||||
(`0c1cc59`) once email-OTP + passkey were the intended factors. The two refactors
|
||||
(`3b43f05`, `c20b12c`) then swept the password-era residue — the now-dead login
|
||||
concurrency limiter and the `ResetMailer` — so no unused password machinery lingered in
|
||||
the compile path, and reconciled the stale comments that referenced it.
|
||||
|
||||
## Why
|
||||
|
||||
`op.console` needs a real login even in deployments without a Cloudflare-Access edge; the
|
||||
password backend was that. Once the passwordless factors landed, keeping the old password
|
||||
scaffolding around was a bug farm — the sweep is the closeout evidence that the migration
|
||||
was complete, not half-done.
|
||||
|
||||
> **Backfill note.** Reconstructed 2026-07-07 from the commit history. `af14f02` was
|
||||
> covered by Go unit tests (content-type guard, anti-enumeration, forced-change lockdown)
|
||||
> at its commit. Not independently re-verified for this doc; current tree green at
|
||||
> `9911b8c` (WSL oracle, go1.26.4).
|
||||
@@ -0,0 +1,38 @@
|
||||
# Deploy: one-line bootstrap installer + demo bring-up (ledger backfill)
|
||||
|
||||
- **Type:** feature + fix — retroactive ledger entry
|
||||
- **Date:** 2026-06-27 – 2026-07-03
|
||||
- **Area:** `deploy/` (bootstrap.sh, Dockerfiles, demo-up.sh), image build context
|
||||
- **Commits:**
|
||||
- `58fa4b0` feat(deploy): one-line bootstrap installer + distroless felis image (auto-detects apt/dnf, installs Docker/k3s/PostgreSQL, opens pg_hba to the pod CIDR, runs migrations, applies the control-plane bundle, leaves Web disabled pending `felis setup`)
|
||||
- `94a3b7b` fix(deploy): harden bootstrap for RHEL-family Linux
|
||||
- `deaa2f8` feat(deploy): zypper support (openSUSE/SLES)
|
||||
- `318a724` feat(deploy): pacman support (Arch)
|
||||
- `e5f1682` refactor(deploy)!: TUI (breaking walkthrough restructure)
|
||||
- `28c3eee` refactor(deploy): improved TUI walkthrough
|
||||
- `c14ed17` fix(docker): keep embedded `panel/` and `deploy/` in the image build context
|
||||
- `d9e866f` fix(deploy): make the lobby image actually build (re-include `plugins/paper`, build on `gradle:8.14-jdk21`)
|
||||
- `b84debf` feat(deploy): one-shot `demo-up.sh` — bootstrap → build/import limbo+lobby images → wire `[velocity]` image refs → `felis setup`, ending in the interactive Owner TUI
|
||||
- **Tasks:** #26 (Phase A bootstrap verified end-to-end on the Demo VM)
|
||||
|
||||
## What it did
|
||||
|
||||
Made a bare Linux box a running Felis with one command. `bootstrap.sh` auto-detects the
|
||||
host package manager across the four major families (apt/dnf/zypper/pacman), installs
|
||||
whatever is missing (Docker, k3s, PostgreSQL, cloudflared), builds+imports the distroless
|
||||
felis image, opens `pg_hba` to the pod CIDR, runs migrations, and applies the rendered
|
||||
control-plane bundle. `demo-up.sh` wraps that plus the login-limbo/lobby image build and
|
||||
`felis setup` into a single command, stopping only at the Owner-creation TUI it cannot
|
||||
automate.
|
||||
|
||||
## Why
|
||||
|
||||
The spec calls for a self-hostable single-node deployment a SysAdmin can stand up without
|
||||
a Kubernetes background. The package-manager fan-out and the demo wrapper are what make
|
||||
"one line" true across real distros rather than only on the author's box.
|
||||
|
||||
> **Backfill note.** Reconstructed 2026-07-07 from the commit history to close the
|
||||
> change-ledger's detail-doc axis. `deploy/` is shell + Dockerfiles (not Go-oracle
|
||||
> verifiable); `d9e866f` records a real build+boot check (limbo `/healthz` 200, lobby
|
||||
> reaches "Done"). Not independently re-verified for this doc; current tree green at
|
||||
> `9911b8c`.
|
||||
@@ -0,0 +1,35 @@
|
||||
# felis CLI: break-glass recovery console + first-run setup (ledger backfill)
|
||||
|
||||
- **Type:** feature + fix — retroactive ledger entry
|
||||
- **Date:** 2026-06-27 – 2026-06-30
|
||||
- **Area:** `cmd/felis` (break-glass/setup TUI, apply, migrate), `internal/api` (audit, owner store), `deploy/`
|
||||
- **Commits:**
|
||||
- `e108a37` feat(cli): break-glass emergency console TUI — root-only (`euid==0`), provisions/resets the Owner directly against Postgres, enables local login, prints a durable one-time-password summary
|
||||
- `2d0bbb0` feat(cli): attribute break-glass recovery to the SysAdmin who runs it — bootstrap / recovery (bcrypt) / root-override, each audited with an honest `verified` flag and payload
|
||||
- `a94b001` feat(deploy): break-glass Operator account provisioning
|
||||
- `eb5875a` feat(felis): Operator break-glass op behind an operation menu
|
||||
- `f5d00f3` feat(cli): `felis apply` for direct CRD creation
|
||||
- `9c46632` feat(cli): `felis setup` first-run console (shared `runConsoleTUI` model, reclaim protection, cfsetup idempotency, `[auth].admin_hostname` respect)
|
||||
- `7d91373` fix(migrate): honor `-config` placed after the `up` verb (flag.Parse stops at the first non-flag token)
|
||||
- **Tasks:** #27 (B1 login→change-pw→TUI reset)
|
||||
|
||||
## What it did
|
||||
|
||||
Built the local-root recovery and first-run surface that bypasses web Zero Trust by
|
||||
design. `felis breakGlass` mints or resets the Owner when the web login is unreachable;
|
||||
`2d0bbb0` makes it accountable by recording *which* SysAdmin broke the glass across three
|
||||
audited modes. `felis setup` is the non-emergency first-run twin sharing the same console
|
||||
model. `felis apply` writes a `MinecraftServer` CRD directly, and `7d91373` fixes the
|
||||
`migrate` flag parse so a configured DB path after `up` is honored.
|
||||
|
||||
## Why
|
||||
|
||||
An operator with root on the node and a kubeconfig must always be able to recover the
|
||||
platform — that is break-glass's whole job, so it never refuses. Attribution
|
||||
(`2d0bbb0`) closes the gap that root is machine authority, not a human identity: the root
|
||||
gate is necessary but not sufficient for the audit trail.
|
||||
|
||||
> **Backfill note.** Reconstructed 2026-07-07 from the commit history. The core logic was
|
||||
> covered by Go unit tests over a fake owner store at each commit (auth match/non-match,
|
||||
> the three audit modes, headless TUI drive). The bubbletea TUI glue is untested by house
|
||||
> convention. Not independently re-verified for this doc; current tree green at `9911b8c`.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Player onboarding data layer §B2: email-OTP, account-link, QR, Bind-Code (ledger backfill)
|
||||
|
||||
- **Type:** feature + fix — retroactive ledger entry
|
||||
- **Date:** 2026-06-27 – 2026-07-03
|
||||
- **Area:** `internal/api` (onboarding/auth-bind handlers), `internal/store` (migrations 0004–0006)
|
||||
- **Commits:**
|
||||
- `dbe34a1` feat(api): player email-OTP verification (§B2) — `POST /account/email/{start,verify}`; 6-digit code, SHA-256-at-rest, 10-min TTL, 5-attempt cap enforced in the repo
|
||||
- `1f8b9bb` feat(api): record account-link auth source (`mojang|thirdparty`) for the dual-Yggdrasil split (§10)
|
||||
- `116595f` feat(api): QR scan-login completion poll on the internal face (`GET /internal/account/link/status/{mc_uuid}`) — read-only, reuses `UserByMCUUID`, no migration
|
||||
- `fe2ece0` feat(api): public Bind-Code onboarding (`POST /auth/bind`) — the one pre-account entrypoint of `console.<root_domain>`; refuses a staff-UUID code with 403 without consuming it, so the public door provably never yields an admin principal
|
||||
- `55592ed` feat(auth): public auth-bind endpoint wiring
|
||||
- `6c3999a` fix(api): rate-limit email-OTP sends to close the email-bomb vector
|
||||
- `879b177` fix(api): make OTP-start throttle atomic to close the concurrent-burst bypass
|
||||
- **Tasks:** #29 (B2 data layer), #32 (OTP rate-limit), #35 (atomic throttle), #39 (console access model)
|
||||
|
||||
## What it did
|
||||
|
||||
Built the Go-verifiable data layer of forced web onboarding: prove control of an email
|
||||
(OTP), record which Yggdrasil authenticated an in-game UUID, let a phone already signed in
|
||||
to the panel complete a QR device-code link, and let an account-less player redeem a
|
||||
one-time Bind Code minted in the Login Lobby to create+link+session in one public step.
|
||||
The two fixes bound the OTP abuse surface — a per-target send rate limit and an atomic
|
||||
reserve that closes the check-then-act race on the attempt counter.
|
||||
|
||||
## Why
|
||||
|
||||
The spec forces onboarding through the web so every account is provably email-controlled
|
||||
and UUID-linked before it can operate anything. The `op.console` redline in `fe2ece0` — a
|
||||
staff-UUID code is refused without being consumed — is what keeps the public console door
|
||||
from ever minting an admin principal.
|
||||
|
||||
> **Backfill note.** Reconstructed 2026-07-07 from the commit history. The account/session
|
||||
> logic, single-use codes, and the op.console redline were covered by handler tests + the
|
||||
> OpenAPI parity gate at each commit; the identity guarantee behind a Bind Code lives in
|
||||
> velocity/Java (CODE-ONLY) and is not verifiable from this repo. Not independently
|
||||
> re-verified for this doc; current tree green at `9911b8c`.
|
||||
@@ -0,0 +1,32 @@
|
||||
# §B3 username-collision reclaim + account migration (ledger backfill)
|
||||
|
||||
- **Type:** feature — retroactive ledger entry
|
||||
- **Date:** 2026-06-27 – 2026-07-05
|
||||
- **Area:** `internal/api` (internal-face reclaim/blacklist, account migrate), `internal/store` (migration 0006)
|
||||
- **Commits:**
|
||||
- `a29571d` feat(api): reclaim squatted usernames for Mojang-priority players (§B3, 正版优先) — `POST /internal/player/reclaim` bars the squatter UUID + stashes its data (30-day hold) in one transaction, idempotent, returns the *first* reclaim's expiry; `GET /internal/player/blacklist/{mc_uuid}` is the login-gate check
|
||||
- `fdb6efb` feat(account): migrate a live account's owned servers to a new account (§B3 inherit)
|
||||
- **Tasks:** #30 (B3 game-login + username-collision reclaim)
|
||||
|
||||
## What it did
|
||||
|
||||
Built the data layer of the Mojang-priority collision flow: when the configured
|
||||
third-party Yggdrasil and official Mojang issue the same username under different UUIDs,
|
||||
the non-genuine squatter is displaced in favour of the real Mojang owner. Both tables are
|
||||
keyed by `mc_uuid`, so the genuine player — identical username, *different* UUID — is
|
||||
never caught by the bar. `fdb6efb` adds the inherit half: migrating an existing account's
|
||||
owned servers onto a new account.
|
||||
|
||||
## Why
|
||||
|
||||
Two players cannot hold one username across two Yggdrasils; the spec resolves it in the
|
||||
genuine Mojang owner's favour with a 30-day data hold for the displaced squatter, told the
|
||||
truth about how long their data is kept (the first hold's window, never a fresh `now()+30d`
|
||||
on retry).
|
||||
|
||||
> **Backfill note.** Reconstructed 2026-07-07 from the commit history. Handlers + the
|
||||
> in-memory repo contract were unit-tested at each commit; the Postgres SQL path is
|
||||
> integration-only, and the velocity collision-routing / limbo prompt / authlib
|
||||
> dual-backend are code-only (Java) and out of this data-layer slice. Not independently
|
||||
> re-verified for this doc; current tree green at `9911b8c`. Related: the operator-facing
|
||||
> `/felis migrate` command has its own doc ([felis-migrate-command](2026-07-05-felis-migrate-command.md)).
|
||||
@@ -0,0 +1,39 @@
|
||||
# felis-api security + robustness hardening (audit sweep) (ledger backfill)
|
||||
|
||||
- **Type:** fix — retroactive ledger entry
|
||||
- **Date:** 2026-06-30 – 2026-07-01
|
||||
- **Area:** `internal/api` (login, request-id, listeners, SSE relays, quota/claim, MyServers), `internal/operator`
|
||||
- **Commits:**
|
||||
- `7a51c1d` fix(api): bound concurrent login bcrypt to shed CPU-pin floods (429 `auth_busy` before the compare; a cap, not a per-account lockout) — *audit #2*
|
||||
- `164ac44` fix(api): validate inbound `X-Request-Id` before echo + audit persist (≤64 bytes, log-safe charset) — *audit-integrity*
|
||||
- `c6c0772` fix(api): read/idle timeouts on all three listeners via a `newAPIServer` factory (closes Slowloris via `ReadHeaderTimeout`; `WriteTimeout` left unset so SSE isn't severed) — *audit #3*
|
||||
- `3c1d647` fix(api): per-principal SSE stream cap (429 `too_many_streams`) — *audit #1, blast-radius bound*
|
||||
- `d6e3189` fix(api): per-write deadline on SSE relay to sever a stalled reader (the real leak close behind the cap) — *audit #1*
|
||||
- `8f41a00` fix(api): clear the SSE write deadline on return so it can't leak onto a reused keep-alive connection — *audit #1*
|
||||
- `6368ab1` fix(api): `COALESCE` the MyServers `owned` flag so an ownerless row doesn't 500 the listing
|
||||
- `2a4a81b` fix(api): don't burn the wake cooldown when refused at capacity
|
||||
- `9873904` fix(operator): populate `Status.Players` from an RCON `list` probe (so the panel doesn't report 0/0)
|
||||
- **Tasks:** #33 (wake cooldown), #34 (Status.Players), #41–#46 (audit #1–#4)
|
||||
|
||||
## What it did
|
||||
|
||||
A hardening sweep across the API's abuse and robustness surface: bound the two unbounded
|
||||
CPU/goroutine amplifiers (concurrent bcrypt, per-principal SSE streams), close the SSE
|
||||
relay's real stalled-reader leak with a per-write deadline (and clear it so it can't leak
|
||||
onto a pooled connection), validate the caller-supplied request id before it reaches the
|
||||
audit trail, set listener timeouts to close Slowloris, and fix two functional bugs — the
|
||||
ownerless-row 500 and the wake cooldown burned on a capacity refusal.
|
||||
|
||||
## Why
|
||||
|
||||
Each is a specific, demonstrated failure mode: a login flood pins every core in bcrypt; a
|
||||
stalled SSE reader leaks a relay goroutine + its upstream kube-apiserver follow *for the
|
||||
life of the process*; an unvalidated `X-Request-Id` is a CR/LF log-forgery vector. The
|
||||
`WriteTimeout`-left-unset detail is load-bearing — a blanket write timeout would sever the
|
||||
healthy long-lived console/build-log streams the platform depends on.
|
||||
|
||||
> **Backfill note.** Reconstructed 2026-07-07 from the commit history. Each fix shipped a
|
||||
> targeted test at its commit — notably `d6e3189`/`8f41a00` use a deadline-aware
|
||||
> `ResponseWriter` that fails closed if the guard is removed. The quota-claim TOCTOU
|
||||
> (audit #4) is a documented KNOWN-LIMITATION (`2c56d17`), closeable only against a real
|
||||
> Postgres. Not independently re-verified for this doc; current tree green at `9911b8c`.
|
||||
@@ -0,0 +1,25 @@
|
||||
# felis_* Prometheus metrics (§23) (ledger backfill)
|
||||
|
||||
- **Type:** feature — retroactive ledger entry
|
||||
- **Date:** 2026-06-30
|
||||
- **Area:** `internal/metrics` + the emit sites in build, platform/fleet, and the start lifecycle
|
||||
- **Commits:**
|
||||
- `75642d9` feat(metrics): named `felis_*` Prometheus collectors
|
||||
- `2a93a9e` feat(metrics): record `felis_image_build_failures_total` on failed builds
|
||||
- `79eae7f` feat(metrics): publish `felis_servers_total` from a fleet snapshot
|
||||
- `8ac5e64` feat(metrics): observe `felis_start_duration_seconds` across the start lifecycle
|
||||
- **Tasks:** #17 (§23 felis_* metrics decision)
|
||||
|
||||
## What it did
|
||||
|
||||
Added the named `felis_*` collector set and wired the three emit points that make it
|
||||
non-empty: a counter incremented on image-build failure, a gauge published from a fleet
|
||||
snapshot, and a histogram observed across the server start lifecycle.
|
||||
|
||||
## Why
|
||||
|
||||
§23 calls for first-class operational metrics under a stable `felis_` namespace rather than
|
||||
ad-hoc logging, so an operator can alert on build failures, fleet size, and start latency.
|
||||
|
||||
> **Backfill note.** Reconstructed 2026-07-07 from the commit history. Not independently
|
||||
> re-verified for this doc; current tree green at `9911b8c` (WSL oracle, go1.26.4).
|
||||
@@ -0,0 +1,37 @@
|
||||
# Auto-update subsystem: decision core + sources + gatherer + window API (ledger backfill)
|
||||
|
||||
- **Type:** feature + fix — retroactive ledger entry
|
||||
- **Date:** 2026-07-01 – 2026-07-05
|
||||
- **Area:** `internal/updates` (pure decision core), `internal/updater` (release sources, gatherer), `internal/api` (window admin API)
|
||||
- **Commits:**
|
||||
- `c01f133` feat(updates): pure I/O-free decision core — each tracked component is Pinned (Minecraft, left alone), Notify, or Scheduled (apply only inside a SysAdmin window); never force-applied, never a downgrade, never an auto-applied prerelease
|
||||
- `3673af6` feat(api): admin API for the maintenance window (`GET`/`PUT /updates/window`), stored as JSON under `platform_settings` — API + persistence only, nothing consumes it yet
|
||||
- `7464fa7` fix(updates): tag `Window` JSON so the persisted window round-trips (the obvious decode is correct by construction; a zero window fails closed to notify-only)
|
||||
- `96b3cc9` feat(updater): wire `updates.Run` to a caller with PaperMC v3 release discovery
|
||||
- `7d27640` feat(updater): GitHub Releases source, routing felis-api/k3s/cloudflared
|
||||
- `7db57b9` feat(updater): `VersionGatherer` extraction core + CLI gather seam
|
||||
- **Tasks:** #38 (auto-update: Felis/k3s/components/Velocity, pin Minecraft)
|
||||
|
||||
## What it did
|
||||
|
||||
Built the auto-update spine as a pure decision core plus the release-discovery sources
|
||||
(PaperMC, GitHub Releases) and the version gatherer, with a SysAdmin-set maintenance
|
||||
window read/written through an admin API. Version parsing tolerates the real feeds (leading
|
||||
`v`, k3s `+k3s1` suffix, calendar versions, prerelease tails) and orders by SemVer
|
||||
precedence.
|
||||
|
||||
## Why
|
||||
|
||||
The red lines are `不要强制自动更新` (never force auto-update) and `能不动的就别动`
|
||||
(Minecraft stays pinned). The design encodes them structurally: a component may be applied
|
||||
*only* inside a window the operator explicitly set, and Minecraft is Pinned so it is never
|
||||
touched. `7464fa7`'s fail-closed zero-window (decodes to notify-only, never a rogue apply)
|
||||
is the safety property for the not-yet-built runner.
|
||||
|
||||
> **Backfill note.** Reconstructed 2026-07-07 from the commit history. The load-bearing
|
||||
> invariants (pinned never changes, no downgrade, no auto-prerelease, apply-only-in-window)
|
||||
> and the JSON round-trip contract were unit-tested at their commits. This subsystem is
|
||||
> deliberately **report-only / integration-deferred**: the concrete Notifier/Applier,
|
||||
> the `felis update` CLI + CronJob, and the current-version producing seams are declared
|
||||
> but not wired (see `internal/updater/doc.go`, `openapi.yaml`). Not independently
|
||||
> re-verified for this doc; current tree green at `9911b8c`.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Passkey (WebAuthn) enrollment subsystem + hardening (ledger backfill)
|
||||
|
||||
- **Type:** feature + fix — retroactive ledger entry
|
||||
- **Date:** 2026-07-01 – 2026-07-02
|
||||
- **Area:** `internal/passkey` (go-webauthn adapter), `internal/api` (enrollment handlers/audit), `internal/store` (migrations 0007–0009)
|
||||
- **Commits:**
|
||||
- `f2c916d` feat(api): passkey enrollment persistence layer
|
||||
- `742f15f` feat(api): passkey enrollment endpoints
|
||||
- `0261204` feat(passkey): go-webauthn enrollment verifier adapter (Oracle-verified against a virtual authenticator)
|
||||
- `fce0fce` feat(passkey): wire the enrollment verifier into felis-api
|
||||
- `7278cd7` feat(passkey): require + record user verification at enrollment (`UserVerification=required`; capture `user_verified`/`backup_eligible`/`backup_state` — migration 0009) — *fix (d)*
|
||||
- `cdbb5ab` fix(api): record credential id in the passkey-register audit event so bind/unbind are symmetric — *fix (a)*
|
||||
- `9953275` fix(api): bound `webauthn_challenges` growth by superseding *all* prior rows per (user, purpose) — *fix (b)*
|
||||
- `20e31fb` fix(store): cascade-delete passkeys + challenges on user removal (recreate both FKs `ON DELETE CASCADE`, scoped to the passkey tables only) — *fix (c)*
|
||||
- `54bc6ef` fix(api): clear bound passkeys on password change to close a takeover foothold — *fix (e)*
|
||||
- **Tasks:** #36 (passkey bind with email-OTP fallback), #48–#52 (fixes a–e)
|
||||
|
||||
## What it did
|
||||
|
||||
Built the WebAuthn *enrollment* half — persistence, the go-webauthn crypto adapter, and
|
||||
the register-begin/finish endpoints — then hardened it through the five-fix batch (a–e):
|
||||
symmetric audit, a bounded challenge table, cascade cleanup, enforced+recorded user
|
||||
verification, and unbinding every passkey on a password reset so a passkey planted through
|
||||
a transiently-hijacked session cannot survive as a standing login foothold.
|
||||
|
||||
## Why
|
||||
|
||||
Passkeys are the phishing-resistant factor with email-OTP as the fallback. The hardening
|
||||
batch closes the seams that make enrollment safe to *rely on*: without UV enforcement a
|
||||
passkey proves possession but not user; without the password-reset clear, a planted
|
||||
passkey outlives the very remediation meant to evict an attacker.
|
||||
|
||||
> **Backfill note.** Reconstructed 2026-07-07 from the commit history. The adapter crypto
|
||||
> was verified against a virtual authenticator (virtualwebauthn), and each fix shipped
|
||||
> with a targeted test (UV-negative rejection, challenge-growth bound, cascade, symmetric
|
||||
> audit) at its commit. Not independently re-verified for this doc; current tree green at
|
||||
> `9911b8c`. The assertion/login half is a separate doc ([passkey-login](2026-07-01-passkey-login.md)).
|
||||
@@ -0,0 +1,36 @@
|
||||
# Passkey (WebAuthn) login: assertion, discoverable, clone-detection (ledger backfill)
|
||||
|
||||
- **Type:** feature — retroactive ledger entry
|
||||
- **Date:** 2026-07-01 – 2026-07-05
|
||||
- **Area:** `internal/passkey` (assertion crypto), `internal/api` (login/assertion, unbind, UA-guard), `internal/store` (migrations 0013/0014)
|
||||
- **Commits:**
|
||||
- `e035142` feat(passkey): WebAuthn login/assertion crypto adapter (BeginLogin/FinishLogin over go-webauthn, Oracle-verified against a virtual authenticator; surfaces the signature counter as a ceremony fact)
|
||||
- `ec468ba` feat(auth): discoverable (usernameless) passkey login — the from-zero door the username-first assertion couldn't key on
|
||||
- `0dbd557` fix(store): renumber the discoverable-login migration 0013 → 0014
|
||||
- `9e1df12` feat(passkey): advance `sign_count`, reject clone-warned assertions
|
||||
- `4f59d51` feat(auth): owner-tier passkey-unbind remediation endpoint
|
||||
- `a63f49d` feat(panel): steer WeChat/QQ in-app browsers to the system browser for passkey — a backend-only UA interstitial (the SPA is untouched); asset/API/health requests pass through, an `ua_ack` cookie lets a determined user continue
|
||||
- **Tasks:** #40 (from-zero discoverable login), #67 (WeChat/QQ UA-guard in `internal/panel`)
|
||||
|
||||
## What it did
|
||||
|
||||
Built the assertion (login) half of the ceremony: the crypto adapter, then discoverable
|
||||
credentials so a user with no typed identifier can still log in (the enrollment
|
||||
identifier problem the earlier deferral doc named), clone detection via the advancing
|
||||
signature counter, and the owner-tier unbind remediation. `a63f49d` guards the flow at the
|
||||
transport edge — WebAuthn is unusable inside the WeChat/QQ WebViews, so those UAs get a
|
||||
bilingual "open in your system browser" page instead of the passkey SPA.
|
||||
|
||||
## Why
|
||||
|
||||
Enrollment without a login path is half a feature. Discoverable credentials resolve the
|
||||
blocker recorded in the earlier deferral (`users.email` is nullable/non-unique and a
|
||||
player's username is their Minecraft UUID, so username-first assertion had nothing to key
|
||||
on). The UA-guard stops the most common real-world dead end: a passkey prompt that can
|
||||
never succeed inside an in-app browser.
|
||||
|
||||
> **Backfill note.** Reconstructed 2026-07-07 from the commit history. The assertion crypto
|
||||
> was verified against a virtual authenticator (enrollment→assertion chain, origin-mismatch
|
||||
> and unbound-credential rejection); `9e1df12`'s clone policy and the UA-guard pass-through
|
||||
> were unit-tested at their commits. Not independently re-verified for this doc; current
|
||||
> tree green at `9911b8c`.
|
||||
@@ -0,0 +1,37 @@
|
||||
# System servers: login-limbo + lobby (always-on gate) (ledger backfill)
|
||||
|
||||
- **Type:** feature — retroactive ledger entry
|
||||
- **Date:** 2026-07-02
|
||||
- **Area:** `internal/config`, `internal/naming`, `internal/api` (CRD readiness), `internal/operator`, `internal/platform`, `cmd/felis`, `plugins/limbo`, `deploy/limbo` + `deploy/lobby`
|
||||
- **Commits:**
|
||||
- `9bed51b` feat(config): `[velocity] login_image/lobby_image` — setup provisions the always-on system services only when set (empty = fail-loud skip; no official LOOHP/Limbo image exists)
|
||||
- `9ef817f` feat(naming): reserved system-server names + service-token identifiers (single source of truth for the internal-API credential Secret)
|
||||
- `159107b` feat(api): HTTP readiness knob on `MinecraftServer` + user-server fallback defaults to the login gate
|
||||
- `dc23cb5` feat(operator): system-server pod HTTP readiness probe + login-only `FELIS_SERVICE_TOKEN` env (keyed off the reserved name so it can never leak into a user pod; sourced via `secretKeyRef`, never inlined)
|
||||
- `3fdb3d0` feat(platform): internal-API base-URL helper + single-sourced token Secret
|
||||
- `f554d52` feat(cli): provision the reaper-exempt login/lobby servers + replicate the service-token Secret into the minecraft namespace
|
||||
- `241fe21` feat(limbo): felis-limbo in-game login flow (join → blacklist check → mint bind code → open book to `console.<root_domain>` → poll link-status → BungeeCord transfer to lobby; fail-closed)
|
||||
- `c7315e4` feat(deploy): login-limbo + lobby images with game-port pinning (server-port pinned to GamePort 25565 on every start)
|
||||
- **Tasks:** #53–#68 (system-server plumbing L1–L4, limbo plugin, operator env injection)
|
||||
|
||||
## What it did
|
||||
|
||||
Stood up the always-on authentication gate: reserved, reaper-exempt login/lobby
|
||||
`MinecraftServer`s provisioned by setup, an HTTP readiness path for the RCON-less LOOHP/Limbo
|
||||
loader (which reports "started" only after the first tick), and the felis-limbo plugin that
|
||||
runs the whole onboarding *inside* Limbo before transferring an admitted player to the
|
||||
lobby. A fresh connection always lands on the login gate, never a user backend, so
|
||||
authentication is always in front.
|
||||
|
||||
## Why
|
||||
|
||||
The spec requires that a player authenticate before reaching any real server. That needs a
|
||||
purpose-built always-on front server (Limbo) that speaks to the internal API — hence the
|
||||
login-only service-token injection (keyed to the reserved name so it can never reach a user
|
||||
pod) and the HTTP readiness knob for a loader that has no RCON.
|
||||
|
||||
> **Backfill note.** Reconstructed 2026-07-07 from the commit history. The Go layer
|
||||
> (config/naming/readiness/operator env/platform) was unit-tested at each commit; the
|
||||
> felis-limbo plugin is Java verified against a real Limbo jar via podman (#65), and the
|
||||
> images carry a real build+boot check (#63, limbo `/healthz` 200 on 25565). Not
|
||||
> independently re-verified for this doc; current tree green at `9911b8c`.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Operator: idle auto-stop, quotas, startup/readiness timeouts, /readyz (ledger backfill)
|
||||
|
||||
- **Type:** feature + fix — retroactive ledger entry
|
||||
- **Date:** 2026-07-05
|
||||
- **Area:** `internal/operator` (idle stop, timeouts), `internal/api` (quotas, /readyz)
|
||||
- **Commits:**
|
||||
- `91bfa27` feat(operator): idle auto-stop (§8)
|
||||
- `e574749` feat(api): enforce CPU/memory/storage quotas (§9.3, §22)
|
||||
- `7f7e459` fix(operator): enforce startup and readiness timeouts (§5, §8)
|
||||
- `7becb38` fix(api): implement `/readyz` with real DB + K8s API + CRD checks (§7)
|
||||
- **Tasks:** §5/§7/§8/§9.3/§22 operator + resource-governance spec items
|
||||
|
||||
## What it did
|
||||
|
||||
Rounded out the operator's lifecycle governance: stop idle servers automatically, enforce
|
||||
per-resource CPU/memory/storage quotas at claim/create, bound how long a server may sit in
|
||||
startup/readiness before the operator gives up, and make `/readyz` a real dependency check
|
||||
(DB, Kubernetes API, and the CRD) rather than a static 200.
|
||||
|
||||
## Why
|
||||
|
||||
An orchestrator that never reclaims idle capacity or bounds startup will accumulate stuck
|
||||
and wasteful workloads; a `/readyz` that always returns 200 tells the load balancer a
|
||||
broken control plane is healthy. These are the spec's resource-governance and
|
||||
readiness-correctness requirements (§5/§7/§8/§9.3/§22).
|
||||
|
||||
> **Backfill note.** Reconstructed 2026-07-07 from the commit history. Covered by Go unit
|
||||
> tests at each commit. Not independently re-verified for this doc; current tree green at
|
||||
> `9911b8c` (WSL oracle, go1.26.4).
|
||||
@@ -27,6 +27,41 @@ not yet committed.
|
||||
|---|---|---|
|
||||
| _None._ | | |
|
||||
|
||||
## Detail docs
|
||||
|
||||
Depth docs for substantial changes, keyed to the commit(s) they cover. The committed
|
||||
ledger table below stays a lossless mirror of `git log` (so it can be regenerated); this
|
||||
section is where a row's detail doc, when it has one, is found. Most rows — panel/UI,
|
||||
docs, chore, style — have no detail doc by convention and are recorded by their table row
|
||||
alone. Entries marked *(backfill)* were reconstructed retroactively on 2026-07-07 from git
|
||||
history to close the ledger's detail-doc axis for the pre-convention functional commits;
|
||||
each carries a backfill note stating it was not independently re-verified. Frontend/`panel`
|
||||
commits are the collaborator's UI work and are not given detail docs here.
|
||||
|
||||
| Detail doc | Commit(s) | Scope |
|
||||
|---|---|---|
|
||||
| [foundational-subsystems](2026-06-26-foundational-subsystems.md) *(backfill)* | `7fbebfe` `708cdfc` `43ab921` `78b8cf6` `d39605e` `b508fcc` `47fcd90` `93f143f` | initial import: CRD, core libs, backup, operator, submit, api, platform, plugins |
|
||||
| [modpack-submission-lane](2026-06-26-modpack-submission-lane.md) *(backfill)* | `d39605e` `598f3d3` | §8 modpack build/approval pipeline + local/S3 backends |
|
||||
| [deploy-bootstrap-installer](2026-06-27-deploy-bootstrap-installer.md) *(backfill)* | `58fa4b0` `94a3b7b` `deaa2f8` `318a724` `e5f1682` `28c3eee` `c14ed17` `d9e866f` `b84debf` | one-line bootstrap installer + demo bring-up |
|
||||
| [console-auth-passwordless](2026-06-27-console-auth-passwordless.md) *(backfill)* | `af14f02` `0c1cc59` `3b43f05` `c20b12c` | local-password login → passwordless migration + residue sweep |
|
||||
| [felis-cli-break-glass-setup](2026-06-27-felis-cli-break-glass-setup.md) *(backfill)* | `e108a37` `2d0bbb0` `a94b001` `eb5875a` `f5d00f3` `9c46632` `7d91373` | break-glass recovery console + first-run setup + apply/migrate |
|
||||
| [cloudflare-tunnel-access-edge](2026-06-27-cloudflare-tunnel-access-edge.md) *(backfill)* | `53a7664` `ba13839` `a531f5e` `2810fe8` `7d3be64` `e058a64` | §14 Tunnel + fail-closed Access edge + NodePort fence |
|
||||
| [player-onboarding-b2](2026-06-27-player-onboarding-b2.md) *(backfill)* | `dbe34a1` `1f8b9bb` `116595f` `fe2ece0` `55592ed` `6c3999a` `879b177` | §B2 email-OTP, account-link, QR, Bind-Code + OTP throttle |
|
||||
| [username-reclaim-b3](2026-06-27-username-reclaim-b3.md) *(backfill)* | `a29571d` `fdb6efb` | §B3 Mojang-priority reclaim + account migration |
|
||||
| [felis-metrics](2026-06-30-felis-metrics.md) *(backfill)* | `75642d9` `2a93a9e` `79eae7f` `8ac5e64` | §23 felis_* Prometheus collectors |
|
||||
| [felis-api-hardening](2026-06-30-felis-api-hardening.md) *(backfill)* | `7a51c1d` `164ac44` `c6c0772` `3c1d647` `d6e3189` `8f41a00` `6368ab1` `2a4a81b` `9873904` | audit #1–#3 + robustness fixes |
|
||||
| [passkey-enrollment](2026-07-01-passkey-enrollment.md) *(backfill)* | `f2c916d` `742f15f` `0261204` `fce0fce` `7278cd7` `cdbb5ab` `9953275` `20e31fb` `54bc6ef` | WebAuthn enrollment + hardening a–e |
|
||||
| [passkey-login](2026-07-01-passkey-login.md) *(backfill)* | `e035142` `ec468ba` `0dbd557` `9e1df12` `4f59d51` `a63f49d` | WebAuthn assertion/discoverable login + UA-guard |
|
||||
| [auto-update-subsystem](2026-07-01-auto-update-subsystem.md) *(backfill)* | `c01f133` `3673af6` `7464fa7` `96b3cc9` `7d27640` `7db57b9` | update decision core + sources + gatherer + window API (report-only) |
|
||||
| [system-servers-login-limbo-lobby](2026-07-02-system-servers-login-limbo-lobby.md) *(backfill)* | `9bed51b` `9ef817f` `159107b` `dc23cb5` `3fdb3d0` `f554d52` `241fe21` `c7315e4` | always-on login-limbo + lobby auth gate |
|
||||
| [operator-idle-quota-readiness](2026-07-05-operator-idle-quota-readiness.md) *(backfill)* | `91bfa27` `e574749` `7f7e459` `7becb38` | idle auto-stop, quotas, timeouts, /readyz |
|
||||
| [break-glass-halt](2026-07-05-break-glass-halt.md) | `c2ee21a` | §B4 break-glass halt-a-server op |
|
||||
| [felis-migrate-command](2026-07-05-felis-migrate-command.md) | `c1aa38b` | §B3 `/felis migrate` account migration |
|
||||
| [on-demand-world-backup](2026-07-07-on-demand-world-backup.md) | `7a7c0d5` | §B4 Sync phase 1 — external backup endpoint + Job |
|
||||
| [internal-backup-endpoint](2026-07-07-internal-backup-endpoint.md) | `f2fc57c` | §B4 Sync phase 2a — internal-face backup endpoint |
|
||||
| [internal-api-clusterip-service](2026-07-07-internal-api-clusterip-service.md) | `2ba9948` | felis-api internal-face ClusterIP Service |
|
||||
| [break-glass-backup-peer](2026-07-07-break-glass-backup-peer.md) | `fc748d3` | §B4 Sync phase 2b — console backup peer |
|
||||
|
||||
## Committed change ledger
|
||||
|
||||
Oldest first (project build order). Commit = short SHA on `main`. Frontend/`panel`
|
||||
@@ -210,3 +245,4 @@ primary record.
|
||||
| f2fc57c | 2026-07-07 | feat(api): add internal-face break-glass world backup endpoint (§B4 Sync) |
|
||||
| 2ba9948 | 2026-07-07 | fix(platform): front the felis-api internal face on its own ClusterIP Service |
|
||||
| fc748d3 | 2026-07-07 | feat(breakglass): add "back up a world now" console peer (§B4 Sync) |
|
||||
| 9911b8c | 2026-07-07 | docs(changes): record the break-glass backup console peer (§B4 Sync phase 2b) |
|
||||
Reference in new issue
Block a user