hub: customer-claim password arc parts 1+2 — code engine, emails, ACK, configgen bake, UI (v0.50.0)
Closes DRILL-day0-vm F-4 hub-side: per-customer claim state (customer_claims,
bcrypt-only custody), the claim engine (issue at real config retrieve = Day-0
bake; first-report issue for live boxes; resend rotates generation; reset
rate-limited 3/day), three Hungarian emails via the dispatcher, report-ACK
claim object {code_hash, generation, issued_at} + set-only claimed ingest,
web.claim_code_* baked into generated controller.yaml, Setup-tab status chip
+ resend button, POST /api/v1/claim/reset-request (self-scoped), claim_lockout
event allowlisted. 13 new tests; full repo green.
Claude-Session: https://claude.ai/code/session_01NptTCFtu7dz2Ru89qHRagN
This commit is contained in:
@@ -13,7 +13,11 @@ import (
|
||||
// Generate takes the template YAML and a customer config,
|
||||
// then produces a complete controller.yaml with customer-specific values
|
||||
// merged in. The returned string is valid YAML ready for deployment.
|
||||
func Generate(templateYAML string, cfg *store.CustomerConfig) (string, error) {
|
||||
//
|
||||
// claimState (v0.50.0, nil-safe): when present, the ACTIVE claim-code bcrypt hash + generation
|
||||
// are baked into web.claim_code_* so a Day-0 box is claim-gated from its FIRST boot (the
|
||||
// controller's precedence: a set password always wins; the hash alone never overrides one).
|
||||
func Generate(templateYAML string, cfg *store.CustomerConfig, claimState *store.ClaimState) (string, error) {
|
||||
// Parse template into generic map
|
||||
var base map[string]interface{}
|
||||
if err := yaml.Unmarshal([]byte(templateYAML), &base); err != nil {
|
||||
@@ -53,6 +57,14 @@ func Generate(templateYAML string, cfg *store.CustomerConfig) (string, error) {
|
||||
}
|
||||
setNested(base, []string{"web", "session_secret"}, sessionSecret)
|
||||
|
||||
// Customer-claim arc (v0.50.0): bake the active claim-code hash so the gate is armed from
|
||||
// first boot. bcrypt only — the plaintext code never reaches any config.
|
||||
if claimState != nil && claimState.CodeHash != "" {
|
||||
setNested(base, []string{"web", "claim_code_hash"}, claimState.CodeHash)
|
||||
setNested(base, []string{"web", "claim_code_generation"}, claimState.Generation)
|
||||
setNested(base, []string{"web", "claim_code_issued_at"}, claimState.IssuedAt.UTC().Format(time.RFC3339))
|
||||
}
|
||||
|
||||
// Marshal back to YAML
|
||||
out, err := yaml.Marshal(base)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestGenerate_DebugLevelOverride(t *testing.T) {
|
||||
// Override present → the generated YAML carries level: debug, and the info default is gone.
|
||||
dbg, err := Generate(tmpl, &store.CustomerConfig{
|
||||
CustomerID: "c1", ConfigJSON: `{"logging":{"level":"debug"}}`,
|
||||
})
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("generate (debug): %v", err)
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func TestGenerate_DebugLevelOverride(t *testing.T) {
|
||||
}
|
||||
|
||||
// No override → the template default (info) stands, debug absent.
|
||||
def, err := Generate(tmpl, &store.CustomerConfig{CustomerID: "c1", ConfigJSON: "{}"})
|
||||
def, err := Generate(tmpl, &store.CustomerConfig{CustomerID: "c1", ConfigJSON: "{}"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("generate (default): %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user