RC-6
release-tag / release-image (push) Successful in 2m10s

This commit is contained in:
2026-08-10 19:44:17 +02:00
parent 551ed007e6
commit 185ccf1101
20 changed files with 875 additions and 124 deletions
+4
View File
@@ -34,6 +34,10 @@ func (s *Server) adminCreateCharacterAnchor(w http.ResponseWriter, r *http.Reque
jsonOut(w, http.StatusConflict, map[string]string{"error": "character anchor already exists"})
return
}
if errors.Is(err, artifact.ErrOpenAIBudgetExceeded) {
jsonOut(w, http.StatusTooManyRequests, map[string]string{"error": err.Error()})
return
}
jsonOut(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
return
}
+23
View File
@@ -64,6 +64,19 @@ func (s *Server) adminArtifactUsage(w http.ResponseWriter, r *http.Request) {
_ = s.store.DB.QueryRowContext(r.Context(), `SELECT count(*),COALESCE(sum(estimated_cost_usd),0)
FROM artifact_api_usage WHERE kind='character_anchor'`).Scan(&anchorCalls, &anchorCost)
var rollingCalls1H, rollingCalls24H int64
var rollingCost24H float64
oneHourMS := now.Add(-time.Hour).UnixMilli()
twentyFourHoursMS := now.Add(-24 * time.Hour).UnixMilli()
_ = s.store.DB.QueryRowContext(r.Context(), `SELECT
COALESCE(sum(CASE WHEN created_at>=? THEN 1 ELSE 0 END),0),
count(*),COALESCE(sum(estimated_cost_usd),0)
FROM artifact_api_usage WHERE provider='openai' AND created_at>=?`, oneHourMS, twentyFourHoursMS).Scan(&rollingCalls1H, &rollingCalls24H, &rollingCost24H)
cfg := s.settings.Get()
circuitBlocked := (cfg.OpenAIMaxCalls1H > 0 && rollingCalls1H >= int64(cfg.OpenAIMaxCalls1H)) ||
(cfg.OpenAIMaxCalls24H > 0 && rollingCalls24H >= int64(cfg.OpenAIMaxCalls24H)) ||
(cfg.OpenAIMaxCost24HUSD > 0 && rollingCost24H+cfg.OpenAIBudgetReserveUSD > cfg.OpenAIMaxCost24HUSD)
recent := make([]artifactUsageRow, 0, 20)
rows, err := s.store.DB.QueryContext(r.Context(), `SELECT created_at,task_id,kind,model,endpoint,size,quality,request_id,
input_tokens,input_text_tokens,input_image_tokens,output_tokens,total_tokens,estimated_cost_usd,pricing_basis
@@ -114,6 +127,16 @@ func (s *Server) adminArtifactUsage(w http.ResponseWriter, r *http.Request) {
"output_tokens": outputTokens,
"total_tokens": totalTokens,
},
"circuit_breaker": map[string]any{
"blocked": circuitBlocked,
"calls_1h": rollingCalls1H,
"calls_24h": rollingCalls24H,
"cost_24h_usd": rollingCost24H,
"max_calls_1h": cfg.OpenAIMaxCalls1H,
"max_calls_24h": cfg.OpenAIMaxCalls24H,
"max_cost_24h_usd": cfg.OpenAIMaxCost24HUSD,
"reserve_per_call_usd": cfg.OpenAIBudgetReserveUSD,
},
"cost_method": "estimated from provider-reported token usage using pinned OpenAI standard public token rates; not invoice reconciliation",
"recent": recent,
})
+280 -26
View File
@@ -10,11 +10,14 @@ import (
"log"
"math"
"net/http"
"net/url"
"os"
"path/filepath"
gort "runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"neuralhunt/internal/artifact"
@@ -40,23 +43,69 @@ type Server struct {
lottery *guessLottery
adminUser, adminPass, staticDir, artifactDir string
upgrader websocket.Upgrader
wsAllowedOrigins map[string]struct{}
maxUserWS, maxLeaderboardWS int64
userWSCount, leaderboardWSCount atomic.Int64
adminSessions sync.Map // sid -> exp unix seconds
}
func New(store *data.Store, a *auth.Manager, sm *settings.Manager, hub *wsx.Hub, runtimeState *rtx.State, artifactDir string, artifactWorker *artifact.Worker) *Server {
return &Server{
store: store,
auth: a,
settings: sm,
hub: hub,
runtime: runtimeState,
artifactWorker: artifactWorker,
lottery: newGuessLottery(),
adminUser: env("ADMIN_USER", "admin"),
adminPass: env("ADMIN_PASSWORD", "change-me"),
staticDir: env("STATIC_DIR", ""),
artifactDir: artifactDir,
upgrader: websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }},
s := &Server{
store: store,
auth: a,
settings: sm,
hub: hub,
runtime: runtimeState,
artifactWorker: artifactWorker,
lottery: newGuessLottery(),
adminUser: env("ADMIN_USER", "admin"),
adminPass: env("ADMIN_PASSWORD", "change-me"),
staticDir: env("STATIC_DIR", ""),
artifactDir: artifactDir,
wsAllowedOrigins: parseOriginAllowlist(os.Getenv("WS_ALLOWED_ORIGINS")),
maxUserWS: int64(envIntServer("WS_MAX_USER_CONNECTIONS", 5000)),
maxLeaderboardWS: int64(envIntServer("WS_MAX_LEADERBOARD_CONNECTIONS", 500)),
}
s.upgrader = websocket.Upgrader{CheckOrigin: s.checkWSOrigin, Subprotocols: []string{"neuralhunt.v1"}}
return s
}
func envIntServer(k string, d int) int {
if raw := strings.TrimSpace(os.Getenv(k)); raw != "" {
if n, err := strconv.Atoi(raw); err == nil && n >= 0 {
return n
}
}
return d
}
func parseOriginAllowlist(raw string) map[string]struct{} {
out := map[string]struct{}{}
for _, item := range strings.Split(raw, ",") {
item = strings.TrimSpace(strings.TrimRight(item, "/"))
if item != "" {
out[strings.ToLower(item)] = struct{}{}
}
}
return out
}
func (s *Server) checkWSOrigin(r *http.Request) bool {
origin := strings.TrimSpace(r.Header.Get("Origin"))
if origin == "" {
// Native clients do not normally send Origin. Authentication and global
// connection caps still apply; browser cross-site websockets do send it.
return true
}
u, err := url.Parse(origin)
if err != nil || u.Host == "" {
return false
}
if strings.EqualFold(u.Host, r.Host) {
return true
}
_, ok := s.wsAllowedOrigins[strings.ToLower(strings.TrimRight(origin, "/"))]
return ok
}
func env(k, d string) string {
@@ -130,9 +179,53 @@ func (s *Server) bearer(r *http.Request) (auth.Claims, error) {
return s.auth.Parse(strings.TrimPrefix(h, "Bearer "))
}
const adminSessionCookie = "neuralhunt_admin_session"
func adminCookieSecure(r *http.Request) bool {
raw := strings.ToLower(strings.TrimSpace(os.Getenv("ADMIN_COOKIE_SECURE")))
switch raw {
case "0", "false", "no", "off":
return false
case "auto":
if r.TLS != nil {
return true
}
return strings.EqualFold(strings.TrimSpace(strings.Split(r.Header.Get("X-Forwarded-Proto"), ",")[0]), "https")
default:
// Public deployments should terminate HTTPS in front of this listener.
// Defaulting to Secure avoids a proxy-header mistake silently weakening
// the admin session. Local plain-HTTP development can set false explicitly.
return true
}
}
func (s *Server) adminCookieClaims(r *http.Request) (auth.Claims, error) {
cookie, err := r.Cookie(adminSessionCookie)
if err != nil || strings.TrimSpace(cookie.Value) == "" {
return auth.Claims{}, errors.New("missing admin session")
}
c, err := s.auth.Parse(cookie.Value)
if err != nil || c.Role != "admin" || c.SessionID == "" {
return auth.Claims{}, errors.New("invalid admin session")
}
expRaw, ok := s.adminSessions.Load(c.SessionID)
exp, typed := expRaw.(int64)
if !ok || !typed || exp < time.Now().Unix() {
s.adminSessions.Delete(c.SessionID)
return auth.Claims{}, errors.New("admin session revoked or expired")
}
return c, nil
}
func (s *Server) require(role string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := s.bearer(r)
var c auth.Claims
var err error
if role == "admin" {
c, err = s.adminCookieClaims(r)
} else {
c, err = s.bearer(r)
}
if err != nil || (role != "" && c.Role != role) {
jsonOut(w, 401, map[string]string{"error": "unauthorized"})
return
@@ -143,15 +236,52 @@ func (s *Server) require(role string, next http.Handler) http.Handler {
func claims(r *http.Request) auth.Claims { return r.Context().Value(claimsKey).(auth.Claims) }
// PublicRoutes is safe to expose to the Internet. The admin UI and every
// /api/admin endpoint are deliberately absent from this listener.
func (s *Server) PublicRoutes() http.Handler {
next := s.Routes()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := strings.ToLower(r.URL.Path)
if p == "/admin" || strings.HasPrefix(p, "/admin/") || p == "/api/admin" || strings.HasPrefix(p, "/api/admin/") {
http.NotFound(w, r)
return
}
next.ServeHTTP(w, r)
})
}
// AdminRoutes is intended for the private/VPN listener. It serves only the
// control plane, its static frontend assets and health check.
func (s *Server) AdminRoutes() http.Handler {
next := s.Routes()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := strings.ToLower(r.URL.Path)
if p == "/" {
http.Redirect(w, r, "/admin", http.StatusTemporaryRedirect)
return
}
allowed := p == "/admin" || strings.HasPrefix(p, "/admin/") || p == "/api/healthz" || p == "/api/admin" || strings.HasPrefix(p, "/api/admin/") || p == "/app.js" || p == "/styles.css" || p == "/index.html"
if !allowed {
http.NotFound(w, r)
return
}
next.ServeHTTP(w, r)
})
}
func (s *Server) Routes() http.Handler {
r := chi.NewRouter()
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Referrer-Policy", "no-referrer")
w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
w.Header().Set("Content-Security-Policy", "default-src 'self'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' ws: wss:; font-src 'self' data:; form-action 'self'")
w.Header().Set("Strict-Transport-Security", "max-age=31536000")
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
path := strings.ToLower(req.URL.Path)
if path == "/" || path == "/admin" || path == "/leaderboard" || strings.HasSuffix(path, ".html") || strings.HasSuffix(path, ".js") || strings.HasSuffix(path, ".css") {
if path == "/" || path == "/admin" || path == "/leaderboard" || strings.HasPrefix(path, "/api/admin") || strings.HasPrefix(path, "/api/auth") || strings.HasSuffix(path, ".html") || strings.HasSuffix(path, ".js") || strings.HasSuffix(path, ".css") {
w.Header().Set("Cache-Control", "no-store")
}
next.ServeHTTP(w, req)
@@ -166,6 +296,7 @@ func (s *Server) Routes() http.Handler {
r.Post("/api/auth/challenge", s.challenge)
r.Post("/api/auth/login", s.login)
r.Post("/api/admin/login", s.adminLogin)
r.Post("/api/admin/logout", s.adminLogout)
r.Group(func(r chi.Router) {
r.Use(func(n http.Handler) http.Handler { return s.require("user", n) })
r.Get("/api/tasks", s.clientTasks)
@@ -178,6 +309,7 @@ func (s *Server) Routes() http.Handler {
})
r.Group(func(r chi.Router) {
r.Use(func(n http.Handler) http.Handler { return s.require("admin", n) })
r.Get("/api/admin/session", s.adminSession)
r.Get("/api/admin/overview", s.adminOverview)
r.Get("/api/admin/performance", s.adminPerformance)
r.Get("/api/admin/profiles/cleanup-preview", s.adminProfileCleanupPreview)
@@ -265,20 +397,25 @@ func (s *Server) challenge(w http.ResponseWriter, r *http.Request) {
jsonOut(w, 400, map[string]string{"error": err.Error()})
return
}
c, err := s.auth.NewChallenge(r.Context(), cid)
proofBits := 0
if !s.store.ClientExists(r.Context(), cid) {
proofBits = s.settings.Get().SybilProofOfWorkBits
}
c, err := s.auth.NewChallengeWithProof(r.Context(), cid, proofBits)
if err != nil {
log.Printf("auth challenge for %s: %v", cid, err)
jsonOut(w, 500, map[string]string{"error": "challenge failed"})
return
}
jsonOut(w, 200, map[string]string{"client_id": cid, "challenge": c})
jsonOut(w, 200, map[string]any{"client_id": cid, "challenge": c, "proof_of_work_bits": proofBits})
}
func (s *Server) login(w http.ResponseWriter, r *http.Request) {
var in struct {
PublicJWK auth.PublicJWK `json:"public_jwk"`
Challenge string `json:"challenge"`
Signature string `json:"signature"`
PublicJWK auth.PublicJWK `json:"public_jwk"`
Challenge string `json:"challenge"`
Signature string `json:"signature"`
ProofOfWorkCounter string `json:"proof_of_work_counter"`
}
if err := decodeAuth(r, &in); err != nil {
log.Printf("auth login decode: %v", err)
@@ -295,7 +432,7 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) {
jsonOut(w, 401, map[string]string{"error": "invalid signature"})
return
}
if err = s.auth.ConsumeChallenge(r.Context(), cid, in.Challenge); err != nil {
if err = s.auth.ConsumeChallengeWithProof(r.Context(), cid, in.Challenge, in.ProofOfWorkCounter); err != nil {
log.Printf("auth login challenge for %s: %v", cid, err)
jsonOut(w, 401, map[string]string{"error": err.Error()})
return
@@ -323,13 +460,53 @@ func (s *Server) adminLogin(w http.ResponseWriter, r *http.Request) {
jsonOut(w, 401, map[string]string{"error": "invalid credentials"})
return
}
tok, _, err := s.auth.Issue("admin", "admin", 8*time.Hour)
tok, sessionClaims, err := s.auth.Issue("admin", "admin", 8*time.Hour)
if err != nil {
log.Printf("admin auth issue token: %v", err)
jsonOut(w, 500, map[string]string{"error": "token creation failed"})
jsonOut(w, 500, map[string]string{"error": "session creation failed"})
return
}
jsonOut(w, 200, map[string]string{"token": tok})
nowUnix := time.Now().Unix()
s.adminSessions.Range(func(key, value any) bool {
if exp, ok := value.(int64); !ok || exp < nowUnix {
s.adminSessions.Delete(key)
}
return true
})
s.adminSessions.Store(sessionClaims.SessionID, sessionClaims.Exp)
http.SetCookie(w, &http.Cookie{
Name: adminSessionCookie,
Value: tok,
Path: "/",
MaxAge: int((8 * time.Hour).Seconds()),
HttpOnly: true,
Secure: adminCookieSecure(r),
SameSite: http.SameSiteStrictMode,
})
jsonOut(w, 200, map[string]bool{"ok": true})
}
func (s *Server) adminLogout(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie(adminSessionCookie); err == nil {
if c, err := s.auth.Parse(cookie.Value); err == nil && c.SessionID != "" {
s.adminSessions.Delete(c.SessionID)
}
}
http.SetCookie(w, &http.Cookie{
Name: adminSessionCookie,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: adminCookieSecure(r),
SameSite: http.SameSiteStrictMode,
})
jsonOut(w, 200, map[string]bool{"ok": true})
}
func (s *Server) adminSession(w http.ResponseWriter, r *http.Request) {
c := claims(r)
jsonOut(w, 200, map[string]any{"ok": true, "role": c.Role, "expires_at": time.Unix(c.Exp, 0).UTC()})
}
func taskIntervals(t data.Task, sm settings.Runtime) (int, int) {
@@ -476,6 +653,19 @@ func (s *Server) guess(w http.ResponseWriter, r *http.Request) {
jsonOut(w, 400, false)
return
}
if warmup := s.settings.Get().SybilWarmupSec; warmup > 0 {
created, err := s.store.ClientCreatedAt(r.Context(), c.ClientID)
if err != nil {
jsonOut(w, 401, false)
return
}
readyAt := created.Add(time.Duration(warmup) * time.Second)
if wait := time.Until(readyAt); wait > 0 {
retry := int(math.Ceil(wait.Seconds()))
jsonAPIError(w, http.StatusTooManyRequests, "identity_warmup", "new identity is still in anti-sybil warmup", map[string]any{"retry_after_sec": retry})
return
}
}
t, err := s.store.SecretTask(r.Context(), id)
if err != nil || t.Status != "active" {
jsonAPIError(w, http.StatusConflict, "task_inactive", "task is no longer active", nil)
@@ -751,6 +941,11 @@ func (s *Server) publicArtifactPreview(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) leaderboardWS(w http.ResponseWriter, r *http.Request) {
if !acquireWSCap(&s.leaderboardWSCount, s.maxLeaderboardWS) {
http.Error(w, "leaderboard websocket capacity reached", http.StatusServiceUnavailable)
return
}
defer s.leaderboardWSCount.Add(-1)
conn, err := s.upgrader.Upgrade(w, r, nil)
if err != nil {
return
@@ -760,9 +955,31 @@ func (s *Server) leaderboardWS(w http.ResponseWriter, r *http.Request) {
defer s.hub.Remove(cl)
initial, _ := s.store.LiveLeaderboard(r.Context(), 200)
cl.Enqueue(wsx.Event{Type: "leaderboard", Data: initial})
conn.SetReadLimit(1024)
_ = conn.SetReadDeadline(time.Now().Add(90 * time.Second))
conn.SetPongHandler(func(string) error { return conn.SetReadDeadline(time.Now().Add(90 * time.Second)) })
ping := time.NewTicker(30 * time.Second)
defer ping.Stop()
done := make(chan struct{})
go func() {
defer close(done)
for {
if _, _, err := conn.ReadMessage(); err != nil {
return
}
}
}()
for {
if _, _, err := conn.ReadMessage(); err != nil {
select {
case <-done:
return
case <-r.Context().Done():
return
case <-ping.C:
if err := cl.Ping(); err != nil {
return
}
}
}
}
@@ -1226,8 +1443,40 @@ func (s *Server) adminEnsure(w http.ResponseWriter, r *http.Request) {
jsonOut(w, 200, map[string]bool{"ok": true})
}
func websocketAuthToken(r *http.Request) string {
if h := strings.TrimSpace(r.Header.Get("Authorization")); strings.HasPrefix(h, "Bearer ") {
return strings.TrimSpace(strings.TrimPrefix(h, "Bearer "))
}
for _, part := range strings.Split(r.Header.Get("Sec-WebSocket-Protocol"), ",") {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "nh-auth.") {
return strings.TrimPrefix(part, "nh-auth.")
}
}
if strings.EqualFold(strings.TrimSpace(os.Getenv("WS_ALLOW_QUERY_TOKEN")), "1") {
return r.URL.Query().Get("token")
}
return ""
}
func acquireWSCap(counter *atomic.Int64, max int64) bool {
if max <= 0 {
counter.Add(1)
return true
}
for {
cur := counter.Load()
if cur >= max {
return false
}
if counter.CompareAndSwap(cur, cur+1) {
return true
}
}
}
func (s *Server) ws(w http.ResponseWriter, r *http.Request) {
tok := r.URL.Query().Get("token")
tok := websocketAuthToken(r)
c, err := s.auth.Parse(tok)
if err != nil || c.Role != "user" {
http.Error(w, "unauthorized", 401)
@@ -1247,6 +1496,11 @@ func (s *Server) ws(w http.ResponseWriter, r *http.Request) {
return
}
s.runtime.SetTaskSelection(c.ClientID, t.ID)
if !acquireWSCap(&s.userWSCount, s.maxUserWS) {
http.Error(w, "websocket capacity reached", http.StatusServiceUnavailable)
return
}
defer s.userWSCount.Add(-1)
leaseID, err := s.runtime.AcquirePresence(c.ClientID, c.SessionID)
if err != nil {
http.Error(w, "identity already connected", 409)