carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// Package ledger — the open-ledger stream primitive: append-only,
2// hash-chained public JSON entries in the deployment bucket, keyed by
3// (account, app) exactly as the fleet page groups routes. The chain
4// hashes STORED entry bytes verbatim: the writer marshals once and the
5// hash commits to exactly those bytes, so verification needs no
6// canonicalization pass in any language.
7// Spec: docs/superpowers/specs/2026-08-03-open-ledgers-design.md §2.
8package ledger
9
10import (
11 "crypto/sha256"
12 "encoding/hex"
13 "encoding/json"
14 "regexp"
15 "strings"
16
17 "github.com/carlosframework/platform/internal/release"
18)
19
20const (
21 Schema = 1
22 MaxPayload = 64 << 10
23 MaxBlob = 4 << 20
24)
25
26type Entry struct {
27 Schema int `json:"schema"`
28 Ledger string `json:"ledger"`
29 Writer string `json:"writer"`
30 TS string `json:"ts"`
31 Payload json.RawMessage `json:"payload"`
32 PrevKey string `json:"prev_key"`
33 PrevSHA256 string `json:"prev_sha256"`
34}
35
36type Head struct {
37 Schema int `json:"schema"`
38 UpdatedAt string `json:"updated_at"`
39 HeadKey string `json:"head_key"`
40 HeadSHA256 string `json:"head_sha256"`
41 Count int `json:"count"`
42}
43
44type Manifest struct {
45 Schema int `json:"schema"`
46 Public bool `json:"public"`
47 Ledgers []string `json:"ledgers"`
48 Title string `json:"title"`
49}
50
51var nameRe = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,63}$`)
52
53func ValidName(s string) bool {
54 return nameRe.MatchString(s)
55}
56
57func Prefix(sqid, app, name string) string {
58 return release.LedgerAppPrefix(sqid, app) + name + "/"
59}
60
61func HeadKey(sqid, app, name string) string {
62 return Prefix(sqid, app, name) + "HEAD.json"
63}
64
65func ManifestKey(sqid, app string) string {
66 return release.LedgerAppPrefix(sqid, app) + "ledgers.json"
67}
68
69func BlobKey(sqid, app, name, sha string) string {
70 return Prefix(sqid, app, name) + "blobs/" + sha
71}
72
73// tsCompact strips the separators RFC3339 puts in a UTC timestamp so entry
74// keys sort chronologically as plain strings.
75func tsCompact(ts string) string {
76 return strings.NewReplacer("-", "", ":", "").Replace(ts)
77}
78
79func EntryKey(sqid, app, name, ts, sha string) string {
80 return Prefix(sqid, app, name) + "entries/" + tsCompact(ts) + "-" + sha[:12] + ".json"
81}
82
83func SHA256Hex(b []byte) string {
84 sum := sha256.Sum256(b)
85 return hex.EncodeToString(sum[:])
86}
87