package report import "log" // Customer-claim arc (v0.122.0, F-4) — the ACK-side of the claim-code delivery. The hub serves // the ACTIVE code's bcrypt hash + monotonic generation on every report ACK of a managed // customer; this reconciler caches it into settings.json IDEMPOTENTLY BY GENERATION (the // offsite-descriptor guard shape: an unchanged generation never rewrites, a hub outage never // clears — set/refresh only, exactly like the escrow confirmer's one-way rules). The gate itself // (internal/web) reads the cached state; nothing here decides gating. // ClaimStatus mirrors the hub ACK's `claim` object (nil when the hub has no claim row / old hub). type ClaimStatus struct { CodeHash string `json:"code_hash"` Generation int `json:"generation"` IssuedAt string `json:"issued_at"` // RFC3339 } // ClaimSettings is the settings surface the sync needs (satisfied by *settings.Settings). type ClaimSettings interface { GetClaimCode() (hash string, generation int, issuedAt string) SetClaimCode(hash string, generation int, issuedAt string) error } // ClaimSync applies one ACK's claim status to the settings cache. type ClaimSync struct { Settings ClaimSettings Logger *log.Logger } func (c *ClaimSync) logf(f string, a ...any) { if c.Logger != nil { c.Logger.Printf(f, a...) } } // Reconcile caches a newer-generation code state; same-or-older generations and nil/empty // statuses are no-ops (a rotation is the ONLY thing that moves the cache — no write-back, no // clearing on hub silence). func (c *ClaimSync) Reconcile(cs *ClaimStatus) { if cs == nil || cs.CodeHash == "" || cs.Generation <= 0 { return } _, curGen, _ := c.Settings.GetClaimCode() if cs.Generation <= curGen { return // idempotent: this generation (or a newer one) is already cached } if err := c.Settings.SetClaimCode(cs.CodeHash, cs.Generation, cs.IssuedAt); err != nil { c.logf("[ERROR] [claim-sync] caching hub claim code (gen %d) failed (retries next ACK): %v", cs.Generation, err) return } c.logf("[INFO] [claim-sync] hub claim code cached (generation %d) — hash first 8: %.8s…", cs.Generation, cs.CodeHash) }