carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Account identity: minted into the BUCKET (domains.MintAccount), then
2// materialized by the registry and by the console's own tables. The id is the
3// next free integer, allocated by a create-only manifest write rather than by
4// any local row number, because identity has to survive a registry rebuild and
5// be identical on every box in a deployment. See
6// docs/superpowers/specs/2026-08-02-multi-account-design.md §1.
7package console
8
9import (
10 "context"
11 "encoding/json"
12 "errors"
13 "os"
14 "time"
15
16 "github.com/carlosframework/platform/internal/domains"
17 "github.com/carlosframework/platform/internal/release"
18)
19
20// accountSqid is the account's public identity: the FROZEN encoder over its
21// id (internal/domains). Wrapped here so every mint in the console goes
22// through one place — the encoding is what every platform domain and every
23// bucket key embeds, and it can never be recomputed differently.
24func accountSqid(id int64) string { return domains.Sqid(id) }
25
26// AccountManifestKey is where an account's identity lives in the bucket. The
27// sqid is in the PATH as well as the body so the agent can list accounts
28// without opening every object. Defined once, in internal/domains, and shared
29// by everything that touches the object: the console, the CLI and the agent's
30// reconcile pass.
31func AccountManifestKey(sqid string) string { return domains.AccountManifestKey(sqid) }
32
33// accountManifest is the wire shape at that key. The agent's reconcile pass
34// materializes exactly this and assigns nothing.
35type accountManifest = domains.AccountManifest
36
37// WriteAccountManifest publishes an account into the deployment bucket — the
38// one place account identity crosses from the console to every box. The
39// registry only ever reads this; it never assigns an id of its own, which is
40// what makes the identity survive a registry rebuild.
41//
42// Overwrite-safe (read etag, put, one retry), the same shape the fleet
43// snapshot publisher uses: the store's Put is CAS-only, so a rename racing
44// another of the owner's own tabs must retry rather than fail.
45func WriteAccountManifest(ctx context.Context, s release.Store, id int64, sqid, name string, created time.Time) error {
46 body, err := json.Marshal(accountManifest{
47 ID: id, Sqid: sqid, Name: name, CreatedAt: created.UTC(),
48 })
49 if err != nil {
50 return err
51 }
52 key := AccountManifestKey(sqid)
53 put := func() error {
54 etag := ""
55 if _, cur, err := s.Get(ctx, key); err == nil {
56 etag = cur
57 } else if !errors.Is(err, os.ErrNotExist) {
58 return err
59 }
60 _, err := s.Put(ctx, key, body, etag)
61 return err
62 }
63 if err := put(); errors.Is(err, release.ErrCASConflict) {
64 return put() // one retry on a lost race; a second loss surfaces
65 } else if err != nil {
66 return err
67 }
68 return nil
69}
70