zeekayandHanzo Dev dfee0253b2 account: an explicit org selection succeeds or refuses, never redirects the bill
EffectiveOrg returned the home org for an ask outside the membership set and
called that "fail closed". It is not. This function selects the PAYER, so
falling back does not fail — it SUCCEEDS against a different economic principal.
The model runs, the tokens meter, the ledger writes, and the wrong account is
charged with no error anywhere. An obvious failure would have been safer.

Measured, which is how this surfaced: org `hanzo` holds 14,986,317 and user
`hanzo/z` holds 0. A request selecting `hanzo` that the subject could not bill
resolved silently to `hanzo/z` and answered "insufficient balance" — a true
statement about an account nobody meant to charge.

The contract is now (string, error):

  requested == ""          -> owner, nil
  requested == owner       -> owner, nil
  requested in memberships -> requested, nil
  otherwise                -> "", ErrOrgForbidden

Unauthorized and nonexistent are the SAME refusal so the ask cannot be used to
enumerate orgs; callers surface both as 403.

An empty owner now refuses an explicit ask too. It previously took the first
branch and answered ("", nil) — success naming no payer, the same shape as the
fallback being removed, one level down.

Breaking on purpose, no EffectiveOrgStrict alongside it: leaving the permissive
signature reachable invites the next caller to pick it by accident, and this is
exactly the API where "accidentally permissive" spends someone else's money.

LedgerOrg is untouched. Its sudo->home rule is already correct: an admin acting
inside a customer's org must never spend the customer's balance.

21 table cases become refusals; restoring the fallback fails all 21. Six call
sites in the suite migrate to the two-value form.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-08 06:07:56 -07:00

account

The billing-account rule. One value, one decision, no dependencies.

account.Payer(account.Credential{Owner: org, Name: user, Account: claim}).Subject()

(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 spend-gate reads it, a usage debit spends it. One address, one answer, every caller.

Payer is the whole point: it, and only it, decides whether a request pays from an org pool or a person's own account. That decision shipped wrong twice by being made in two places — a gate keyed on the pool while the debit spent a person's wallet, so a funded pool green-lit a request that drained an empty personal one, and an empty pool 402'd a funded person. Any new money path resolves its address here rather than re-deriving it.

Why it is its own module

It has zero dependencies — go.mod is two lines and the code imports only strings. That is what lets every layer import it: cloud's trust boundary (clients/principal), its metering middleware, and commerce's billing resolver all call the same rule without any of them depending on each other.

Folding it into the commerce service would invert that: identity code would have to import a ledger, a SQLite store and an HTTP surface to answer a pure question about who pays.

Layout

  • account.go — Account, Org/Person/Project, Parse, Payer, Credential
  • org.go — org-level helpers

Licensed under MIT OR Apache-2.0, per HIP-0137.

S
Description
pull mirror of github.com/hanzoai/account
Readme
166 KiB
Languages
Go 100%