carlos / platform Public

domains: account creation skips ids whose sqid is a reserved label (#285)

Flagship app hosts are <app>.<sqid>.oncarlos.com, and flagship's zone now
carries *.dev.oncarlos.com for the dev deployment. Sqid(1168) is "dev"
(the discussion said 1169, which is "dew"), so account 1168's apps would
have resolved to dev's edge. Paul's ruling 2026-10-14: skip such ids at
creation; the alphabet and offset stay frozen; existing accounts are
untouched.

- internal/domains/reserved.go holds the list (dev, edge, www) with where
  each label comes from, plus IsReservedSqid and ReservedSqids.
- NextAccountID steps over reserved ids. It is the one allocator: every
  creation path (console landing mint, POST /accounts/new,
  POST /auth/cli/accounts, carlos accounts create --owner) reaches it
  through MintAccount. The skip is a pure function of the id, so it needs
  no stored state, is the same on every minter, survives restarts, and a
  skipped id ends up below the highest claim where nothing allocates.
- console.MaterializeAccounts logs a live account holding a reserved sqid,
  so a list change over an existing account shows up at console start.
- Tests: producibility of every listed label, the skip across 1168 seeded
  with one manifest, the create-only race at the skip, five concurrent
  mints, a fresh store over the same bucket, each console door, the CLI
  bucket path, the materialization log line, and a census of the
  creation call sites.
- Plan 14 census: new envelope-only overlay plan14-root-reads-reserved285
  loaded after zddr4; earlier overlays unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 08cb0b2063863050d7094044fae5ade86d7f6ae1 parent ebddcfc
8 files changed, +620 −1
  • auth_e2e_test.go +38 −0
  • internal/console/materialize.go +9 −0
  • internal/console/reserved_sqids_test.go +140 −0
  • internal/domains/mint.go +8 −1
  • internal/domains/reserved.go +76 −0
  • internal/domains/reserved_test.go +340 −0
  • internal/provision/plan14_root_reads_test.go +6 −0
  • internal/provision/testdata/plan14-root-reads-reserved285.tsv +3 −0

CI cancelled — superseded by a newer push — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.013s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m

amadan: job cancelled — a newer push superseded it
diff --git a/auth_e2e_test.go b/auth_e2e_test.go
index 38bc15301..b754890d1 100644
--- a/auth_e2e_test.go
+++ b/auth_e2e_test.go
@@ -225,6 +225,44 @@ func TestAccountsCreateWithOwnerFlag(t *testing.T) {
}
}
+// The headless path mints through the same allocator as the console, so it
+// steps over a reserved sqid too (#285): with 1167 the highest account in the
+// bucket, the next account is 1169 ("dew"), never 1168 ("dev").
+func TestAccountsCreateSkipsAReservedSqid(t *testing.T) {
+ bin := buildCarlos(t)
+ tmp := t.TempDir()
+ storeDir := filepath.Join(tmp, "store")
+ store := release.NewDirStore(storeDir)
+ if _, err := store.Put(t.Context(), domains.AccountManifestKey(domains.Sqid(1167)),
+ []byte(`{"id":1167,"sqid":"`+domains.Sqid(1167)+`","name":"neighbour","created_at":"2026-08-02T09:00:00Z"}`), ""); err != nil {
+ t.Fatal(err)
+ }
+ cmd := exec.Command(bin, "accounts", "create", "-name", "shop", "-owner", "owner@example.com")
+ cmd.Env = append(os.Environ(),
+ "CARLOS_DEPLOYMENT_DIR="+storeDir,
+ "CARLOS_CREDENTIALS="+filepath.Join(tmp, "credentials"),
+ "CARLOS_DATA_DIR="+tmp,
+ "HOME="+tmp,
+ "CARLOS_CONSOLE=",
+ )
+ cmd.Dir = tmp
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("carlos accounts create --owner: %v\n%s", err, out)
+ }
+ if _, _, err := store.Get(t.Context(), domains.AccountManifestKey("dev")); err == nil {
+ t.Fatalf("accounts create minted the reserved sqid dev:\n%s", out)
+ }
+ body, _, err := store.Get(t.Context(), domains.AccountManifestKey("dew"))
+ if err != nil {
+ t.Fatalf("no manifest at accounts/dew/ (%v):\n%s", err, out)
+ }
+ var m domains.AccountManifest
+ if err := json.Unmarshal(body, &m); err != nil || m.ID != 1169 || m.Name != "shop" {
+ t.Fatalf("manifest = %+v (%v), want shop at id 1169", m, err)
+ }
+}
+
// A padded name is trimmed BEFORE it is validated or minted — the gate and
// the write must agree on what "the name" is. Before this fix, -name "
// kass " passed ValidAccountName on the trimmed copy but handed the
diff --git a/internal/console/materialize.go b/internal/console/materialize.go
index 295a052e2..dd3020ca7 100644
--- a/internal/console/materialize.go
+++ b/internal/console/materialize.go
@@ -80,6 +80,15 @@ func MaterializeAccounts(ctx context.Context, s release.Store, st *Store) (int,
"not a new account", key, sqid, m.Sqid, m.ID, domains.Sqid(m.ID))
continue
}
+ // A live account holding a reserved sqid (#285). The allocator never
+ // mints one, so this fires only if the list grows over an existing
+ // account or a manifest was written by hand. The account is left alone,
+ // as the ruling says; this line is what makes it visible on every
+ // console start instead of surfacing as broken DNS.
+ if !m.Deleted() && domains.IsReservedSqid(m.Sqid) {
+ log.Printf("console: account %s (id %d) has a reserved sqid; its hostnames under the apps domain "+
+ "collide with platform infrastructure (#285)", m.Sqid, m.ID)
+ }
// Everything the row and the manifest can say to each other. Only the
// last case — never seen here, and live — falls through to birth.
switch {
diff --git a/internal/console/reserved_sqids_test.go b/internal/console/reserved_sqids_test.go
new file mode 100644
index 000000000..357d8f677
--- /dev/null
+++ b/internal/console/reserved_sqids_test.go
@@ -0,0 +1,140 @@
+package console
+
+import (
+ "bytes"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/domains"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// devAccountID is the id whose sqid is "dev" (#285).
+const devAccountID = int64(1168)
+
+// Every console door that creates an account (landing's personal-account
+// mint, POST /accounts/new, POST /auth/cli/accounts) mints through
+// domains.MintAccount, so none of them can hand out the reserved "dev". Each
+// door is driven against a bucket whose highest account is 1167, so the next
+// plain id would be 1168.
+func TestConsoleCreationDoorsSkipReservedSqids(t *testing.T) {
+ doors := map[string]func(t *testing.T, fx *consoleFixture) (owner string){
+ "landing": func(t *testing.T, fx *consoleFixture) string {
+ fx.setUser("newbie@keymail.dev")
+ defer fx.setUser("paul@keymail.dev")
+ c := newBrowser(t)
+ signUp(t, fx, c, "code-new")
+ if st, loc, _ := get(t, c, fx.ts.URL+"/"); st != http.StatusFound || !strings.HasPrefix(loc, "/a/") {
+ t.Fatalf("landing = %d -> %q, want 302 into an account", st, loc)
+ }
+ return "newbie@keymail.dev"
+ },
+ "POST /accounts/new": func(t *testing.T, fx *consoleFixture) string {
+ c := signInPaul(t, fx)
+ csrf := sessionToken(t, fx, c)
+ if st, loc, _ := postForm(t, c, fx.ts.URL+"/accounts/new",
+ url.Values{"csrf": {csrf}, "name": {"shop"}}); st != http.StatusSeeOther {
+ t.Fatalf("create = %d -> %q, want 303", st, loc)
+ }
+ return "paul@keymail.dev"
+ },
+ "POST /auth/cli/accounts": func(t *testing.T, fx *consoleFixture) string {
+ token := cliToken(t, fx, "paul@keymail.dev", "code-acct")
+ if st, out := postJSON(t, fx.ts.URL+"/auth/cli/accounts", map[string]string{"name": "shop"}, token); st != http.StatusOK {
+ t.Fatalf("accounts create = %d %v", st, out)
+ }
+ return "paul@keymail.dev"
+ },
+ }
+ for name, door := range doors {
+ t.Run(name, func(t *testing.T) {
+ fx := newConsoleFixture(t)
+ ctx := t.Context()
+ if err := WriteAccountManifest(ctx, fx.rel, devAccountID-1, domains.Sqid(devAccountID-1),
+ "neighbour", time.Unix(1700000000, 0)); err != nil {
+ t.Fatal(err)
+ }
+ owner := door(t, fx)
+
+ accts, err := fx.st.AccountsFor(owner)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var made *Account
+ for i := range accts {
+ if accts[i].ID > devAccountID-1 {
+ made = &accts[i]
+ }
+ }
+ if made == nil {
+ t.Fatalf("no new account for %s in %+v", owner, accts)
+ }
+ if made.ID != devAccountID+1 || made.Sqid != "dew" {
+ t.Fatalf("minted (%d, %q), want (%d, \"dew\"): the door skipped the reserved-sqid rule",
+ made.ID, made.Sqid, devAccountID+1)
+ }
+ if _, _, err := fx.rel.Get(ctx, AccountManifestKey("dev")); err == nil {
+ t.Fatal("a manifest landed at accounts/dev/")
+ }
+ if a, err := fx.st.AccountBySqid("dev"); err != nil || a != nil {
+ t.Fatalf("AccountBySqid(dev) = %+v %v, want none", a, err)
+ }
+ })
+ }
+}
+
+// The ruling leaves existing accounts alone, so an account that already holds
+// a reserved sqid (the list grew over it, or a manifest was written by hand)
+// is materialized as usual and named in the log, once per pass. A tombstoned
+// one is not named: it serves nothing.
+func TestMaterializeFlagsALiveReservedSqid(t *testing.T) {
+ var logged bytes.Buffer
+ log.SetOutput(&logged)
+ t.Cleanup(func() { log.SetOutput(os.Stderr) })
+
+ st := openTestStore(t)
+ rel := release.NewDirStore(t.TempDir())
+ ctx := t.Context()
+ created := time.Unix(1700000000, 0).UTC()
+ edgeID := int64(49868) // Sqid "edge"
+ for _, a := range []struct {
+ id int64
+ name string
+ }{{devAccountID, "held"}, {devAccountID + 1, "plain"}, {edgeID, "retired"}} {
+ if err := WriteAccountManifest(ctx, rel, a.id, domains.Sqid(a.id), a.name, created); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := domains.TombstoneAccount(ctx, rel, "edge", created); err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := MaterializeAccounts(ctx, rel, st); err != nil {
+ t.Fatal(err)
+ }
+ if a, err := st.AccountBySqid("dev"); err != nil || a == nil || a.ID != devAccountID {
+ t.Fatalf("AccountBySqid(dev) = %+v %v: an existing account must be left as it is", a, err)
+ }
+ out := logged.String()
+ if !strings.Contains(out, "account dev (id 1168) has a reserved sqid") {
+ t.Errorf("no reserved-sqid line for the live dev account:\n%s", out)
+ }
+ flagged := 0
+ for _, line := range strings.Split(out, "\n") {
+ if !strings.Contains(line, "reserved sqid") {
+ continue
+ }
+ flagged++
+ if strings.Contains(line, "—") {
+ t.Errorf("the reserved-sqid log line carries an em dash: %s", line)
+ }
+ }
+ if flagged != 1 {
+ t.Errorf("%d reserved-sqid lines, want exactly one (dev; not the plain dew, not the retired edge):\n%s", flagged, out)
+ }
+}
diff --git a/internal/domains/mint.go b/internal/domains/mint.go
index be905f63e..1fa33bfe1 100644
--- a/internal/domains/mint.go
+++ b/internal/domains/mint.go
@@ -130,6 +130,13 @@ const mintAttempts = 5
// materialization, which skips it): handing that id to somebody else would put
// two objects on one identity, which is the single thing this scheme exists to
// prevent.
+//
+// An id whose sqid is reserved (IsReservedSqid, #285) is stepped over: the
+// answer is the first id past the highest claimed one whose sqid is free. No
+// manifest or tombstone is written for the skipped id, and none is needed.
+// The skip is a pure function of the id, so every minter computes the same
+// answer, a restart changes nothing, and once a later id is minted the
+// skipped one sits below the highest claim, where nothing allocates.
func NextAccountID(ctx context.Context, s release.Store) (int64, error) {
keys, err := s.List(ctx, "accounts/")
if err != nil {
@@ -155,7 +162,7 @@ func NextAccountID(ctx context.Context, s release.Store) (int64, error) {
max = m.ID
}
}
- return max + 1, nil
+ return skipReservedIDs(max + 1), nil
}
// MintAccount allocates the next free account id and publishes its manifest,
diff --git a/internal/domains/reserved.go b/internal/domains/reserved.go
new file mode 100644
index 000000000..c07c41f44
--- /dev/null
+++ b/internal/domains/reserved.go
@@ -0,0 +1,76 @@
+package domains
+
+// reservedSqids are the labels no account may ever be given as its sqid
+// (#285, Paul's ruling 2026-10-14).
+//
+// Why: an account's apps live at <app>.<sqid>.<apps-domain>, and the
+// deployment's own infrastructure also hangs labels directly under the apps
+// domain. When the two meet, DNS answers for the infrastructure, not the
+// account. On 2026-10-13 flagship gained `*.dev.oncarlos.com` pointing at the
+// dev deployment's box, and Sqid(1168) is "dev": every app of account 1168
+// would have resolved to dev's edge, and dev's edge would have tried to
+// issue certificates for them.
+//
+// The alphabet and the offset stay frozen. Instead, NextAccountID steps over
+// any id whose sqid is listed here, so the id is simply never handed out.
+// Existing accounts are untouched: console.MaterializeAccounts logs any live
+// account that holds a listed sqid, so adding a label that some account
+// already has shows up on the next console start rather than silently.
+//
+// Only list labels that Sqid can actually produce. TestReservedSqidsAreProducible
+// fails on any other entry, so dead entries cannot creep in. A label cannot
+// be produced when it:
+// - contains i, l or o (not in the frozen alphabet): relay, platform,
+// console, mail, tunnel, static;
+// - has fewer than 3 letters (sqidOffset's floor): us, uk, au, eu;
+// - starts with "a" (base 23 has no leading zero digit): acme, app, api,
+// admin, assets, auth;
+// - contains anything but lowercase letters: eu-west-1, _acme-challenge.
+//
+// Extending: add the label with a comment naming where it comes from. The
+// skip only matters for ids that have not been minted yet, so adding a label
+// is always safe. Removing one is safe too, but once the allocator has
+// passed its id that id stays unused for good (allocation is "one past the
+// highest id in the bucket", so nothing goes back for it).
+var reservedSqids = []string{
+ // dev.oncarlos.com: the dev deployment's apps domain. Flagship's Route53
+ // zone has `*.dev.oncarlos.com A <dev edge>` (carloku-infrastructure
+ // dns53.tf, oncarlos_dev_wildcard, #9). Sqid(1168).
+ "dev",
+ // edge.<apps-domain>: the steering hostname the converger publishes and
+ // health-checks on every armed edge (steering.ConvergeDeployment,
+ // edge steeringprobe, console reservedDomains). Sqid(49868).
+ "edge",
+ // www.<apps-domain>: not served today, but it is the label anyone would
+ // add next for the apps domain's own apex site, and a tenant holding it
+ // would look like the platform's own website. Held back pre-emptively;
+ // Sqid(9978).
+ "www",
+}
+
+// ReservedSqids returns a copy of the labels no account may be given as its
+// sqid. A copy, so no caller can change what the allocator skips.
+func ReservedSqids() []string {
+ return append([]string(nil), reservedSqids...)
+}
+
+// IsReservedSqid reports whether s is a label no account may be given.
+func IsReservedSqid(s string) bool {
+ for _, r := range reservedSqids {
+ if s == r {
+ return true
+ }
+ }
+ return false
+}
+
+// skipReservedIDs returns id, or the first id after it whose sqid is not
+// reserved. A pure function of the id and the list, which is what makes the
+// skip safe without any stored state: every minter, on every box and after
+// every restart, steps over exactly the same ids.
+func skipReservedIDs(id int64) int64 {
+ for IsReservedSqid(Sqid(id)) {
+ id++
+ }
+ return id
+}
diff --git a/internal/domains/reserved_test.go b/internal/domains/reserved_test.go
new file mode 100644
index 000000000..34f9e97db
--- /dev/null
+++ b/internal/domains/reserved_test.go
@@ -0,0 +1,340 @@
+package domains_test
+
+import (
+ "encoding/json"
+ "math"
+ "os"
+ "path/filepath"
+ "runtime"
+ "slices"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/domains"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// devID is the account id whose sqid is "dev". Pinned here as well as
+// derived, because #285 was filed against the wrong id (1169, which is
+// "dew"): a test that only ever derived it would not have caught that.
+const devID = 1168
+
+// sqidID inverts Sqid by plain base-23 decoding, the way the encoder builds
+// its digits. ok is false for a string Sqid can never emit for a positive id.
+// It lives in the test on purpose: nothing in production decodes a sqid, and
+// the producibility check should not trust the encoder to vouch for itself.
+func sqidID(s string) (int64, bool) {
+ const alphabet = "abcdefghjkmnpqrstuvwxyz"
+ if len(s) < 3 || len(s) > 16 {
+ return 0, false
+ }
+ var n int64
+ for i := 0; i < len(s); i++ {
+ d := strings.IndexByte(alphabet, s[i])
+ if d < 0 {
+ return 0, false
+ }
+ if n > (math.MaxInt64-int64(d))/23 {
+ return 0, false // past every int64 id: Sqid never gets here
+ }
+ n = n*23 + int64(d)
+ }
+ id := n - 23*23
+ if id < 1 || domains.Sqid(id) != s {
+ return 0, false // a leading "a", or "baa" (id 0)
+ }
+ return id, true
+}
+
+func TestDevIsReservedAndIsSqid1168(t *testing.T) {
+ if got := domains.Sqid(devID); got != "dev" {
+ t.Fatalf("Sqid(%d) = %q, want \"dev\"", devID, got)
+ }
+ if !domains.IsReservedSqid("dev") {
+ t.Fatal(`"dev" is not reserved: account 1168's apps would resolve to the dev deployment's edge`)
+ }
+ if domains.Sqid(1169) != "dew" || domains.IsReservedSqid("dew") {
+ t.Fatalf("Sqid(1169) = %q (reserved %v): 1169 is the id the allocator should land on",
+ domains.Sqid(1169), domains.IsReservedSqid(domains.Sqid(1169)))
+ }
+}
+
+// Every listed label must be one Sqid can actually emit. A label with i, l
+// or o in it, under 3 letters, or starting with "a" can never be an account's
+// sqid, and listing it would only suggest a protection that is not needed.
+func TestReservedSqidsAreProducible(t *testing.T) {
+ list := domains.ReservedSqids()
+ if len(list) == 0 {
+ t.Fatal("no reserved sqids")
+ }
+ seen := map[string]bool{}
+ for _, label := range list {
+ if seen[label] {
+ t.Errorf("%q is listed twice", label)
+ }
+ seen[label] = true
+ id, ok := sqidID(label)
+ if !ok {
+ t.Errorf("%q is listed as reserved but Sqid can never produce it; drop it", label)
+ continue
+ }
+ if !domains.ValidSqid(label) {
+ t.Errorf("%q decodes to id %d but ValidSqid rejects it", label, id)
+ }
+ t.Logf("reserved %q = Sqid(%d)", label, id)
+ }
+ // The decoder itself, against the frozen golden values and the cases it
+ // must refuse.
+ for s, want := range map[string]int64{"bab": 1, "cxn": 1000, "dev": devID} {
+ if got, ok := sqidID(s); !ok || got != want {
+ t.Errorf("sqidID(%q) = %d %v, want %d", s, got, ok, want)
+ }
+ }
+ for _, s := range []string{"app", "acme", "baa", "relay", "us", "ops"} {
+ if _, ok := sqidID(s); ok {
+ t.Errorf("sqidID(%q) decoded, but Sqid never emits it", s)
+ }
+ }
+}
+
+// ReservedSqids hands out a copy: a caller editing its slice must not change
+// what the allocator skips.
+func TestReservedSqidsIsACopy(t *testing.T) {
+ list := domains.ReservedSqids()
+ list[0] = "zzz"
+ if domains.IsReservedSqid("zzz") || !slices.Contains(domains.ReservedSqids(), "dev") {
+ t.Fatal("editing ReservedSqids' result changed the reserved set")
+ }
+}
+
+// The allocator steps over every reserved id, seeded one below it with a
+// single manifest rather than a thousand real rows.
+func TestNextAccountIDSkipsReservedSqids(t *testing.T) {
+ ctx := t.Context()
+ for _, label := range domains.ReservedSqids() {
+ id, ok := sqidID(label)
+ if !ok {
+ t.Fatalf("%q is not producible", label)
+ }
+ s := release.NewDirStore(t.TempDir())
+ writeManifest(t, s, id-1, domains.Sqid(id-1))
+ got, err := domains.NextAccountID(ctx, s)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got <= id {
+ t.Errorf("after id %d, NextAccountID = %d (%q); want past %d, whose sqid is the reserved %q",
+ id-1, got, domains.Sqid(got), id, label)
+ }
+ if domains.IsReservedSqid(domains.Sqid(got)) {
+ t.Errorf("NextAccountID = %d, whose sqid %q is reserved", got, domains.Sqid(got))
+ }
+ }
+
+ // And the plain case is untouched: an id whose sqid is free is the
+ // answer, exactly as before.
+ s := release.NewDirStore(t.TempDir())
+ writeManifest(t, s, devID-2, domains.Sqid(devID-2))
+ if got, err := domains.NextAccountID(ctx, s); err != nil || got != devID-1 {
+ t.Errorf("NextAccountID after %d = %d (%v), want %d", devID-2, got, err, devID-1)
+ }
+}
+
+// MintAccount, the one allocator every creation path uses, lands on 1169
+// across the skip, and writes nothing at accounts/dev/.
+func TestMintAccountNeverMintsDev(t *testing.T) {
+ ctx := t.Context()
+ s := release.NewDirStore(t.TempDir())
+ writeManifest(t, s, devID-1, domains.Sqid(devID-1))
+ id, sqid, err := domains.MintAccount(ctx, s, "shop", "owner@example.com", time.Unix(1700000000, 0))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if id != devID+1 || sqid != "dew" {
+ t.Fatalf("minted (%d, %q), want (%d, \"dew\")", id, sqid, devID+1)
+ }
+ if _, _, err := s.Get(ctx, domains.AccountManifestKey("dev")); err == nil {
+ t.Fatal("a manifest landed at accounts/dev/")
+ }
+}
+
+// The race at the skip: another minter takes 1169 between our listing and our
+// write. We must re-list and take 1170, never fall back to 1168.
+func TestMintAccountLosesTheRaceAtTheSkip(t *testing.T) {
+ ctx := t.Context()
+ inner := release.NewDirStore(t.TempDir())
+ writeManifest(t, inner, devID-1, domains.Sqid(devID-1))
+ planted := false
+ s := &racingStore{Store: inner, beforePut: func(key string) {
+ if planted {
+ return
+ }
+ planted = true
+ if key != domains.AccountManifestKey("dew") {
+ t.Errorf("first create-only write went to %s, want accounts/dew/", key)
+ }
+ writeManifest(t, inner, devID+1, "dew")
+ }}
+ id, sqid, err := domains.MintAccount(ctx, s, "shop", "", time.Now())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if id != devID+2 || sqid != domains.Sqid(devID+2) {
+ t.Fatalf("minted (%d, %q), want (%d, %q)", id, sqid, devID+2, domains.Sqid(devID+2))
+ }
+}
+
+// Concurrent minters around the skip: every one gets a distinct id, none gets
+// 1168, and together they fill 1169.. with no gap but the reserved one.
+func TestConcurrentMintsAroundTheSkip(t *testing.T) {
+ ctx := t.Context()
+ s := release.NewDirStore(t.TempDir())
+ writeManifest(t, s, devID-1, domains.Sqid(devID-1))
+
+ // Five at once: a minter loses at most once to each of the others, and
+ // MintAccount allows five attempts.
+ const n = 5
+ ids := make([]int64, n)
+ errs := make([]error, n)
+ var wg sync.WaitGroup
+ start := make(chan struct{})
+ for i := range n {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ <-start
+ ids[i], _, errs[i] = domains.MintAccount(ctx, s, "shop", "", time.Now())
+ }()
+ }
+ close(start)
+ wg.Wait()
+ for i, err := range errs {
+ if err != nil {
+ t.Fatalf("minter %d: %v", i, err)
+ }
+ }
+ slices.Sort(ids)
+ for i, id := range ids {
+ if want := int64(devID + 1 + i); id != want {
+ t.Fatalf("minted ids %v, want %d..%d", ids, devID+1, devID+n)
+ }
+ }
+ if _, _, err := s.Get(ctx, domains.AccountManifestKey("dev")); err == nil {
+ t.Fatal("a concurrent mint landed at accounts/dev/")
+ }
+}
+
+// The skip holds across restarts: a fresh store over the same bucket (a new
+// process, another box) computes the same answer, before and after the id
+// past the skip is minted. Nothing is stored for the skipped id, so there is
+// nothing for a restart to lose.
+func TestReservedSkipSurvivesRestart(t *testing.T) {
+ ctx := t.Context()
+ dir := t.TempDir()
+ writeManifest(t, release.NewDirStore(dir), devID-1, domains.Sqid(devID-1))
+
+ for range 2 {
+ got, err := domains.NextAccountID(ctx, release.NewDirStore(dir))
+ if err != nil || got != devID+1 {
+ t.Fatalf("NextAccountID on a fresh store = %d (%v), want %d", got, err, devID+1)
+ }
+ }
+ if _, _, err := domains.MintAccount(ctx, release.NewDirStore(dir), "shop", "", time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ got, err := domains.NextAccountID(ctx, release.NewDirStore(dir))
+ if err != nil || got != devID+2 {
+ t.Fatalf("NextAccountID after minting %d = %d (%v), want %d", devID+1, got, err, devID+2)
+ }
+ keys, err := release.NewDirStore(dir).List(ctx, "accounts/")
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, k := range keys {
+ body, _, err := release.NewDirStore(dir).Get(ctx, k)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var m domains.AccountManifest
+ if err := json.Unmarshal(body, &m); err != nil {
+ t.Fatal(err)
+ }
+ if domains.IsReservedSqid(m.Sqid) {
+ t.Fatalf("%s holds the reserved sqid %q", k, m.Sqid)
+ }
+ }
+}
+
+// Every creation path goes through the one rule. The rule lives in
+// NextAccountID, MintAccount is its only caller, and the account rows the
+// console and the registry write must take their ids from MintAccount. This
+// census pins the callers, so a new creation path has to be added here on
+// purpose, with someone checking that it mints through MintAccount.
+//
+// The materialization writers (console EnsureAccount, registry PutAccount in
+// domains.Reconcile and the CLI) copy an id the bucket already holds; they
+// never choose one, so they are not creation paths.
+func TestAccountCreationPathsCensus(t *testing.T) {
+ _, here, _, ok := runtime.Caller(0)
+ if !ok {
+ t.Fatal("no caller")
+ }
+ root := filepath.Join(filepath.Dir(here), "..", "..")
+ want := map[string][]string{
+ "domains.NextAccountID(": nil, // only MintAccount, inside this package
+ "NextAccountID(ctx": {"internal/domains/mint.go"},
+ "domains.MintAccount(": {
+ "accounts.go", // carlos accounts create --owner
+ "internal/console/server.go", // Server.mintAccount: landing, /accounts/new, /auth/cli/accounts
+ },
+ "s.mintAccount(": {
+ "internal/console/authcli.go", // POST /auth/cli/accounts
+ "internal/console/server.go", // landing mint and POST /accounts/new
+ },
+ ".CreateAccount(": {"internal/console/server.go"}, // inside Server.mintAccount only
+ }
+ got := map[string][]string{}
+ err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d.IsDir() {
+ switch d.Name() {
+ case ".git", "node_modules", "vendor", "testdata":
+ return filepath.SkipDir
+ }
+ if strings.HasPrefix(d.Name(), ".") && path != root {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+ if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
+ return nil
+ }
+ b, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ rel, _ := filepath.Rel(root, path)
+ rel = filepath.ToSlash(rel)
+ for needle := range want {
+ if strings.Contains(string(b), needle) && !slices.Contains(got[needle], rel) {
+ got[needle] = append(got[needle], rel)
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ for needle, files := range want {
+ slices.Sort(files)
+ slices.Sort(got[needle])
+ if !slices.Equal(files, got[needle]) {
+ t.Errorf("%q is called from %v, want %v. A new account creation path must mint through "+
+ "domains.MintAccount so it skips reserved sqids (#285); then add it here", needle, got[needle], files)
+ }
+ }
+}
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go
index fe8dbe6b7..d1c2bb971 100644
--- a/internal/provision/plan14_root_reads_test.go
+++ b/internal/provision/plan14_root_reads_test.go
@@ -668,6 +668,12 @@ func TestPlan14RootReadCensus(t *testing.T) {
// activator rows and four new ones (generation socket directories) add
// fourteen frozen declarations.
seal = "e736a5e2652cc546db02c5a385ae7da9fa8a4313d3064f6b431642431974d65d"
+ // Census overlay reserved285 (#285): envelope-only, for the reserved-sqid
+ // skip in the account allocator and its materialization log line. No
+ // root-read unit changes, so the legacy seal above stands.
+ if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-reserved285.tsv"), &inv, true); err != nil {
+ t.Fatal(err)
+ }
errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...)
errs = append(errs, checkRootReadDeclarations(got, inv)...)
if len(errs) > 0 {
diff --git a/internal/provision/testdata/plan14-root-reads-reserved285.tsv b/internal/provision/testdata/plan14-root-reads-reserved285.tsv
new file mode 100644
index 000000000..031735ddb
--- /dev/null
+++ b/internal/provision/testdata/plan14-root-reads-reserved285.tsv
@@ -0,0 +1,3 @@
+# Plan 14 census overlay reserved285 (amadan #285, reserved account sqids): the account allocator (internal/domains NextAccountID) steps over ids whose sqid is a reserved label, and the console's account materialization logs a live account that holds one. Envelope-only: no root-read unit changed or was added, nothing runs as root that did not before, and no registry, /data, release or claim read was added. Earlier overlays are immutable.
+@coverage internal/domains 5d2afacc1669ea25966a274801f709a1f896c4c24c946766bb4bf1cf29915ea1
+@coverage internal/console 8a423453c4f4e86c0e6ba1548333bb36159487adaed9c8e97a1a63f63f031444