| 1 | // Package domains is the platform-domains machinery: the account-sqid |
| 2 | // encoder and the agent's reconcile pass that mints platform domains, stamps |
| 3 | // DNS/TLS health, and materializes bucket account manifests into the |
| 4 | // registry. See docs/superpowers/specs/2026-08-02-custom-domains-design.md. |
| 5 | package domains |
| 6 | |
| 7 | import "strings" |
| 8 | |
| 9 | // sqidAlphabet is FROZEN FOREVER: lowercase base-23 with the lookalikes |
| 10 | // i, l, o removed. Every platform domain ever minted embeds a sqid over this |
| 11 | // alphabet — changing it (or the offset below) re-homes every one of them. |
| 12 | // The golden test (sqid_test.go) exists to make that impossible to do by |
| 13 | // accident. Deliberately not configurable: vanity account names in the |
| 14 | // platform domain invite squatting; custom domains are the vanity mechanism. |
| 15 | const sqidAlphabet = "abcdefghjkmnpqrstuvwxyz" |
| 16 | |
| 17 | // SqidAlphabet exports the frozen alphabet for one purpose: an error message |
| 18 | // that tells someone which letters a sqid may contain (the CLI's -account-id |
| 19 | // refusal) should read those letters from the encoder itself, so it can |
| 20 | // never drift from the truth it is describing. |
| 21 | const SqidAlphabet = sqidAlphabet |
| 22 | |
| 23 | // sqidOffset guarantees a minimum length of 3: encoding id+23² in plain |
| 24 | // base-23 means every id ≥ 1 needs at least three digits, with no padding |
| 25 | // case-split (a single strictly-monotonic bijection, so distinct ids can |
| 26 | // never collide). Frozen with the alphabet, for the same reason. |
| 27 | const sqidOffset = 23 * 23 |
| 28 | |
| 29 | // Sqid encodes an account's registry row id as its frozen domain label — |
| 30 | // e.g. the built-in ops account (id 1) is Sqid(1) forever, on every |
| 31 | // deployment. |
| 32 | func Sqid(id int64) string { |
| 33 | n := id + sqidOffset |
| 34 | var buf [16]byte // 23^16 dwarfs int64; never grows |
| 35 | i := len(buf) |
| 36 | for n > 0 { |
| 37 | i-- |
| 38 | buf[i] = sqidAlphabet[n%23] |
| 39 | n /= 23 |
| 40 | } |
| 41 | return string(buf[i:]) |
| 42 | } |
| 43 | |
| 44 | // ValidSqid reports whether s is shaped like something Sqid could have |
| 45 | // emitted: 3 to 16 letters, every one of them in the frozen alphabet. |
| 46 | // |
| 47 | // This is STRICTER than release.ValidAccount (^[a-z]{3,16}$), and the |
| 48 | // difference is the whole point. ValidAccount gates what may become a bucket |
| 49 | // path segment, so it accepts any lowercase word — including account NAMES |
| 50 | // like "ops" or "amadan". A name is not a sqid: the frozen alphabet omits i, |
| 51 | // l and o, so "ops" is a string Sqid can never produce for any id. Anything |
| 52 | // that means "this value IS an account's identity, matched as a sqid and |
| 53 | // never as a name" — the CLI's -account-id, above all — must ask THIS |
| 54 | // question, because accepting "ops" there resolves to an account that does |
| 55 | // not exist, and writes under it without ever failing (the real ops account |
| 56 | // is Sqid(1), "bab"). |
| 57 | // |
| 58 | // The 3 floor is sqidOffset's guarantee and the 16 ceiling is Sqid's buffer; |
| 59 | // both are frozen with the alphabet. |
| 60 | func ValidSqid(s string) bool { |
| 61 | if len(s) < 3 || len(s) > 16 { |
| 62 | return false |
| 63 | } |
| 64 | for i := 0; i < len(s); i++ { |
| 65 | if !strings.ContainsRune(sqidAlphabet, rune(s[i])) { |
| 66 | return false |
| 67 | } |
| 68 | } |
| 69 | return true |
| 70 | } |
| 71 | |