carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Package seeds is the shared vocabulary of instances that start from a
2// database (#260 --from, #261 import): seed ids, the snapshot's bucket keys,
3// the states a record's seed moves through, the verify token, and the
4// registry column the box reads. It holds no I/O. Spec:
5// docs/superpowers/specs/2026-10-08-seeded-instances-design.md.
6//
7// It must never import internal/moves (the moves copy gate inventories every
8// changed file that does).
9package seeds
10
11import (
12 "crypto/rand"
13 "encoding/base32"
14 "errors"
15 "fmt"
16 "io"
17 "regexp"
18 "strings"
19
20 "github.com/carlosframework/platform/internal/controlname"
21)
22
23// The two kinds of seed (spec §3.2).
24const (
25 KindReplica = "replica" // --from: a sibling's replica
26 KindImport = "import" // an uploaded snapshot
27)
28
29// The states of spec §3.2.
30const (
31 StateReserving = "reserving"
32 StateAwaitingUpload = "awaiting-upload"
33 StateUploaded = "uploaded"
34 StateReady = "ready"
35 StateSwapped = "swapped"
36 StatePlaced = "placed"
37 StateCommitted = "committed"
38 StateRefused = "refused"
39 StateAborted = "aborted"
40 StateRolledBack = "rolled-back"
41)
42
43// MaxBytes is the largest database either door accepts (spec §4 step 5,
44// §5.1): 4 GiB, under S3's 5 GB single-PUT limit.
45const MaxBytes int64 = 4 << 30
46
47// Prefix is the snapshots' prefix in a region's data bucket, beside
48// instances/ and moves/ (spec §3.1).
49const Prefix = "seeds/"
50
51const snapshotName = "snapshot.db"
52
53// invalidHost stands in for a host or id that failed validation. It is not a
54// host shape (the "!"), so no real seed lives under it: a read of such a key
55// finds nothing and a delete of its prefix removes nothing.
56const invalidHost = "!invalid"
57
58// ErrBadHost and ErrBadID are what Check returns. A door that takes a host
59// or id from a request or a record calls Check first and refuses with them.
60var (
61 ErrBadHost = errors.New("seeds: not a host")
62 ErrBadID = errors.New("seeds: not a seed id")
63)
64
65var idShape = regexp.MustCompile(`^sd_[a-z2-7]{26}$`)
66
67var lowerBase32 = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567").WithPadding(base32.NoPadding)
68
69// randReader is crypto/rand's reader, a variable so a test can fail it.
70var randReader io.Reader = rand.Reader
71
72// NewID mints a seed id: "sd_" and 128 random bits in lowercase base32. An
73// id is never reused (spec §3.2): every cleanup is scoped to one. Ids come
74// only from crypto/rand.
75func NewID() (string, error) {
76 var b [16]byte
77 if _, err := io.ReadFull(randReader, b[:]); err != nil {
78 return "", fmt.Errorf("seeds: mint an id: %w", err)
79 }
80 return "sd_" + lowerBase32.EncodeToString(b[:]), nil
81}
82
83// ValidID reports whether id has NewID's shape. Every id that passes is 29
84// bytes of [a-z2-7_] and so is one clean path element: no slash, backslash
85// or dot.
86func ValidID(id string) bool { return idShape.MatchString(id) }
87
88// Check reports ErrBadHost if host is not a host shape (lowercase DNS labels,
89// so one clean path element with no slash or "..") and ErrBadID if id is not
90// a seed id.
91func Check(host, id string) error {
92 if !controlname.ValidHostShape(host) {
93 return ErrBadHost
94 }
95 if !ValidID(id) {
96 return ErrBadID
97 }
98 return nil
99}
100
101// HostPrefix is every seed of host: seeds/<host>/. A host that is not a host
102// shape yields seeds/!invalid/, which names nothing.
103func HostPrefix(host string) string {
104 if !controlname.ValidHostShape(host) {
105 host = invalidHost
106 }
107 return Prefix + host + "/"
108}
109
110// SnapshotPrefix is one seed's prefix: seeds/<host>/<id>/. Every delete of
111// a snapshot names it (codex R4-1). A host or id that fails Check yields
112// seeds/!invalid/invalid/, so no key built here leaves seeds/<host>/<id>/ and
113// no prefix built here covers more than one seed.
114func SnapshotPrefix(host, id string) string {
115 if Check(host, id) != nil {
116 return HostPrefix("") + "invalid/"
117 }
118 return HostPrefix(host) + id + "/"
119}
120
121// SnapshotKey is one seed's snapshot: seeds/<host>/<id>/snapshot.db.
122func SnapshotKey(host, id string) string { return SnapshotPrefix(host, id) + snapshotName }
123
124// ParseSnapshotKey is SnapshotKey's inverse. Anything else under seeds/ is
125// not a snapshot and does not parse.
126func ParseSnapshotKey(key string) (host, id string, ok bool) {
127 rest, found := strings.CutPrefix(key, Prefix)
128 if !found {
129 return "", "", false
130 }
131 parts := strings.Split(rest, "/")
132 if len(parts) != 3 || parts[2] != snapshotName || Check(parts[0], parts[1]) != nil {
133 return "", "", false
134 }
135 return parts[0], parts[1], true
136}
137