Files
zeekayandHanzo Dev f16f998889 Add Of: the namespace a billing subject owns
An account's subject (hanzoai/account Account.Subject) already names "org or
user" — a bare slug for an org, "<org>/<name>" for a person or project — and it
is what the ledger keys on. Deriving the namespace from that one string means
the wallet and the database cannot disagree: a request that bills hanzo/alice
reads and writes hanzo/alice's data by construction.

The alternative is a table mapping each account kind to a namespace kind, which
is one more place for the two vocabularies to drift.

The org half is kept via the existing group, so acme/alice and globex/alice are
different namespaces — and therefore different databases — and a person's
namespace differs from their org's, which is what stops every personal account
in the signup org from sharing one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 11:21:30 -07:00

42 lines
1.5 KiB
Go

// Copyright (C) 2026, Hanzo AI. All rights reserved.
// See the file LICENSE for licensing terms.
package namespace
import "strings"
// Of returns the namespace naming the data that belongs to subject — the key an
// account holds within its ledger (github.com/hanzoai/account Account.Subject):
// a bare slug for an org, "<org>/<name>" for a person or a project.
//
// It is a total function of that one string, deliberately. The alternative is a
// table mapping each account kind to a namespace kind, and a table is somewhere
// the two vocabularies can drift; deriving both the wallet and the database from
// the same value means they cannot. If a request bills "hanzo/alice" it reads and
// writes hanzo/alice's data, by construction rather than by agreement.
//
// The org half is kept rather than collapsing a person to a bare name, so
// "acme/alice" and "globex/alice" stay different namespaces — and therefore
// different databases — even where two orgs use the same username.
//
// Validation is inherited: the org must be a legal id and the name a legal
// group, so a subject with a path separator, a traversal, or an oversized
// segment is rejected here rather than reaching a filesystem or a key.
func Of(subject string) (Namespace, error) {
org, name, hasName := strings.Cut(subject, "/")
ns, err := Org(org)
if err != nil {
return Namespace{}, err
}
if !hasName {
return ns, nil
}
g, err := NewGroup(name)
if err != nil {
return Namespace{}, err
}
return ns.WithGroup(g), nil
}