zeekayandHanzo Dev 4fa4ef983f account: resolve the org that acts and the org that pays
The org selected in the switcher was not the org that paid. Three layers each
derived it from a different place: the edge minted X-Org-Id from the `owner`
claim, cloud honored a membership-gated selection, and ai read user.Owner and
could not see a switch at all. A gate could therefore read one ledger while the
debit landed on another — the same split the two deleted billing allowlists
caused on the person axis, recurring on the org axis.

Add the two predicates the three layers can share, as pure functions in the leaf
module all three already import:

  EffectiveOrg(owner, orgs, requested) — which org the request ACTS IN. The ask
  is honored iff it byte-equals an entry of the SIGNED `orgs` claim; anything
  else is home, silently. Membership never comes from the request, because
  believing a raw client org is a cross-tenant read. The comparison is verbatim —
  no trim, no fold — since "acme" and "ACME" are distinct orgs in IAM, and a
  looser rule could only ever admit a switch this one refuses.

  LedgerOrg(effective, home, sudo) — which org PAYS. Two branches, not one
  expression: sudo bills home, because an admin acting on a customer must never
  spend the customer's money; everyone else bills the org they selected, which
  is the whole feature.

OrgRef carries IAM's exact wire tags so cloud, gateway and ai can alias one
decode type instead of maintaining three hand-copies that drift a field at a
time.

Adopting this is a no-op for every principal who is not switching — which is
every principal today, since no client sends a selection yet. TestNoSwitchIsANoOp
proves it by crossing every owner kind against every claim-set shape and every
refused ask, and asserting the resolved account is byte-identical to what Payer
returned from the owner claim alone.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 19:49:56 -07:00
S
Description
pull mirror of github.com/hanzoai/account
166 KiB
Languages
Go 100%