diff --git a/REUSE.md b/REUSE.md index 220fc6d..8f8463e 100644 --- a/REUSE.md +++ b/REUSE.md @@ -14,7 +14,11 @@ | `(*Handler).checkAuthHost` | hub/internal/api/handler.go (~L119) | `(r) (hostID, customerID string, isGlobal, ok bool)` | Bearer auth for agent-facing endpoints (global OR per-host key) | Sibling of checkAuthCustomer — do NOT mix the two token namespaces. Global key requires the host row to already exist (see handleHostReport). | | `(*Handler).handleEvent` + `allowedEventTypes` | hub/internal/api/handler.go (~L1115 / ~L1063) | `POST /api/v1/event` | The ONLY controller→hub structured-event ingest | Unknown `event_type` → 400 (add to the map FIRST). Accepted severities: info/warning/error/critical (critical since v0.31.0); anything else coerces to `"info"` — exact-match lowercase (`"Critical"` coerces). Tests: hub/internal/api/event_test.go. | | `(*Handler).handleHostReport` | hub/internal/api/handler.go (~L464) | `POST /api/v1/host-report` | Agent heartbeat ingest: denorm + guest upsert | Body cap via LimitReader; per-host key enforces `host_id` match (403 on mismatch); `received_at` is the dead-man's-switch. | -| `(*Handler).handleConfigRetrieve` | hub/internal/api/handler.go (~L1484) | `GET /api/v1/config/{id}`, header `X-Retrieval-Password` | Canonical password-gated retrieval endpoint | Constant-time compare vs `cfg.RetrievalPassword`; 404-before-401 ordering. `handleArtifactManifest` mirrors it EXACTLY — keep them in lockstep. | +| `(*Handler).handleConfigRetrieve` | hub/internal/api/handler.go (~L1484) | `GET /api/v1/config/{id}`, header `X-Retrieval-Password` | Canonical password-gated retrieval endpoint | Constant-time compare vs `cfg.RetrievalPassword`; 404-before-401 ordering. `handleArtifactManifest` mirrors it EXACTLY — keep them in lockstep. Also the Day-0 claim entry point: calls `claimEngine.EnsureIssued` + bakes the hash via `configgen.Generate(…, claimState)`. | +| `claim.Engine` | hub/internal/claim/engine.go | `EnsureIssued` / `Resend` / `RequestReset` / `MarkClaimed` (all take `*store.CustomerConfig`) | Customer-claim code engine (v0.50.0, F-4) | Stores `bcrypt(code)` ONLY — plaintext lives just in the email send. `EnsureIssued` is idempotent (never rotates/re-sends an existing row). Wired via `api.SetClaimEngine` + `web.SetClaimEngine`; the `Mailer` seam is `*notify.Dispatcher`. | +| `(*Store).RotateClaimCode` / `GetClaim` / `MarkClaimed` | hub/internal/store/store.go | claim-state CRUD | `customer_claims` row (v0.50.0) | `RotateClaimCode` bumps generation (single active code) + PRESERVES `claimed_at` (reset never un-claims); `MarkClaimed` is set-only. | +| `configgen.Generate` | hub/internal/configgen/configgen.go (~L16) | `(templateYAML string, cfg *store.CustomerConfig, claimState *store.ClaimState) (string, error)` | Generate a customer's controller.yaml | The 3rd arg (nil-safe) bakes `web.claim_code_*`. The REAL config-retrieve path issues+emails first (EnsureIssued); the preview/DR paths bake read-only via `store.GetClaim`. | +| `notify.FormatClaimEmail` / `(*Dispatcher).SendClaimEmail` | hub/internal/notify/{templates,dispatcher}.go | `(kind, customerID, email, domain, code)` | Hungarian claim/reset/claimed emails | `kind` ∈ claim\|reset\|claimed. The dispatcher method IS the `claim.Mailer`. Never log the `code`. | | `writeJSON` | hub/internal/api/dr.go (~L25) | `(w, code int, v any)` | JSON responses in api package | Only used in dr.go so far; prefer it over ad-hoc byte-writes for new endpoints. | | `(*mailRateLimiter).allow` | hub/internal/api/mail.go (~L48) | `(key string) bool` | Per-key token-bucket rate limiting | Refill = perMinute/60 per sec, burst = perMinute; in-memory (lost on restart, accepted). `now` is injectable for tests. | diff --git a/hub/CHANGELOG.md b/hub/CHANGELOG.md index 58f15c1..c3771e9 100644 --- a/hub/CHANGELOG.md +++ b/hub/CHANGELOG.md @@ -1,5 +1,33 @@ # Felhom Hub — Changelog +## v0.50.0 — customer-claim password arc: code engine + email + ACK/config delivery (2026-07-12) + +Hub half of the customer-claim password gate (closes DRILL-day0-vm F-4/F-5; needs controller +v0.122.0). The customer OWNS the dashboard password — the hub generates a one-time claim code, +emails it (Hungarian) to the REGISTERED address, and stores only `bcrypt(code)`. No operator-set +path; the plaintext code exists solely inside the email send (the retrieval-passphrase custody +rule). + +- **`internal/claim`** — the code engine. `EnsureIssued` (idempotent — issue+email at the FIRST + real config retrieve = Day-0, and at a live box's first report; repeated pulls/reports never + rotate or re-send), `Resend` (operator button; rotates generation — unclaimed gets the claim + template, claimed gets the reset template), `RequestReset` (controller-forwarded "Elfelejtett + jelszó", rate-limited 3/day/customer), `MarkClaimed` (set-only; one confirmation email on the + unclaimed→claimed transition). +- **`store.customer_claims`** — per-customer `{code_hash, generation, issued_at, emailed_at, + claimed_at, reset_day, reset_count}`. `RotateClaimCode` bumps the generation (single active + code) and PRESERVES `claimed_at` (a reset never un-claims); `MarkClaimed` is set-only. +- **Delivery**: `GET /api/v1/config/{id}` bakes `web.claim_code_{hash,generation,issued_at}` into + the generated controller.yaml (gate-from-first-boot) and issues the first code; the report ACK + serves the active `claim {code_hash, generation, issued_at}` (allowlisted) and ingests the + controller's `claimed` flag (set-only). `POST /api/v1/claim/reset-request` (self-scoped by the + box's report key). New emails via the notify dispatcher; `claim_lockout` event allowlisted. +- **UI**: the customer page Setup tab shows a claim status chip (Nyitott — kód kiküldve / Claimed) + + a "Kód újraküldése" button (`POST /configs/{id}/claim-resend`) — no plaintext code ever + rendered (there is none to render). +- 15 tests (engine, ACK/config, UI); the arc's red-proofs live in the controller repo (gate) + + here (generation bump, reset non-DoS). + ## v0.49.0 — Edit tab merge (edit-a), scoped auto-refresh, style.css cache-bust (2026-07-12) > The task spec targeted "v0.48.0", but v0.48.0 (app_start_failed, below) had already shipped + diff --git a/hub/internal/web/controller.yaml.default b/hub/internal/web/controller.yaml.default index 8e7da88..d439b6a 100644 --- a/hub/internal/web/controller.yaml.default +++ b/hub/internal/web/controller.yaml.default @@ -38,9 +38,15 @@ system: # --- Web UI --- web: listen: ":8080" - # Bcrypt hash. Empty = first-visit setup prompt. + # Bcrypt hash of the customer's dashboard password (customer-owned; set via the claim flow). + # Empty = not yet claimed → the claim gate serves only the claim page (see claim_code_* below). password_hash: "" session_secret: "" # Auto-generated on first start + # Customer-claim arc (v0.50.0/controller v0.122.0). The hub OVERWRITES these at config generation + # from the active claim state — leave empty here. bcrypt(code) + monotonic generation + issue time. + claim_code_hash: "" + claim_code_generation: 0 + claim_code_issued_at: "" # --- Git synchronization --- git: