From 9ef817f2b3f30ec1363d2859da9865e082dc8335 Mon Sep 17 00:00:00 2001 From: Minseong Choi Date: Thu, 2 Jul 2026 19:36:58 +0900 Subject: [PATCH] feat(naming): system-server names, validation, and service-token identifiers SystemLoginServer/SystemLobbyServer plus ValidateSystemServerName (format rule without the reservation check) let the platform provision the reserved login/lobby names users can never claim. ServiceTokenSecretName/Key are the one source of truth for the internal-API credential Secret, shared by the platform renderer and the operator's login-pod injection. --- internal/naming/naming.go | 42 ++++++++++++++++++++++++++++++++++ internal/naming/naming_test.go | 37 ++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/internal/naming/naming.go b/internal/naming/naming.go index 99393f1..bba5b3a 100644 --- a/internal/naming/naming.go +++ b/internal/naming/naming.go @@ -30,6 +30,32 @@ var reserved = map[string]struct{}{ "www": {}, } +// System server names Felis provisions itself. They deliberately live on the +// reserved list so no user can claim them, yet the platform must be able to +// create them — see ValidateSystemServerName. +const ( + // SystemLoginServer is the always-on limbo auth gate (LOOHP/Limbo). It is the + // front door every fresh connection lands on and the ONLY safe fallback: a + // stopped/starting backend routes here, never onward past authentication. + SystemLoginServer = "login" + // SystemLobbyServer is the post-auth /menu hub (Paper + felis-paper). It is + // reachable only after the login gate passes a player through, so it must + // never be used as a fallback target (that would bypass the gate). + SystemLobbyServer = "lobby" +) + +// ServiceTokenSecretName / ServiceTokenSecretKey name the internal-API bearer +// credential Secret (spec §7). They are one source of truth shared across +// subsystems: the platform renderer wires this Secret into the felis-api +// Deployment, and the operator injects it into the login system server's pod as +// FELIS_SERVICE_TOKEN via a secretKeyRef (never a literal). The Secret itself is +// provisioned out-of-band (deploy/bootstrap.sh) and, for the login gate, replicated +// into the minecraft namespace by `felis setup`; these constants only name it. +const ( + ServiceTokenSecretName = "felis-service-token" + ServiceTokenSecretKey = "token" +) + // ValidateServerName checks the §22 name rule and reservation list. func ValidateServerName(name string) error { if !serverNameRE.MatchString(name) { @@ -44,6 +70,22 @@ func ValidateServerName(name string) error { return nil } +// ValidateSystemServerName checks the §22 format rule (^[a-z0-9-]{3,32}$, no +// leading/trailing dash) but DELIBERATELY skips the reservation check. It is the +// admission path for platform-provisioned system services (login, lobby), which +// carry reserved names on purpose: users can never claim them via +// ValidateServerName, yet setup must still be able to create them. It is not a +// public claim path — only the setup/system-service provisioner calls it. +func ValidateSystemServerName(name string) error { + if !serverNameRE.MatchString(name) { + return fmt.Errorf("naming: invalid system server name %q: must match ^[a-z0-9-]{3,32}$", name) + } + if strings.HasPrefix(name, "-") || strings.HasSuffix(name, "-") { + return fmt.Errorf("naming: system server name %q must not start or end with '-'", name) + } + return nil +} + // IsReserved reports whether label is on the reserved list. func IsReserved(label string) bool { _, ok := reserved[label] diff --git a/internal/naming/naming_test.go b/internal/naming/naming_test.go index 6b21f36..5a8e63b 100644 --- a/internal/naming/naming_test.go +++ b/internal/naming/naming_test.go @@ -37,6 +37,43 @@ func TestValidateServerName(t *testing.T) { } } +// ValidateSystemServerName keeps the format rule but drops the reservation +// check, so the platform can provision the reserved system names (login, lobby) +// that ValidateServerName correctly refuses to hand to users. +func TestValidateSystemServerName(t *testing.T) { + cases := []struct { + name string + ok bool + }{ + {"login", true}, // reserved, but a legal system service + {"lobby", true}, // reserved, but a legal system service + {"admin", true}, // reserved names are allowed on this path + {"survival", true}, + {"ab", false}, // still too short + {"Login", false}, // still case-sensitive + {"-leading", false}, // still no leading hyphen + {"has space", false}, // still no illegal chars + } + for _, c := range cases { + err := naming.ValidateSystemServerName(c.name) + if c.ok && err != nil { + t.Errorf("ValidateSystemServerName(%q) = %v, want ok", c.name, err) + } + if !c.ok && err == nil { + t.Errorf("ValidateSystemServerName(%q) = nil, want error", c.name) + } + } + + // The two paths must genuinely differ on reserved names: user path refuses + // "lobby", system path accepts it. + if naming.ValidateServerName("lobby") == nil { + t.Error("ValidateServerName(lobby) accepted; reserved name must be refused for users") + } + if naming.ValidateSystemServerName("lobby") != nil { + t.Error("ValidateSystemServerName(lobby) refused; system path must accept it") + } +} + func TestWorldPVCName(t *testing.T) { cases := map[string]string{ "survival": "world-survival-0",