Documentation
¶
Overview ¶
Package account answers ONE question: who pays for a request.
An Account is the thing that holds a balance and gets charged. It belongs to exactly one owner — a Person, an Org, or a Project — and (Org, Subject) IS the money's address: Org names the ledger that holds it, Subject the key within that ledger. A deposit credits that address, the gate reads it, a usage debit spends it. One address, one answer, every caller.
This package is a pure leaf with no dependencies, because the rule must be IDENTICAL in every layer that touches money. The grant (commerce), the gate (ai), and the ledger call the same function or they disagree — and when they disagree, a customer tops up one account and spends from another.
WHO PAYS IS A PROPERTY OF THE CREDENTIAL. Payer is the only function that answers it, and it reads nothing but the credential: no environment variable, no allowlist, no precedence rule. The two global lists this replaces (PERSONAL_BILLING_ORGS / ORG_BILLING_ORGS) encoded one boolean per org in two repos with a precedence between them, and one lied when unset — so the grant and the gate could consult "the same" rule and disagree. They did: the grant landed on the org account while the gate read the person's, and a funded org 402'd its own members.
Index ¶
Constants ¶
const SignupOrg = "hanzo"
SignupOrg is the org every self-serve signup lands in — IAM's DefaultOrganization (iam/object/session.go). It is NOT a tenant: it is the platform's own org, and it is the home of everyone who has not been placed in a real one, because IAM's social/OAuth signup path assigns Owner = application.Organization with no tenant logic at all (iam/controllers/auth.go). Its members are strangers to each other, so each pays from their OWN account — a shared org is not a shared wallet.
This is a constant of the product, not a deployment knob: the same value is hardcoded in IAM, and nothing in the fleet configures it.
It is the last fact about billing that does not come from the credential, and it survives for ONE reason: tokens minted before IAM shipped the `billing_account` claim name no payer, and Payer's fallback still has to answer for them. IAM states this same rule authoritatively for every token minted since (iam/object/billing_account.go), so this constant is a bridge, not a floor — when the last pre-claim token has expired, it deletes with the fallback.
Variables ¶
This section is empty.
Functions ¶
func IsMachine ¶ added in v0.2.0
IsMachine reports whether an IAM User.Type names a service credential rather than a person. It is the ONE place that predicate lives, so the day it becomes trustworthy it changes here and nowhere else.
It is NOT trustworthy today — see Credential.Machine. IAM's own auth refuses to trust this field alone and requires four correlated fields (object.IsClientCredentialsClaim: type=="application" AND name==app.Name AND provider=="" AND signinMethod==""), because "type" rides IAM's non-admin UpdateUser column list and a user can set their own. Billing should resolve this at the auth boundary, where those four fields exist, and pass the answer in; this function is the seam that makes that a one-line change.
Types ¶
type Account ¶ added in v0.2.0
type Account struct {
// contains filtered or unexported fields
}
Account is one account money is recorded against. Its fields are unexported so every Account is valid by construction — this is money; an Account whose owner disagrees with its key is not a value we allow to exist.
func Org ¶ added in v0.2.0
Org returns the account owned by an org: one pooled balance for the whole tenant, keyed by the org slug. This is the account an admin grant credits.
func Parse ¶ added in v0.2.0
Parse reads a `billing_account` claim back into the Account it names. It is a PARSE, not a decision: it never invents an owner, and anything it cannot read — an empty claim, an unknown kind, a missing subject, a person or project with no name — returns the zero Account, which Payer treats as "the credential named nobody" and falls back rather than billing a guess.
Every component funnels through the same constructors the rest of this file uses (Org/Person/Project), so a parsed Account is valid and folded by construction — it can never address a wallet a constructed one could not.
func Payer ¶ added in v0.2.0
func Payer(c Credential) Account
Payer returns the Account that pays for a credential. It is the ONE function that answers "who pays": the grant, the gate, the usage debit and the console read all call it, so they cannot drift. It reads no configuration.
The rule:
the account the credential NAMES → that account (IAM said so, over its signature) nothing named (a pre-claim token) → the legacy fallback below
THE CREDENTIAL NAMES ITS PAYER. IAM mints `billing_account` at the identity boundary from the REAL grant context and signs it (iam/object/billing_account.go); the gateway validates it, strips any client copy, and mints X-Billing-Account-Id from it. So the pooled/personal distinction — real, and money depends on it — is now IN the credential, and this function reads it instead of inferring it. That is the whole point: an inference can be wrong, and this one was. It ran on User.Type=="application", which IAM's UpdateUser lets a user set, so a member of the shared signup org could name themselves a machine and spend the org pool. A signed claim cannot be forged by the caller it describes.
THE FALLBACK IS FOR OLD TOKENS, NOT FOR DOUBT. A token minted before the claim shipped carries no account, and refusing it would 402 every live session. So an unnamed credential resolves the old way — and the old way is exactly what the claim now states for a new one, so the two agree for every principal that is not forging. When the last pre-claim token has expired, the fallback (and SignupOrg with it) deletes, and the rule is one line: the account the credential names.
The claim is only honored WITHIN the caller's own org — a claim naming another tenant's ledger is discarded, not billed. IAM never mints one (the claim and `owner` come from the same signed token), so this can only fire on a mis-wired caller pairing a foreign claim with a local owner; it costs one comparison to make that a fallback instead of a cross-tenant debit.
func PayerOf ¶ added in v0.2.0
PayerOf is Payer for callers holding the identity as an "<org>/<name>" key rather than two fields — usage records, ZAP params, searchAuth.UserID. If org is empty it is taken from the key's prefix. It is a parse, not a second rule: it funnels into Payer, so it can never answer differently.
func Person ¶ added in v0.2.0
Person returns the account owned by one person within an org — a balance no other member of that org can spend.
func Project ¶ added in v0.2.0
Project returns the account owned by a project within an org.
A project needs no org of its own to be billed: the project IS the owner, and the org is only the ledger its money lives in. Nothing in Payer special-cases it — it is one more owner kind, exactly like a Person.
KNOWN LIMIT, stated rather than papered over: a Project and a Person are distinct VALUES here but share one ledger key space ("<org>/<name>"), so a project and a person with the same name in the same org would address the SAME wallet. Nothing constructs a Project account yet (no credential can name one — see Payer), so nothing collides today; giving projects their own key prefix is a ledger change and must land with the credential that names them, not before.
func (Account) Org ¶ added in v0.2.0
Org reports the ledger holding this account — the X-Org-Id namespace, and the per-org file the balance lives in.
func (Account) String ¶ added in v0.2.0
String renders an Account as the `billing_account` claim IAM signs into every token: `<kind>:<subject>` — "org:acme", "person:hanzo/alice", "project:acme/website". A zero Account renders "" (unattributable names nobody).
This is one half of a WIRE CONTRACT with iam/object/billing_account.go, which builds the same string from the grant context. IAM shares no code with this module, so the grammar is the only thing holding them together: kind is one of person|org|project, subject is the Account's own Subject(). Parse is the inverse — String ∘ Parse is the identity on every account IAM can mint.
func (Account) Subject ¶ added in v0.2.0
Subject is the account's key within its ledger: what a deposit credits as DestinationId, the gate reads as ?user=, and a usage debit spends as SourceId. An org account is the bare slug; a person's or project's is "<org>/<name>".
Always folded, because the read paths lowercase and the write paths store verbatim: an un-lowercased subject would record usage that never nets against the balance — a silent leak.
type Credential ¶ added in v0.2.0
Credential is what a request presents, reduced to the only facts about it that are both VALIDATED and relevant to money. It is the ONLY input to Payer.
Owner and Name are the IAM `owner` and `name` claims — minted by the gateway from a verified JWT and stripped from client input, so they are trustworthy.
Account is the IAM `billing_account` claim: the credential NAMING its own payer, signed at the identity boundary (iam/object/billing_account.go). It is the whole answer when present — Payer parses it and stops guessing. It reaches a caller two ways, both server-minted and neither forgeable: the gateway validates the claim, strips any client-supplied copy, and mints X-Billing-Account-Id from it (gateway/iamauth), and cloud's own identity boundary mints the same header from the same claim for the in-cluster path.
Machine is the LEGACY fallback signal, and it is not trustworthy: callers derive it from User.Type == "application", a field IAM's UpdateUser carries in its NON-admin column list — IAM's own code says "User.Type=='application' ALONE is forgeable" (iam/object/client_credentials.go), which is why IAM's auth requires four correlated fields (IsClientCredentialsClaim). A signup-org member who set their own Type could be billed as a machine and reach the org pool. That is exactly what the Account claim removes: IAM now resolves machine-ness from the client_credentials GRANT SHAPE and states the answer in the claim, so Machine only ever decides a token minted BEFORE the claim shipped.