carlos / platform Public

feat: claim capabilities split — wildcard (cert parent) + catchall (route) (#151, woodstar coordination)

Two orthogonal additive fields per the woodstar/#82 review: wildcard
opts into delegated-ACME cert parenthood alone; catchall derives the
*.<host> alias and is refused without wildcard at the attach surface.
Declared here for writer-side round-trip; converge preserves both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 086f1504d2e0d82038f3928b1fe72b915195e1fd parent fa5dc0b
2 files changed, +19 −10
  • internal/domains/claims.go +15 −7
  • internal/domains/claims_test.go +4 −3
diff --git a/internal/domains/claims.go b/internal/domains/claims.go
index 51c5abae3..d2689d59a 100644
--- a/internal/domains/claims.go
+++ b/internal/domains/claims.go
@@ -39,14 +39,22 @@ 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 and Catchall are the woodstar-carloku branch's two claim
+ // capabilities (its migration spec §4-5 gives them meaning; nothing in
+ // this package acts on either). They are deliberately orthogonal:
+ // Wildcard opts the domain into a delegated-ACME wildcard cert (one
+ // *.<host> cert covering named aliases, no per-host CT-logged certs);
+ // Catchall derives the *.<host> catch-all alias entry to the primary
+ // route, and the attach surface refuses it without Wildcard (a
+ // catch-all nobody can serve TLS for is a config error, not a state).
+ // Both are declared here so a converge rewrite by this binary
+ // round-trips them instead of stripping fields it doesn't know — the
+ // additive-wire rule applied to writers, not just readers. Converge
+ // preserves only DECLARED fields (json.Unmarshal drops unknown ones),
+ // which is exactly why any future claim field must be added here in
+ // the same change that first writes it.
Wildcard bool `json:"wildcard,omitempty"`
+ Catchall bool `json:"catchall,omitempty"`
}
// ErrHostUnavailable is returned by AttachClaim when host is already claimed
diff --git a/internal/domains/claims_test.go b/internal/domains/claims_test.go
index f542ace87..e4f08a64f 100644
--- a/internal/domains/claims_test.go
+++ b/internal/domains/claims_test.go
@@ -59,7 +59,7 @@ 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",
- CreatedAt: "2026-08-12T00:00:00Z", Wildcard: true}
+ CreatedAt: "2026-08-12T00:00:00Z", Wildcard: true, Catchall: true}
if _, err := AttachClaim(ctx, s, claim); err != nil {
t.Fatal(err)
}
@@ -78,8 +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)
+ if !got.Wildcard || !got.Catchall || got.CreatedAt != "2026-08-12T00:00:00Z" {
+ t.Fatalf("converge must preserve Wildcard+Catchall+CreatedAt, got wildcard=%v catchall=%v created_at=%q",
+ got.Wildcard, got.Catchall, got.CreatedAt)
}
}