// Account identity: minted into the BUCKET (domains.MintAccount), then // materialized by the registry and by the console's own tables. The id is the // next free integer, allocated by a create-only manifest write rather than by // any local row number, because identity has to survive a registry rebuild and // be identical on every box in a deployment. See // docs/superpowers/specs/2026-08-02-multi-account-design.md §1. package console import ( "context" "encoding/json" "errors" "os" "time" "github.com/carlosframework/platform/internal/domains" "github.com/carlosframework/platform/internal/release" ) // accountSqid is the account's public identity: the FROZEN encoder over its // id (internal/domains). Wrapped here so every mint in the console goes // through one place — the encoding is what every platform domain and every // bucket key embeds, and it can never be recomputed differently. func accountSqid(id int64) string { return domains.Sqid(id) } // AccountManifestKey is where an account's identity lives in the bucket. The // sqid is in the PATH as well as the body so the agent can list accounts // without opening every object. Defined once, in internal/domains, and shared // by everything that touches the object: the console, the CLI and the agent's // reconcile pass. func AccountManifestKey(sqid string) string { return domains.AccountManifestKey(sqid) } // accountManifest is the wire shape at that key. The agent's reconcile pass // materializes exactly this and assigns nothing. type accountManifest = domains.AccountManifest // WriteAccountManifest publishes an account into the deployment bucket — the // one place account identity crosses from the console to every box. The // registry only ever reads this; it never assigns an id of its own, which is // what makes the identity survive a registry rebuild. // // Overwrite-safe (read etag, put, one retry), the same shape the fleet // snapshot publisher uses: the store's Put is CAS-only, so a rename racing // another of the owner's own tabs must retry rather than fail. func WriteAccountManifest(ctx context.Context, s release.Store, id int64, sqid, name string, created time.Time) error { body, err := json.Marshal(accountManifest{ ID: id, Sqid: sqid, Name: name, CreatedAt: created.UTC(), }) if err != nil { return err } key := AccountManifestKey(sqid) put := func() error { etag := "" if _, cur, err := s.Get(ctx, key); err == nil { etag = cur } else if !errors.Is(err, os.ErrNotExist) { return err } _, err := s.Put(ctx, key, body, etag) return err } if err := put(); errors.Is(err, release.ErrCASConflict) { return put() // one retry on a lost race; a second loss surfaces } else if err != nil { return err } return nil }