feat(hub): v0.57.0 reinstall-of-existing-customer arc — claim/offsite/escrow continuity

F2 claim re-issue on clean-slate re-enroll (ReissueForReenroll, host-enroll mint path,
single-bump, reset code; hub never stores the password so fork B). F3 offsite re-issue on
re-enroll (ReissueOffsiteForCustomer, same machinery as the manual button). 2.3 escrow honesty
(red-proofed): re-issuing offsite marks the escrow stale (MarkEscrowStale), withholds the
mismatched restic hash from auto-confirm, DR checklist shows stale not done. Events:
claim_reissued_reenroll / offsite_reissued / escrow_stale.

Controller + scripts unchanged (source contradicted both premises): the controller reads escrow
prereqs live from the agent; the installer can't know the descriptor-provisioned storage id. F4
root fix is agent-side -> ROADMAP R-22; demo unblocked live (Part 0 ACL grant). VALIDATION doc
F2 erratum + F3/F4 dispositions. Green gate + Scenario-C red-proof pass.
This commit is contained in:
2026-07-16 18:00:13 +02:00
parent dd961a66bb
commit 7747a16ff1
15 changed files with 395 additions and 37 deletions
+50
View File
@@ -139,6 +139,56 @@ func TestResend_ClaimedGetsResetTemplateAndStaysClaimed(t *testing.T) {
}
}
// v0.57.0 (F2) — ReissueForReenroll rotates + emails a RESET code for a CLAIMED customer whose box
// was clean-slate reinstalled (fresh box has no password), and is a NO-OP for an unclaimed customer
// (the first-provision path, where EnsureIssued owns the first code — re-enrolling must not rotate).
func TestReissueForReenroll(t *testing.T) {
t.Run("claimed rotates and sends the reset template", func(t *testing.T) {
e, st, m := newTestEngine(t)
if _, err := e.EnsureIssued(cust()); err != nil {
t.Fatalf("EnsureIssued: %v", err)
}
if err := e.MarkClaimed(cust()); err != nil {
t.Fatalf("MarkClaimed: %v", err)
}
sendsBefore := len(m.sends)
gen, reissued, err := e.ReissueForReenroll(cust())
if err != nil {
t.Fatalf("ReissueForReenroll: %v", err)
}
if !reissued {
t.Fatal("a CLAIMED customer must re-issue a code on box re-enrollment")
}
cs, _ := st.GetClaim("c1")
if gen < 2 || cs.Generation != gen {
t.Fatalf("re-enroll must bump the generation once: gen=%d stored=%d", gen, cs.Generation)
}
if !cs.Claimed() {
t.Fatal("re-issue must NEVER un-claim (reset rides rotation)")
}
if len(m.sends) != sendsBefore+1 || !strings.HasPrefix(m.sends[len(m.sends)-1], "reset:") {
t.Fatalf("claimed re-enroll must send exactly one RESET email, got %v", m.sends)
}
})
t.Run("unclaimed is a no-op (first-provision path)", func(t *testing.T) {
e, _, m := newTestEngine(t)
if _, err := e.EnsureIssued(cust()); err != nil { // issued but NOT claimed
t.Fatalf("EnsureIssued: %v", err)
}
sendsBefore := len(m.sends)
_, reissued, err := e.ReissueForReenroll(cust())
if err != nil {
t.Fatalf("ReissueForReenroll: %v", err)
}
if reissued {
t.Fatal("an UNCLAIMED customer must NOT re-issue on re-enroll (first provision owns the code)")
}
if len(m.sends) != sendsBefore {
t.Fatalf("no email may be sent on an unclaimed re-enroll, got %v", m.sends)
}
})
}
// RequestReset caps at 3/day per customer, hub-side.
func TestRequestReset_DailyCap(t *testing.T) {
e, _, m := newTestEngine(t)