account

package
v1.801.477 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package account is your own account: API keys you mint and revoke, and org onboarding.

It mounts the signed-in caller's OWN self-service surface natively in the unified cloud binary — the Go port of the console's two NON-proxy Next server routes (app/keys + app/onboard) plus the server-side money work the statically-exported console needs (task #41, "True 1-binary FE"). It replaces the retired /v1/console/* namespace: "console" is just the cloud FE name, so there is NO /v1/console API domain — every route lives on its REAL domain.

WHY THESE ROUTES (and not the pure passthrough proxies). The console's PURE BFF reverse-proxies — app/cloud, app/ai — vanish in the one-binary model: the SPA calls the canonical /v1/* on its own origin and the already-mounted subsystems answer. The routes ported HERE do REAL server work a static SPA cannot: keys/onboard run privileged IAM logic as the confidential `hanzo-console` client, and embed does server-side verification. Each has no pure-proxy equivalent, so it must be ported.

The billing and store DATA are not among them, and the difference is the whole lesson. They were ported as two catch-all forwarders — GET|POST /v1/billing/* and full-CRUD /v1/commerce/*, mounted last (order 122), re-serving families other apps already own by re-dialing them over HTTP with the admin COMMERCE_SERVICE_TOKEN. That token satisfies commerce's MayMintMoney, so forwarding WAS authorization and the only thing standing between a signed-in member and the mint routes was a hand-maintained allowlist. They are gone. Every endpoint either forwarded is served natively — by billing (order 121) or by the co-resident commerce embed (order 100) — at a prefix the manifest names DEEPER than the bare stem, so each already won the route and the forwarder saw none of them. What survives here is the part that was never the proxy: the subject-pinning those native routes apply themselves (billing_coresident.go's PinBillingSubject) and the S2S token check they gate on (billing.go's IsServiceToken).

SURFACE — each route on its REAL domain (every one requires a VALIDATED principal — a gateway-minted, IAM-verified X-User-Id; a client-forged X-Org-Id on the bearer-less path is refused):

GET    /v1/keys                  — the caller's keys: { keys: [{ type, prefix, createdAt }] }; no secret.
POST   /v1/keys                  — create/rotate a key of { type: publishable | secret }; returns it ONCE.
DELETE /v1/keys                  — revoke the key of that type.
POST   /v1/orgs                  — create the caller's org (+ move them in on first run).
GET    /v1/csrf                  — mint the anti-CSRF token the SPA echoes on money writes (csrf.go).
GET    /v1/embed                 — brand-app embed entitlement + reachability probe (embed.go).

ONE SUBSYSTEM REGISTRATION, at order 48. The order is a convention, not the protection: the fiber fork inserts endpoint routes MOST-SPECIFIC-FIRST regardless of when they were registered (zap-proto/fiber router_precedence.go — ServeMux semantics, a static literal beats a param beats a greedy wildcard), so /v1/keys and /v1/commerce/topup/wallet win over clients/iam's /v1/iam/* and the commerce embed because they are DEEPER, not because they mount earlier. Specificity is also why the retired bridge's two bare stems could never have shadowed anything — and why nothing needed to replace them when they went.

TYPED OPS. Every ADDRESSABLE route here is a typed op (zip.Get/Post/Delete with real In/Out types) — eleven of them — so each is ONE registry entry the REST route, the OpenAPI operation's schema and prose, the MCP tool, the CLI command and every generated SDK method all derive from. That is now ALL of them: the seven exceptions were the bridge's seven wildcard methods, and they went with it. typed_wire_test.go holds the exception list as a CLOSED (and now EMPTY) set and fails on any account operation that is neither a typed op nor named there — so the next route added here is typed by default, and dropping one out of the registry takes a deliberate edit with a reason.

TENANCY. The caller is resolved from the VALIDATED identity headers ONLY (principal.Validated / c.Org() / c.User()), the same trust boundary every mutating subsystem uses. The IAM id targeted is DERIVED as `<owner>/<name>` from those validated claims — never taken from the request body/query — so a caller can only ever mint/revoke their OWN key and onboard THEMSELVES; there is no path to name a third-party subject. When the confidential client is unwired the surface is honestly "not configured" (501), never a fabricated key or org.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsServiceToken

func IsServiceToken(c *zip.Ctx) bool

IsServiceToken is the exported view of s2sBillingCall — whether the request is a trusted in-proc S2S caller bearing the verified COMMERCE_SERVICE_TOKEN. Used by co-resident route gates (e.g. the spend-alert admin gate) that must admit the metering cap-gate and the SuperAdmin cap-oversight Forward alongside org admins, while refusing a plain member.

func MountAccount

func MountAccount(app cloud.Router, deps cloud.Deps) error

MountAccount wires account's self-service routes (order 48) — the ones that must win over the IAM /v1/iam/* wildcard (50) and the commerce embed (100).

func PinBillingSubject

func PinBillingSubject() zip.Handler

PinBillingSubject pins every billing subject key in the request query to the VALIDATED caller's own subject (dropping ?org), so a co-resident commerce read handler downstream can only ever return the caller's OWN rows. It is the co-resident twin of billingData's subject-pinning, reusing the SAME resolveCaller → account.Payer rule and the SAME scopedBillingSearch, so the two paths scope identically.

Three cases, mirroring billingData exactly:

  • Browser customer (a validated principal with an org): OVERWRITE the subject keys with the caller's own subject and drop ?org. The client cannot widen scope.
  • Trusted in-proc S2S (the verified COMMERCE_SERVICE_TOKEN bearer, carrying its own X-Org-Id): pass the query through VERBATIM — it legitimately names its own subject, scoped by the EdgeAuth-controlled org.
  • Neither: refuse. A bearer-less request with a forged X-Org-Id has no validated principal and is fail-closed here, before the read handler runs.

The pin rewrites the request URI's query string AND (on a write) the JSON body in place; fasthttp's SetQueryString resets the parsed-args cache and SetBody replaces the body bytes, so the handler's later c.Query() / c.Bind() read the pinned values. Pinning the body is what keeps a co-resident WRITE handler that reads its subject from the body (commerce's CreatePaymentMethod reads customerId from the JSON body) IDOR-safe — query-only pinning would leave a client-named customerId/userId in the body untouched.

func RequireCSRF

func RequireCSRF() zip.Handler

RequireCSRF exposes the ambient-cookie anti-CSRF gate as a STANDALONE middleware for a co-resident money-WRITE route registered OUTSIDE this package — specifically apps/commerce.go's POST /v1/billing/topup/token. That write used to be wrapped in requireCSRF by the /v1/billing/* forwarder this package once mounted; moving it co-resident (to break the commerce transport self-dispatch loop) must NOT silently drop the gate, so the identical enforcement rides along as its own handler — and it is now the ONLY thing enforcing it, the forwarder being gone. It binds to the SAME process-wide key (sharedCSRFKey) the GET /v1/csrf issuer uses, so a token minted at /v1/csrf verifies here byte-identically. Enforces ONLY on the ambient-cookie path (a Bearer/gateway/API caller is not CSRF-able); on success it c.Next()s into the rest of the chain. The minimal Service carries only the shared key — requireCSRF/verifyCSRF read nothing else off it.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL