Files
Felis/cmd/felis/nano.go
T
flyemoji 4e5a809dad docs(nano): drop the promise of a felis setup --nano that should not exist
nano.go's package comment told the reader that `felis setup --nano` installs the
multiplexer as a service. No such flag has ever existed -- `felis setup` defines
only -config and -dev -- so anyone following the comment gets "flag provided but
not defined: -nano" and exit 2.

Adding the flag was the obvious reading, and it is the wrong one. setup does not
install anything selectively: it re-images the host by running the full bootstrap
TUI, and no install-mode parameter is threaded anywhere -- nothing in Go reads or
writes FELIS_INSTALL_MODE, which is a shell variable bootstrap.sh consumes on its
own. So `--nano` could only mean one of two things. Re-run the installer in nano
mode, which is what pointing at the installer already does. Or convert a
provisioned full host into a nano one, which means tearing down k3s, Postgres and
the proxy -- an uninstall, not a flag.

setup's --dev already settled this shape once. It looked like a channel selector,
silently installed release, and the fix was to refuse it and name the installer
rather than pretend to choose. The same answer applies here, so the comment now
names the real entry point -- the installer's `[2] Felis-nano` prompt, or
FELIS_INSTALL_MODE=nano -- and records why there is no flag, so the next reader
does not reopen it.

No refusing --nano flag is added: --dev exists because it used to be a silent
no-op that people passed, and nothing has ever accepted --nano, so flag's own
"not defined" error is already the correct and clearer failure.
2026-07-28 17:27:02 +09:00

85 lines
4.0 KiB
Go

package main
// felis nano: the Felis-nano hasJoined multiplexer as a felis subcommand — the lightweight
// serve path for a third-party server operator who wants multi-Yggdrasil federation without a
// full Felis control plane (no k3s, no Postgres, no DB). It reads [[auth_source]] from
// felis.toml, leads with Mojang as the code-owned identity anchor (正版优先), and serves the
// vanilla sessionserver hasJoined endpoint. Point Velocity at it with
// -Dmojang.sessionserver=http://127.0.0.1:8081/session/minecraft/hasJoined
// — Velocity's property takes the FULL endpoint URL, path included (its default is the
// full https://sessionserver.mojang.com/session/minecraft/hasJoined), and Velocity issues
// that request itself rather than through authlib. Then it verifies logins against Mojang
// plus every configured third-party source. Serving a proxy on another host means binding
// off-loopback with -listen; see the flag below for why that is an explicit opt-in.
//
// This is the no-database delivery of the identical brain `felis api` mounts through its
// route table (internal/api.HasJoinedHandler). The bootstrap installer's nano choice — its
// `[2] Felis-nano` prompt, or FELIS_INSTALL_MODE=nano — installs this as the
// felis-nano.service unit; here it just serves.
//
// There is deliberately no `felis setup --nano`. setup re-images the host through the full
// bootstrap TUI and carries no install-mode parameter anywhere (nothing in Go reads or sets
// FELIS_INSTALL_MODE), so such a flag would either re-run the installer — which is what
// pointing at the installer already does — or tear a full install down into a nano one,
// which is an uninstall, not a flag. This is the same reasoning that makes setup's --dev
// refuse and name the installer rather than pretend to choose a channel.
import (
"context"
"flag"
"fmt"
"io"
"net/http"
"felis.lolicon.best/internal/api"
"felis.lolicon.best/internal/config"
)
// nanoStubRepo satisfies api.Repo but implements only the one method handleHasJoined calls.
// The reclaim username blacklist is a felis-api/DB concern; a nano host has no Postgres, so
// nothing is barred here. ponytail: a real blacklist would need the very DB nano exists to
// avoid — YAGNI until a nano host grows a reclaim store.
type nanoStubRepo struct{ api.Repo }
func (nanoStubRepo) IsUsernameBlacklisted(context.Context, string) (bool, error) { return false, nil }
func cmdNano(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("nano", flag.ContinueOnError)
fs.SetOutput(stderr)
cfgPath := fs.String("config", "/etc/felis/felis.toml", "path to felis.toml (reads [[auth_source]])")
// Loopback default: hasJoined carries no auth token (authlib speaks the vanilla
// sessionserver protocol), so a public bind is an open auth relay — anyone can point
// their proxy at it and spend this host's egress IP on Mojang. A same-host Velocity
// reaches 127.0.0.1; serving an off-host proxy is an explicit -listen opt-in.
listen := fs.String("listen", "127.0.0.1:8081", "listen address for the hasJoined endpoint")
if err := fs.Parse(args); err != nil {
return 2
}
cfg, err := config.LoadNano(*cfgPath)
if err != nil {
fmt.Fprintln(stderr, "felis nano:", err)
return 1
}
handler := api.HasJoinedHandler(authSourcesFromConfig(cfg.AuthSources), nanoStubRepo{})
fmt.Fprintf(stderr, "felis nano: hasJoined multiplexer on %s — Mojang + %d third-party source(s)\n", *listen, len(cfg.AuthSources))
for i, s := range cfg.AuthSources {
fmt.Fprintf(stderr, " [%d] %s -> %s\n", i+1, s.Tag, s.URL)
}
// Log each request so a live login attempt is visible while testing against a real
// Velocity — "is authlib even reaching me?" is the first question during verification.
logged := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(stderr, "felis nano: %s %s\n", r.Method, r.RequestURI)
handler.ServeHTTP(w, r)
})
srv := newAPIServer(*listen, logged)
if err := srv.ListenAndServe(); err != nil {
fmt.Fprintln(stderr, "felis nano:", err)
return 1
}
return 0
}