carlos / platform Public

ledger: types, keys, and name validation for the open-ledger stream

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 96be71bc6ad3d621374ad8c857f29a2c735b296c parent 114255c
2 files changed, +121 −0
  • internal/ledger/ledger.go +84 −0
  • internal/ledger/ledger_test.go +37 −0
diff --git a/internal/ledger/ledger.go b/internal/ledger/ledger.go
new file mode 100644
index 000000000..47d1f23ad
--- /dev/null
+++ b/internal/ledger/ledger.go
@@ -0,0 +1,84 @@
+// Package ledger — the open-ledger stream primitive: append-only,
+// hash-chained public JSON entries in the deployment bucket, keyed by
+// (account, app) exactly as the fleet page groups routes. The chain
+// hashes STORED entry bytes verbatim: the writer marshals once and the
+// hash commits to exactly those bytes, so verification needs no
+// canonicalization pass in any language.
+// Spec: docs/superpowers/specs/2026-08-03-open-ledgers-design.md §2.
+package ledger
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "regexp"
+ "strings"
+)
+
+const (
+ Schema = 1
+ MaxPayload = 64 << 10
+ MaxBlob = 4 << 20
+)
+
+type Entry struct {
+ Schema int `json:"schema"`
+ Ledger string `json:"ledger"`
+ Writer string `json:"writer"`
+ TS string `json:"ts"`
+ Payload json.RawMessage `json:"payload"`
+ PrevKey string `json:"prev_key"`
+ PrevSHA256 string `json:"prev_sha256"`
+}
+
+type Head struct {
+ Schema int `json:"schema"`
+ UpdatedAt string `json:"updated_at"`
+ HeadKey string `json:"head_key"`
+ HeadSHA256 string `json:"head_sha256"`
+ Count int `json:"count"`
+}
+
+type Manifest struct {
+ Schema int `json:"schema"`
+ Public bool `json:"public"`
+ Ledgers []string `json:"ledgers"`
+ Title string `json:"title"`
+}
+
+var nameRe = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,63}$`)
+
+func ValidName(s string) bool {
+ return nameRe.MatchString(s)
+}
+
+func Prefix(sqid, app, name string) string {
+ return "ledgers/" + sqid + "/" + app + "/" + name + "/"
+}
+
+func HeadKey(sqid, app, name string) string {
+ return Prefix(sqid, app, name) + "HEAD.json"
+}
+
+func ManifestKey(sqid, app string) string {
+ return "ledgers/" + sqid + "/" + app + "/ledgers.json"
+}
+
+func BlobKey(sqid, app, name, sha string) string {
+ return Prefix(sqid, app, name) + "blobs/" + sha
+}
+
+// tsCompact strips the separators RFC3339 puts in a UTC timestamp so entry
+// keys sort chronologically as plain strings.
+func tsCompact(ts string) string {
+ return strings.NewReplacer("-", "", ":", "").Replace(ts)
+}
+
+func EntryKey(sqid, app, name, ts, sha string) string {
+ return Prefix(sqid, app, name) + "entries/" + tsCompact(ts) + "-" + sha[:12] + ".json"
+}
+
+func SHA256Hex(b []byte) string {
+ sum := sha256.Sum256(b)
+ return hex.EncodeToString(sum[:])
+}
diff --git a/internal/ledger/ledger_test.go b/internal/ledger/ledger_test.go
new file mode 100644
index 000000000..9bba8214c
--- /dev/null
+++ b/internal/ledger/ledger_test.go
@@ -0,0 +1,37 @@
+package ledger
+
+import "testing"
+
+func TestValidName(t *testing.T) {
+ for _, ok := range []string{"carbon", "a", "prompt-registry", "x9"} {
+ if !ValidName(ok) {
+ t.Errorf("ValidName(%q) = false, want true", ok)
+ }
+ }
+ for _, bad := range []string{"", "Carbon", "-x", "a_b", "a/b", "café",
+ "0123456789012345678901234567890123456789012345678901234567890123x"} {
+ if ValidName(bad) {
+ t.Errorf("ValidName(%q) = true, want false", bad)
+ }
+ }
+}
+
+func TestKeys(t *testing.T) {
+ if got, want := HeadKey("bab", "platform", "carbon"),
+ "ledgers/bab/platform/carbon/HEAD.json"; got != want {
+ t.Errorf("HeadKey = %q, want %q", got, want)
+ }
+ if got, want := ManifestKey("bab", "platform"),
+ "ledgers/bab/platform/ledgers.json"; got != want {
+ t.Errorf("ManifestKey = %q, want %q", got, want)
+ }
+ if got, want := BlobKey("bab", "platform", "prompts", "abc123"),
+ "ledgers/bab/platform/prompts/blobs/abc123"; got != want {
+ t.Errorf("BlobKey = %q, want %q", got, want)
+ }
+ sha := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+ if got, want := EntryKey("bab", "platform", "carbon", "2026-08-03T21:04:00Z", sha),
+ "ledgers/bab/platform/carbon/entries/20260803T210400Z-0123456789ab.json"; got != want {
+ t.Errorf("EntryKey = %q, want %q", got, want)
+ }
+}