Files
felhom.eu/hub/internal/api/routes.go
T
admin d62cc08b2e
gates / gates (push) Successful in 6m44s
hub v0.152.0: operator access tokens with scopes (09 §3 decision 251, operator 2026-10-15 11:38)
- Configuration → Access tokens: named, scoped (group read/write), expiring (≤365 d), shown once,
  stored SHA-256 only, every use logged by name. Bearer fhp_… on the web surface only; /api/v1 refuses it.
- Route table = the code's single source (web/routes.go), pinned to ServeHTTP's switch; session-only:
  secrets shown/minted, deletes/resets, password, tokens.
- R-952 closed: /api/v1 route table names each caller, pinned to the switch; no-credential refusal test.
- Fail-open fixed: an empty operator password served the UI open (main skipped RequireAuth). Live hub never open.
- 05 §17, 04 §6.2, 13, 09 §3 251; register 155→157 (+R-1054, R-1055, R-1056, −R-952); STATUS.
- website: prowlarr-logo.svg (byte copy from felhom.eu-media) — the site gate refused every push.
Not deployed — waits for the operator's yes.

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
2026-10-15 12:20:35 +02:00

84 lines
5.2 KiB
Go

package api
// The box-API route table (hub v0.152.0, R-952, `05` §17.6). EVERY case of ServeHTTP's route switch has one entry,
// in the same order and with the same match, naming WHO may call it. TestAPIRouteTable_MatchesSwitch reads the
// switch from handler.go's syntax tree and fails on a route added without an entry here; TestAPIRoutes_NoCredential
// sends every non-public route a request with no credential and requires a refusal. The table documents and pins;
// each handler still does its own check (the table is not consulted at run time).
//
// An operator access token (fhp_…) is refused on EVERY route here, before the switch (handler.go ServeHTTP).
type apiAuth string
const (
authPublic apiAuth = "public" // no credential by design — the reason is on the entry
authCustomer apiAuth = "customer" // the household's controller key (or the global key)
authHost apiAuth = "host" // the box's own host key, self-scoped (or the global key)
authGlobal apiAuth = "global" // the operator/global key only
authPassphrase apiAuth = "passphrase" // X-Retrieval-Password of that household
authAppliance apiAuth = "appliance" // the appliance's own bearer token
authRecovery apiAuth = "recovery" // only while the operator has armed recovery mode for the host
)
type apiRoute struct {
Method string
Exact, Prefix, Suffix, Contains string
Auth apiAuth
Why string // for authPublic
}
var apiRoutes = []apiRoute{
{Method: "POST", Exact: "/report", Auth: authCustomer},
{Method: "GET", Exact: "/wait", Auth: authCustomer},
{Method: "POST", Exact: "/appliance/register", Auth: authPublic, Why: "a secret-free ISO box registers itself; per-IP rate-limited, answers no enumeration"},
{Method: "GET", Exact: "/appliance/poll", Auth: authAppliance},
{Method: "POST", Exact: "/host-report", Auth: authHost},
{Method: "POST", Exact: "/host-enroll", Auth: authPassphrase},
{Method: "POST", Exact: "/admin/hosts", Auth: authGlobal},
{Method: "PUT", Prefix: "/hosts/", Suffix: "/escrow", Auth: authHost},
{Method: "GET", Prefix: "/hosts/", Suffix: "/escrow", Auth: authHost},
{Method: "GET", Prefix: "/hosts/", Suffix: "/escrow/retained", Auth: authHost},
{Method: "PUT", Prefix: "/hosts/", Suffix: "/recovery-credential", Auth: authHost},
{Method: "GET", Prefix: "/admin/hosts/", Suffix: "/recovery-credential", Auth: authGlobal},
{Method: "PUT", Prefix: "/admin/hosts/", Suffix: "/recovery-mode", Auth: authGlobal},
{Method: "DELETE", Prefix: "/admin/hosts/", Suffix: "/recovery-mode", Auth: authGlobal},
{Method: "POST", Prefix: "/hosts/", Suffix: "/re-enroll", Auth: authRecovery},
{Method: "GET", Prefix: "/hosts/", Suffix: "/restore-directive", Auth: authHost},
{Method: "POST", Prefix: "/hosts/", Suffix: "/wg", Auth: authHost},
{Method: "POST", Prefix: "/hosts/", Suffix: "/pbs/consume-token", Auth: authHost},
{Method: "POST", Prefix: "/hosts/", Suffix: "/os-report", Auth: authHost},
{Method: "GET", Prefix: "/hosts/", Suffix: "/desired-state", Auth: authHost},
{Method: "GET", Prefix: "/hosts/", Suffix: "/jobs", Auth: authHost},
{Method: "DELETE", Prefix: "/hosts/", Contains: "/jobs/", Auth: authHost},
{Method: "PUT", Prefix: "/admin/hosts/", Suffix: "/desired-state", Auth: authGlobal},
{Method: "POST", Prefix: "/admin/hosts/", Suffix: "/jobs", Auth: authGlobal},
{Method: "PUT", Exact: "/admin/wg/endpoint", Auth: authGlobal},
{Method: "GET", Exact: "/admin/wg/endpoint", Auth: authGlobal},
{Method: "POST", Exact: "/admin/wg/peers", Auth: authGlobal},
{Method: "DELETE", Exact: "/admin/wg/peers", Auth: authGlobal},
{Method: "GET", Exact: "/admin/wg/peers", Auth: authGlobal},
{Method: "PUT", Exact: "/admin/wg/operator-peer", Auth: authGlobal},
{Method: "GET", Exact: "/admin/wg/operator-peer", Auth: authGlobal},
{Method: "POST", Exact: "/claim/reset-request", Auth: authCustomer},
{Method: "POST", Exact: "/event", Auth: authCustomer},
{Method: "POST", Exact: "/mail", Auth: authCustomer},
{Method: "POST", Exact: "/notify", Auth: authCustomer},
{Method: "POST", Exact: "/preferences", Auth: authCustomer},
{Method: "GET", Exact: "/customers", Auth: authGlobal},
{Method: "GET", Prefix: "/customers/", Auth: authGlobal},
{Method: "GET", Prefix: "/recovery/", Auth: authPassphrase},
{Method: "GET", Prefix: "/config/", Auth: authPassphrase},
{Method: "POST", Prefix: "/offsite/consume-password/", Auth: authCustomer},
{Method: "POST", Prefix: "/offsite/register-key/", Auth: authCustomer},
{Method: "POST", Prefix: "/offsite/confirm-key/", Auth: authCustomer},
{Method: "POST", Prefix: "/offsite/move-aside/", Auth: authCustomer},
{Method: "POST", Prefix: "/offsite/window-open/", Auth: authCustomer},
{Method: "POST", Prefix: "/offsite/window-close/", Auth: authCustomer},
{Method: "POST", Prefix: "/offsite/abandon-request/", Auth: authCustomer},
{Method: "POST", Prefix: "/offsite/abandon-cancel/", Auth: authCustomer},
{Method: "POST", Prefix: "/offsite/abandon-status/", Auth: authCustomer},
{Method: "GET", Prefix: "/artifacts/", Auth: authPassphrase},
{Method: "GET", Exact: "/assets/manifest", Auth: authCustomer},
{Method: "GET", Prefix: "/assets/file/", Auth: authCustomer},
}