store+server: P2-18a announce-and-approve schema + endpoint

migration 0011 adds pending_hosts table (id, hostname, public_key,
fingerprint, expiry). store/pending_hosts.go covers full CRUD plus
hostname-collision count + expired-row sweeper.

POST /api/agents/announce takes {hostname, os, arch, agent_version,
restic_version, public_key (base64)}, returns {pending_id,
fingerprint, hostname_collision}. Per-source-IP token-bucket
rate limit (10/min) + global cap of 100 in-flight rows. Public
key must be exactly 32 bytes (Ed25519).
This commit is contained in:
2026-05-04 11:03:41 +01:00
parent a5a2cb91d0
commit cd80be3b13
5 changed files with 654 additions and 1 deletions
+14 -1
View File
@@ -49,6 +49,10 @@ type Server struct {
// sync.Mutex; checked-and-locked atomically via drainLocksMu.
drainLocksMu sync.Mutex
drainLocks map[string]*sync.Mutex
// announceRL is the per-source-IP token-bucket guarding
// POST /api/agents/announce (P2-18). One process-local map.
announceRL *announceLimiter
}
// New builds a configured but not-yet-started server.
@@ -67,7 +71,11 @@ func New(deps Deps) *Server {
w.WriteHeader(stdhttp.StatusNoContent)
})
s := &Server{deps: deps, drainLocks: make(map[string]*sync.Mutex)}
s := &Server{
deps: deps,
drainLocks: make(map[string]*sync.Mutex),
announceRL: newAnnounceLimiter(),
}
s.routes(r)
s.srv = &stdhttp.Server{
@@ -92,6 +100,11 @@ func (s *Server) routes(r chi.Router) {
// Agent enrollment (open endpoint — token is the credential).
r.Post("/agents/enroll", s.handleAgentEnroll)
// Announce-and-approve enrolment (open endpoint — fingerprint
// comparison in the UI is the gate). Per-IP rate-limited and
// globally capped (P2-18).
r.Post("/agents/announce", s.handleAnnounce)
// Operator → server (authenticated). Spec.md §6.1's
// /hosts/{id}/enrollment-token (regenerate) lands when the
// host page can call it; for now just the create endpoint.