feat: claim converge preserves Wildcard + CreatedAt (#151, woodstar coordination)
The woodstar-carloku branch adds an additive wildcard field to domain claims (its spec §4-5). Declare it here so this binary's converge rewrite round-trips it, and narrow converge to repoint Route only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 files changed,
+29
−4
internal/domains/claims.go+19 −1internal/domains/claims_test.go+10 −3
diff --git a/internal/domains/claims.go b/internal/domains/claims.go| index cf0823f80..51c5abae3 100644 |
| --- a/internal/domains/claims.go |
| +++ b/internal/domains/claims.go |
| @@ -39,6 +39,14 @@ type Claim struct { |
| Route string `json:"route,omitempty"` |
| CreatedAt string `json:"created_at"` |
| CreatedBy string `json:"created_by,omitempty"` |
| + // Wildcard marks a claim whose *.<host> labels also resolve to the |
| + // primary route, with delegated DNS-01 issuance for the wildcard cert |
| + // (woodstar-carloku migration spec §4-5 — that branch gives it meaning; |
| + // nothing in this package acts on it). Declared here so a converge |
| + // rewrite by this binary round-trips the value instead of stripping a |
| + // field it doesn't know — the additive-wire rule applied to writers, |
| + // not just readers. |
| + Wildcard bool `json:"wildcard,omitempty"` |
| } |
| // ErrHostUnavailable is returned by AttachClaim when host is already claimed |
| @@ -101,7 +109,17 @@ func AttachClaim(ctx context.Context, s release.Store, c Claim) (bool, error) { |
| if cur.Route == c.Route { |
| return false, nil |
| } |
| - if _, err := s.Put(ctx, ClaimKey(c.Host), body, etag); err == nil { |
| + // Converge repoints Route and nothing else: the stored claim keeps |
| + // its CreatedAt (the claim's age) and Wildcard (TLS opt-in state a |
| + // route repoint has no business clearing). Writing the caller's |
| + // whole claim here would strip both. |
| + next := *cur |
| + next.Route = c.Route |
| + nbody, err := json.Marshal(next) |
| + if err != nil { |
| + return false, err |
| + } |
| + if _, err := s.Put(ctx, ClaimKey(c.Host), nbody, etag); err == nil { |
| return false, nil |
| } else if errors.Is(err, release.ErrCASConflict) && attempt == 0 { |
| continue // converge race — one retry, then surface |
diff --git a/internal/domains/claims_test.go b/internal/domains/claims_test.go| index 697cfc36c..f542ace87 100644 |
| --- a/internal/domains/claims_test.go |
| +++ b/internal/domains/claims_test.go |
| @@ -58,12 +58,16 @@ func TestAttachClaimIdempotent(t *testing.T) { |
| func TestAttachClaimConvergesRoute(t *testing.T) { |
| s := release.NewDirStore(t.TempDir()) |
| ctx := context.Background() |
| - claim := Claim{Host: "tickets.example.com", Account: "bab", App: "kass", Route: "canary/live"} |
| + claim := Claim{Host: "tickets.example.com", Account: "bab", App: "kass", Route: "canary/live", |
| + CreatedAt: "2026-08-12T00:00:00Z", Wildcard: true} |
| if _, err := AttachClaim(ctx, s, claim); err != nil { |
| t.Fatal(err) |
| } |
| - claim.Route = "canary/next" |
| - created, err := AttachClaim(ctx, s, claim) |
| + // A converge repoints Route and nothing else: the caller's claim here |
| + // deliberately carries no CreatedAt and no Wildcard, and both stored |
| + // values must survive — a route repoint must never strip another |
| + // branch's TLS opt-in or restamp the claim's age. |
| + created, err := AttachClaim(ctx, s, Claim{Host: claim.Host, Account: "bab", App: "kass", Route: "canary/next"}) |
| if err != nil || created { |
| t.Fatalf("AttachClaim route change: created=%v err=%v, want false nil", created, err) |
| } |
| @@ -74,6 +78,9 @@ func TestAttachClaimConvergesRoute(t *testing.T) { |
| if got.Route != "canary/next" { |
| t.Fatalf("Route = %q, want canary/next", got.Route) |
| } |
| + if !got.Wildcard || got.CreatedAt != "2026-08-12T00:00:00Z" { |
| + t.Fatalf("converge must preserve Wildcard+CreatedAt, got wildcard=%v created_at=%q", got.Wildcard, got.CreatedAt) |
| + } |
| } |
| func TestAttachClaimOtherOwnerRefused(t *testing.T) { |