carlos / platform Public

Bind preference state to versioned immutable receipt history

claude agent, operated by paul@keymail.dev 78d95cc67ad77a2da9c759ffe0c3faa8dedbde50 parent a1177cd
2 files changed, +196 −14
  • internal/console/overagepreferencestore.go +76 −14
  • internal/console/overagepreferencestore_test.go +120 −0

CI passed — run details

diff --git a/internal/console/overagepreferencestore.go b/internal/console/overagepreferencestore.go
index f42b4bb7c..2da74e953 100644
--- a/internal/console/overagepreferencestore.go
+++ b/internal/console/overagepreferencestore.go
@@ -3,12 +3,13 @@ package console
import (
"crypto/sha256"
"database/sql"
+ "encoding/binary"
"encoding/hex"
- "encoding/json"
"errors"
"math"
"regexp"
"time"
+ "unicode/utf8"
"github.com/carlosframework/platform/internal/maintenanceguard"
)
@@ -82,6 +83,9 @@ func (s *Store) migrateOveragePreference() error {
)`,
`CREATE TRIGGER IF NOT EXISTS overage_preference_receipts_no_update BEFORE UPDATE ON overage_preference_receipts BEGIN SELECT RAISE(ABORT,'overage_preference_receipt_immutable'); END`,
`CREATE TRIGGER IF NOT EXISTS overage_preference_receipts_no_delete BEFORE DELETE ON overage_preference_receipts BEGIN SELECT RAISE(ABORT,'overage_preference_receipt_immutable'); END`,
+ `CREATE TRIGGER IF NOT EXISTS overage_preference_receipts_no_replace BEFORE INSERT ON overage_preference_receipts
+ WHEN EXISTS(SELECT 1 FROM overage_preference_receipts WHERE operation_id=NEW.operation_id OR (account_id=NEW.account_id AND revision=NEW.revision))
+ BEGIN SELECT RAISE(ABORT,'overage_preference_receipt_immutable'); END`,
} {
if _, err := s.db.Exec(stmt); err != nil {
return err
@@ -99,7 +103,7 @@ func validOverageEvidence(e overageEvidence) bool {
}
func validateOverageChange(c overagePreferenceChange) error {
- if c.AccountID <= 0 || c.Actor == "" || len(c.Actor) > 320 || c.ExpectedRevision < 0 ||
+ if c.AccountID <= 0 || c.Actor == "" || len(c.Actor) > 320 || !utf8.ValidString(c.Actor) || c.ExpectedRevision < 0 ||
!maintenanceguard.ValidID(c.OperationID) {
return errOveragePreferenceInvalid
}
@@ -113,9 +117,25 @@ func validateOverageChange(c overagePreferenceChange) error {
}
func overageChangeDigest(c overagePreferenceChange) string {
- b, _ := json.Marshal(c)
- h := sha256.Sum256(b)
- return hex.EncodeToString(h[:])
+ h := sha256.New()
+ h.Write([]byte("carlos-overage-preference-change-v1\x00"))
+ var n [8]byte
+ binary.BigEndian.PutUint64(n[:], uint64(c.AccountID))
+ h.Write(n[:])
+ binary.BigEndian.PutUint64(n[:], uint64(c.ExpectedRevision))
+ h.Write(n[:])
+ if c.RequestedOn {
+ h.Write([]byte{1})
+ } else {
+ h.Write([]byte{0})
+ }
+ for _, value := range []string{c.Actor, c.OperationID, c.Evidence.TermsVersion,
+ c.Evidence.PriceBook, c.Evidence.Currency, c.Evidence.DisclosureDigest, c.Evidence.OfferDigest} {
+ binary.BigEndian.PutUint64(n[:], uint64(len(value)))
+ h.Write(n[:])
+ h.Write([]byte(value))
+ }
+ return hex.EncodeToString(h.Sum(nil))
}
func scanOveragePreference(row interface{ Scan(...any) error }, accountID int64) (overagePreference, error) {
@@ -134,7 +154,7 @@ func scanOveragePreference(row interface{ Scan(...any) error }, accountID int64)
}
p.RequestedOn = on != 0
p.UpdatedAt = time.Unix(at, 0).UTC()
- if p.Revision <= 0 || (on != 0 && on != 1) || p.Actor == "" ||
+ if p.Revision <= 0 || (on != 0 && on != 1) || p.Actor == "" || !utf8.ValidString(p.Actor) ||
!maintenanceguard.ValidID(p.OperationID) || at < 0 ||
(p.RequestedOn && !validOverageEvidence(p.Evidence)) ||
(!p.RequestedOn && p.Evidence != (overageEvidence{})) {
@@ -157,14 +177,58 @@ func scanOverageReceipt(row interface{ Scan(...any) error }) (overagePreferenceR
return r, digest, err
}
if !maintenanceguard.ValidID(r.OperationID) || r.AccountID <= 0 || r.Actor == "" ||
- !overageDigest.MatchString(digest) || r.Revision <= 0 || (on != 0 && on != 1) || at < 0 ||
+ !utf8.ValidString(r.Actor) || !overageDigest.MatchString(digest) || r.Revision <= 0 || (on != 0 && on != 1) || at < 0 ||
(r.RequestedOn && !validOverageEvidence(r.Evidence)) ||
(!r.RequestedOn && r.Evidence != (overageEvidence{})) {
return overagePreferenceReceipt{}, "", errOveragePreferenceCorrupt
}
+ reconstructed := overagePreferenceChange{AccountID: r.AccountID, Actor: r.Actor,
+ ExpectedRevision: r.Revision - 1, RequestedOn: r.RequestedOn,
+ OperationID: r.OperationID, Evidence: r.Evidence}
+ if overageChangeDigest(reconstructed) != digest {
+ return overagePreferenceReceipt{}, "", errOveragePreferenceCorrupt
+ }
return r, digest, nil
}
+func loadOveragePreferenceTx(tx *sql.Tx, accountID int64) (overagePreference, error) {
+ p, err := scanOveragePreference(tx.QueryRow(`SELECT revision,requested_on,terms_version,price_book,currency,disclosure_digest,offer_digest,actor,operation_id,updated_at
+ FROM overage_preferences WHERE account_id=?`, accountID), accountID)
+ if err != nil {
+ return overagePreference{}, err
+ }
+ if p.Revision == 0 {
+ var receipts int
+ if err := tx.QueryRow(`SELECT count(*) FROM overage_preference_receipts WHERE account_id=?`, accountID).Scan(&receipts); err != nil {
+ return overagePreference{}, err
+ }
+ if receipts != 0 {
+ return overagePreference{}, errOveragePreferenceCorrupt
+ }
+ return p, nil
+ }
+ var latestRevision sql.NullInt64
+ if err := tx.QueryRow(`SELECT max(revision) FROM overage_preference_receipts WHERE account_id=?`, accountID).Scan(&latestRevision); err != nil {
+ return overagePreference{}, err
+ }
+ if !latestRevision.Valid || latestRevision.Int64 != p.Revision {
+ return overagePreference{}, errOveragePreferenceCorrupt
+ }
+ r, _, err := scanOverageReceipt(tx.QueryRow(`SELECT operation_id,account_id,actor,request_digest,revision,requested_on,terms_version,price_book,currency,disclosure_digest,offer_digest,changed_at
+ FROM overage_preference_receipts WHERE operation_id=?`, p.OperationID))
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return overagePreference{}, errOveragePreferenceCorrupt
+ }
+ return overagePreference{}, err
+ }
+ if r.AccountID != p.AccountID || r.Actor != p.Actor || r.Revision != p.Revision ||
+ r.RequestedOn != p.RequestedOn || r.Evidence != p.Evidence || !r.ChangedAt.Equal(p.UpdatedAt) {
+ return overagePreference{}, errOveragePreferenceCorrupt
+ }
+ return p, nil
+}
+
func authorizeOverageOwnerTx(tx *sql.Tx, accountID int64, actor string) error {
if _, err := lockMembershipWritesTx(tx); err != nil {
return err
@@ -195,8 +259,7 @@ func (s *Store) getOveragePreference(accountID int64, actor string) (overagePref
if err := authorizeOverageOwnerTx(tx, accountID, actor); err != nil {
return overagePreference{}, err
}
- return scanOveragePreference(tx.QueryRow(`SELECT revision,requested_on,terms_version,price_book,currency,disclosure_digest,offer_digest,actor,operation_id,updated_at
- FROM overage_preferences WHERE account_id=?`, accountID), accountID)
+ return loadOveragePreferenceTx(tx, accountID)
}
func (s *Store) changeOveragePreference(c overagePreferenceChange, now time.Time) (overagePreferenceReceipt, error) {
@@ -213,6 +276,10 @@ func (s *Store) changeOveragePreference(c overagePreferenceChange, now time.Time
if err := authorizeOverageOwnerTx(tx, c.AccountID, c.Actor); err != nil {
return overagePreferenceReceipt{}, err
}
+ current, err := loadOveragePreferenceTx(tx, c.AccountID)
+ if err != nil {
+ return overagePreferenceReceipt{}, err
+ }
digest := overageChangeDigest(c)
r, storedDigest, err := scanOverageReceipt(tx.QueryRow(`SELECT operation_id,account_id,actor,request_digest,revision,requested_on,terms_version,price_book,currency,disclosure_digest,offer_digest,changed_at
FROM overage_preference_receipts WHERE operation_id=?`, c.OperationID))
@@ -225,11 +292,6 @@ func (s *Store) changeOveragePreference(c overagePreferenceChange, now time.Time
if !errors.Is(err, sql.ErrNoRows) {
return overagePreferenceReceipt{}, err
}
- current, err := scanOveragePreference(tx.QueryRow(`SELECT revision,requested_on,terms_version,price_book,currency,disclosure_digest,offer_digest,actor,operation_id,updated_at
- FROM overage_preferences WHERE account_id=?`, c.AccountID), c.AccountID)
- if err != nil {
- return overagePreferenceReceipt{}, err
- }
if current.Revision != c.ExpectedRevision {
return overagePreferenceReceipt{}, errOveragePreferenceStale
}
diff --git a/internal/console/overagepreferencestore_test.go b/internal/console/overagepreferencestore_test.go
index 5c1215d34..5eb4dc7a4 100644
--- a/internal/console/overagepreferencestore_test.go
+++ b/internal/console/overagepreferencestore_test.go
@@ -128,6 +128,10 @@ func TestOveragePreferenceEvidenceAndCorruptRow(t *testing.T) {
if _, err := s.changeOveragePreference(c, now); !errors.Is(err, errOveragePreferenceInvalid) {
t.Fatalf("invalid evidence error = %v", err)
}
+ badActor := overageChange(42, string([]byte{0xff}), 0, false, 41)
+ if _, err := s.changeOveragePreference(badActor, now); !errors.Is(err, errOveragePreferenceInvalid) {
+ t.Fatalf("invalid UTF-8 actor error = %v", err)
+ }
c = overageChange(42, overageOwner, 0, false, 8)
c.Evidence = overageEvidenceForTest()
if _, err := s.changeOveragePreference(c, now); !errors.Is(err, errOveragePreferenceInvalid) {
@@ -142,6 +146,110 @@ func TestOveragePreferenceEvidenceAndCorruptRow(t *testing.T) {
}
}
+func TestOveragePreferenceReceiptAndCurrentMustAgree(t *testing.T) {
+ now := time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC)
+ newFixture := func(t *testing.T, op byte) (*Store, overagePreferenceChange) {
+ t.Helper()
+ s := newOverageStore(t, filepath.Join(t.TempDir(), "console.db"))
+ seedOverageAccount(t, s, 42, overageOwner, now)
+ c := overageChange(42, overageOwner, 0, true, op)
+ if _, err := s.changeOveragePreference(c, now); err != nil {
+ t.Fatal(err)
+ }
+ return s, c
+ }
+ t.Run("receipt digest", func(t *testing.T) {
+ s, c := newFixture(t, 42)
+ if _, err := s.db.Exec(`DROP TRIGGER overage_preference_receipts_no_update`); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.db.Exec(`UPDATE overage_preference_receipts SET request_digest=? WHERE operation_id=?`,
+ "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", c.OperationID); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.changeOveragePreference(c, now.Add(time.Second)); !errors.Is(err, errOveragePreferenceCorrupt) {
+ t.Fatalf("corrupt receipt replay error = %v", err)
+ }
+ })
+ t.Run("missing current", func(t *testing.T) {
+ s, _ := newFixture(t, 43)
+ if _, err := s.db.Exec(`DELETE FROM overage_preferences WHERE account_id=42`); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.getOveragePreference(42, overageOwner); !errors.Is(err, errOveragePreferenceCorrupt) {
+ t.Fatalf("orphan receipt error = %v", err)
+ }
+ })
+ t.Run("current mismatch", func(t *testing.T) {
+ s, _ := newFixture(t, 44)
+ if _, err := s.db.Exec(`UPDATE overage_preferences SET updated_at=updated_at+1 WHERE account_id=42`); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.getOveragePreference(42, overageOwner); !errors.Is(err, errOveragePreferenceCorrupt) {
+ t.Fatalf("current mismatch error = %v", err)
+ }
+ })
+ t.Run("current names older valid receipt", func(t *testing.T) {
+ s, enable := newFixture(t, 46)
+ r1, err := s.changeOveragePreference(enable, now)
+ if err != nil {
+ t.Fatal(err)
+ }
+ disable := overageChange(42, overageOwner, 1, false, 47)
+ if _, err := s.changeOveragePreference(disable, now.Add(time.Second)); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.db.Exec(`UPDATE overage_preferences SET revision=?,requested_on=?,terms_version=?,price_book=?,currency=?,disclosure_digest=?,offer_digest=?,actor=?,operation_id=?,updated_at=? WHERE account_id=42`,
+ r1.Revision, r1.RequestedOn, r1.Evidence.TermsVersion, r1.Evidence.PriceBook, r1.Evidence.Currency,
+ r1.Evidence.DisclosureDigest, r1.Evidence.OfferDigest, r1.Actor, r1.OperationID, r1.ChangedAt.Unix()); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.getOveragePreference(42, overageOwner); !errors.Is(err, errOveragePreferenceCorrupt) {
+ t.Fatalf("older current get error = %v", err)
+ }
+ if _, err := s.changeOveragePreference(overageChange(42, overageOwner, 1, false, 48), now.Add(2*time.Second)); !errors.Is(err, errOveragePreferenceCorrupt) {
+ t.Fatalf("older current change error = %v", err)
+ }
+ var currentRevision, receipts int64
+ if err := s.db.QueryRow(`SELECT revision FROM overage_preferences WHERE account_id=42`).Scan(&currentRevision); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.db.QueryRow(`SELECT count(*) FROM overage_preference_receipts WHERE account_id=42`).Scan(&receipts); err != nil {
+ t.Fatal(err)
+ }
+ if currentRevision != 1 || receipts != 2 {
+ t.Fatalf("corrupt attempt wrote state: revision=%d receipts=%d", currentRevision, receipts)
+ }
+ })
+}
+
+func TestOveragePreferenceReceiptsRejectEveryReplacementRoute(t *testing.T) {
+ now := time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC)
+ s := newOverageStore(t, filepath.Join(t.TempDir(), "console.db"))
+ seedOverageAccount(t, s, 42, overageOwner, now)
+ c := overageChange(42, overageOwner, 0, true, 45)
+ if _, err := s.changeOveragePreference(c, now); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.db.Exec(`PRAGMA recursive_triggers=OFF`); err != nil {
+ t.Fatal(err)
+ }
+ for name, stmt := range map[string]string{
+ "update": `UPDATE overage_preference_receipts SET actor='changed' WHERE operation_id='` + c.OperationID + `'`,
+ "delete": `DELETE FROM overage_preference_receipts WHERE operation_id='` + c.OperationID + `'`,
+ "replace": `INSERT OR REPLACE INTO overage_preference_receipts SELECT * FROM overage_preference_receipts WHERE operation_id='` + c.OperationID + `'`,
+ } {
+ t.Run(name, func(t *testing.T) {
+ if _, err := s.db.Exec(stmt); err == nil {
+ t.Fatal("immutable receipt mutation succeeded")
+ }
+ })
+ }
+ if _, err := s.changeOveragePreference(c, now.Add(time.Second)); err != nil {
+ t.Fatalf("receipt changed after rejected mutations: %v", err)
+ }
+}
+
func TestOveragePreferenceReplayNeverReappliesHistoricalState(t *testing.T) {
now := time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC)
s := newOverageStore(t, filepath.Join(t.TempDir(), "console.db"))
@@ -189,6 +297,18 @@ func TestOveragePreferenceConflictsAndClock(t *testing.T) {
if _, err := s.changeOveragePreference(overageChange(42, overageOwner, 1, false, 13), now); !errors.Is(err, errOveragePreferenceClock) {
t.Fatalf("clock rollback error = %v", err)
}
+ overflowCurrent := c
+ overflowCurrent.ExpectedRevision = math.MaxInt64 - 1
+ if _, err := s.db.Exec(`DROP TRIGGER overage_preference_receipts_no_update`); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.db.Exec(`UPDATE overage_preference_receipts SET revision=?,request_digest=? WHERE operation_id=?`,
+ int64(math.MaxInt64), overageChangeDigest(overflowCurrent), c.OperationID); err != nil {
+ t.Fatal(err)
+ }
+ if err := s.migrateOveragePreference(); err != nil {
+ t.Fatal(err)
+ }
if _, err := s.db.Exec(`UPDATE overage_preferences SET revision=? WHERE account_id=42`, int64(math.MaxInt64)); err != nil {
t.Fatal(err)
}