Zoo had exactly one working OAuth client (zoo-cloud) and no declaration of it
anywhere. This ports the Lux shape into this repo: the brand-neutral mechanism
stays in hanzoai/iam (internal/provision), Zoo owns its DATA here, and
`make iam-provision` is the only supported way that data reaches the IdP.
ISSUER. id.zoo.network serves iss=https://zoolabs.id — host and issuer really do
disagree, and it is resolved in favour of zoolabs.id: IAM_ISSUER_MAP already
maps all three Zoo hosts to it, discovery agrees from either host, every sibling
brand's issuer is its <brand>.id apex (hanzo.id / lux.id / pars.id) with
id.<brand>.network as the alias, and zoo.id has no DNS at all so it cannot be
the issuer. The remaining defect is the DISCOVERY DOCUMENT (OIDC Discovery §4.3
wants issuer == fetch prefix), which is a frontend/ingress fix, not a data one.
Reasoning is in the file header, not just here.
Five clients, each declared only on evidence measured against the live surface:
zoo-cloud, zoo-wallet, zoo-dao, zoo-explore, zoo-id. Hosts that carry no OAuth
wiring, hosts served by shared Hanzo backends, and hosts with no DNS are listed
too, so each omission is a recorded decision.
Two defects fixed rather than described:
cert. The upsert copies req.Cert verbatim on CREATE, so an omitted cert means
an EMPTY cert, and a client with no cert cannot mint an id_token — the login
dies AFTER a successful code exchange. Four Zoo clients were in exactly that
state. Every app now names cert-zoo (convention cert-<org>: lux-cloud has
cert-lux, hanzo-cloud has cert-hanzo); for zoo-cloud it is a no-op.
cloud.zoo.network is no longer declared. It has an ingress but NO DNS record
at 1.1.1.1 or 8.8.8.8, so no browser can reach it; a redirect URI for a host
that cannot exist is the drift this document exists to prevent. Lux keeps
cloud.lux.network because that host does resolve.
Image pin moved forward to sha-f9813f6: sha-4bec5ef predates the `cert` field
and the strict parser rejects the document outright. f9813f6 is a strict
descendant of the public-client fixes and of 4b0f4e93, and is the newest sha-
tag in GHCR that resolves to a known commit.
PROVEN, not asserted:
- converge run twice; the five records are byte-identical apart from
updatedAt, and no client secret was issued (all five stay public/PKCE)
- real login as [email protected] -> authorization code -> PKCE token exchange with
NO client secret, on all five clients
- id_token minted for each: kid=cert-zoo, iss=https://zoolabs.id,
aud=<the client>, and the signature verifies against the published JWKS
- the dropped redirect now correctly 400s: invalid redirect_uri
Co-authored-by: Hanzo Dev <[email protected]>
Migrate Zoo from L2-on-Lux to sovereign L1. Per-env primary networkID
equals per-env EVM chainID (200200 / 200201 / 200202 / 200203).
Explicit httpPort + stakingPort per env (no collisions; the old 9680
dev/local clash is gone — now 9720 / 9730).
chain.yaml:
* networks.<env>.{networkID, primaryEvmChainID, httpPort, stakingPort,
genesisFile, rpcUrl, cluster, namespace} explicit per env
* runtime: block — dataDir / snapshotName / serviceLabel templates
that the lux CLI (pkg/brand) expands per (network, env)
Makefile: collapsed from 16 hardcoded targets (4 verbs × 4 envs with
NID_MAIN := 200200 duplication) to 6 parametric lines:
make up env=devnet ≡ lux up zoo/devnet
make cycle env=mainnet ≡ lux cycle zoo/mainnet
make cycle-all
Per-env constants live in chain.yaml — single source of truth.
deploy.sh removed: replaced by `lux <verb>` calls via Makefile.
LLM.md: documents the new sovereign-L1 model, CLI grammar
(`lux up/down/snap/cycle <network>/<env>`), chain.lock invariant,
per-brand DOKS cluster topology, and the zoo-private/universe sibling.