Documentation
¶
Overview ¶
Package console mounts the console's OWN standalone server surface natively in the unified cloud binary at /v1/console/* (HIP-0106). It is the Go port of the two console2 Next server routes that hold NO backend-proxy role but DO privileged IAM work on the signed-in user's behalf — app/keys/route.ts and app/onboard/route.ts — so console2 can drop those Node server handlers and be statically exported (task #41, "True 1-binary FE": the console SPA is go:embed'd and every dynamic call terminates at THIS binary's /v1, no separate Node origin).
WHY THESE TWO (and not the rest). console2's other server routes are pure BFF reverse-proxies (app/cloud, app/ai, app/commerce, …) that forward the browser to a backend service. In the one-binary model the cloud binary IS that backend, so those proxies vanish — the SPA calls /v1/* on its own origin and the already- mounted subsystems answer. `keys` and `onboard` are different: they are NOT proxies, they run real logic (mint/revoke the user's `hk-` key; create an org + move the user in) as the confidential `hanzo-console` client. They have no cloud equivalent, so they must be ported for the static export to be complete.
SURFACE (every route 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/console/keys — whether the caller has an `hk-` key (+ prefix/mtime); no secret.
POST /v1/console/keys — mint/rotate the key; returns { accessKey } ONCE.
DELETE /v1/console/keys — revoke the key.
POST /v1/console/onboard — create the caller's org (+ move them in on first run).
GET /v1/console/health — real IAM-configured probe (fail-closed when unwired).
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.
embed.go ports console2's app/embed-status/route.ts into the unified binary at GET /v1/console/embed-status (task #41). It answers ONE question for the console's data-product modules (Content Studio / ERP / Help Center): is this brand's shared embedded app provisioned and reachable, so the module can decide embed-vs-provision panel? A cross-origin browser can't read another origin's status (SOP + CORS), so this server route probes it once and returns an honest verdict.
TWO real jobs (why it is a handler, not a vanishing proxy):
ENTITLEMENT (server-authoritative). cms/erp/help are each a SINGLE shared per-BRAND instance, so only a member of the owning brand org — or a global admin — may frame them; a customer org gets the honest provision panel, never a cross-tenant frame. The caller's org is the VALIDATED X-Org-Id (never a browser claim); the owning org is the deployment brand.
SSRF SAFETY. The probe target is ALWAYS `<app>.<brand-domain>` where the brand is the deployment's OWN brand (deps.Brand, fixed at deploy) and app ∈ {cms,erp,help}. There is NO client-controlled host in the target at all — a forged Host header can never steer this into probing an arbitrary origin (strictly tighter than route.ts, which clamped a client Host).
iam.go is the ONE HTTP path from the console subsystem to Hanzo IAM, acting as the confidential first-party `hanzo-console` client (client_secret_basic). It ports the privileged IAM primitives that console2's server-only src/lib/server/identity.ts drove — mint/revoke/get the per-user `hk-` key and create/read/update an organization — so those standalone Next server routes can be retired and console2 statically exported (task #41, "True 1-binary FE").
WHY A CONFIDENTIAL CLIENT (and not the caller's own token). These ops are privileged: `mint-user-keys` writes a user's AccessKey, `add-organization` creates a tenant and moves the user in. IAM authorizes them for an app that is allow-listed (IAM_KEY_MINT_ALLOWED_APPS / IAM_ORG_ADMIN_APPS / IAM_USER_ADMIN_APPS) — the `hanzo-console` client — NOT for an arbitrary user bearer. So this client authenticates as that app (Basic id:secret) and always targets the ALREADY-VALIDATED caller (the handler resolves the principal from the gateway-minted X-User-Id/X-Org-Id before calling here); the caller can only ever act on their OWN id, never a third party's.
CREDENTIALS come from server-only env (IAM_MINT_CLIENT_ID / IAM_MINT_CLIENT_SECRET, sourced from KMS by the deployment), never a NEXT_PUBLIC value and never the browser. When they are unset the subsystem is honestly "not configured" (501), exactly as identity.ts's mintConfigured() gate behaved — no fabricated key/org.
onboarding.go — PURE org-naming + reserved-name policy, no transport/IAM. A faithful Go port of console2's src/lib/server/onboarding.ts, decomplected from the handler so the naming rules are one testable thing (the route does the IAM calls; this decides the slug). Two concerns:
- NAMING: turn a human org name (or a username) into a valid IAM org slug — lowercase, [a-z0-9-], collapsed, trimmed, bounded.
- RESERVED: refuse names that must never become a customer org — IAM system owners (admin/built-in/app) and the brand/staff orgs (hanzo/lux/zoo/pars), which the OrgGate routes to the admin host. Creating one would collide with a staff tenant or a system principal.
topup.go ports console2's app/billing/v1/topup/wallet/route.ts into the unified binary at POST /v1/console/topup/wallet (task #41). It is the verify-and-record seam for an HUSD wallet top-up: the browser sends an HUSD ERC-20 transfer to the treasury and posts the tx hash here; this handler reads the receipt from the Hanzo EVM, confirms it is a mined, successful HUSD Transfer(from → treasury, value), derives USD cents from the (18-decimal, USD-pegged) on-chain value, records it to commerce as an `husd` crypto payment, and returns the credited amount + balance.
THE CREDITED AMOUNT IS THE ON-CHAIN VALUE, never a client number — which is exactly why this MUST be a server handler and cannot collapse to a same-origin call. Two hardenings over the Node route:
- IDOR-safe: the credit lands on the VALIDATED caller's own org/user (the gateway-verified X-Org-Id/X-User-Id), never a client-supplied `userId`.
- S2S to commerce: recorded with the admin COMMERCE_SERVICE_TOKEN + the caller's X-Org-Id (the same service-to-service pattern clients/admin reads balances on), not by forwarding a browser cookie.
The EVM receipt is read over plain JSON-RPC (eth_getTransactionReceipt) — one well-known call + one well-known event, so the stdlib is sufficient and no EVM client dependency is pulled in.
Honest failure (no fabricated credit, ever): HUSD/treasury unconfigured (greenfield — HUSD not yet deployed) → 501; a missing/failed/non-HUSD-to-treasury tx → 400; the chain or commerce unreachable → 502.
waitlist.go ports console2's app/waitlist/route.ts — the "Join waitlist" CTA on coming-soon products — into the unified binary at POST /v1/console/waitlist (task #41). It is NOT a backend that owns the list: it is the session-gated, email-binding seam in front of the Hanzo Base waitlist plugin (the hanzoai/ waitlist pattern, a per-product SQLite-backed list at POST /v1/waitlist/join).
WHY IT IS A HANDLER (not a vanishing proxy). It does real work the SPA must not: it BINDS the recorded email to the VALIDATED session (the gateway-verified X-User-Email) so a signed-in user cannot enroll a third party (victim@othercorp) or forge "org X wants ERP" — the client-supplied email is only a fallback for an account whose token carries no email. The backend URL is server-only env (WAITLIST_URL, sourced from KMS by the deployment, never NEXT_PUBLIC); when it is unset the route is honestly 501 ("not open on this deployment yet"), never a fabricated confirmation.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
This section is empty.