mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-09-30 20:46:32 +00:00
fix(web): one way to resolve IAM — the brand catalog, not a global env
Four of the five hosts this deployment serves could not sign in at all, and
the fifth signed in under the wrong company's name.
bootnode-web serves bootno.de, web3.hanzo.ai, cloud.hanzo.network,
cloud.pars.network and cloud.zoo.network from ONE image. The IAM issuer and
client_id were set by NEXT_PUBLIC_IAM_URL / NEXT_PUBLIC_IAM_CLIENT_ID, which
are inlined into the client bundle at build time — a single global value that
every host shares. It cannot express per-host white-labeling, and it silently
outranked the correct per-brand value because lib/iam.ts read
`process.env.X || brand.iam.X`.
Measured against the live /v1/iam/oauth/authorize (302 = registered pair,
400 = unregistered redirect_uri), with the env in force today:
bootno.de 302 (but branded "Sign in to Hanzo ID")
web3.hanzo.ai 400
cloud.hanzo.network 400
cloud.pars.network 400
cloud.zoo.network 400
The env pinned client_id=bootnode-platform for all five, and that client only
carries bootno.de's callback — so every other host was rejected.
The white-label mechanism the app already has is the runtime brand catalog:
hostname -> brand key -> BrandConfig, resolved after hydration. It was never
usable, for two reasons, both fixed here:
* getBrand() REPLACED the default with the catalog entry, so any partial
entry yielded a BrandConfig with undefined name/colors and crashed
rendering. Entries are now merged over the built-in default, so a brand
that only needs a different issuer writes only its `iam` block.
* public/config.json shipped empty with a comment claiming the deploy
overwrites it. Nothing does — /config.json 404s in production, so every
host fell back to bootnode. The real catalog now ships in the image.
Removes both IAM env knobs entirely (app, Dockerfile ARG/ENV). The Dockerfile
defaults were also both wrong — iam.bootno.de serves no TLS (525) and
bootnode-web is not a registered client — so any rebuild taking the defaults
would have broken sign-in outright.
After this change, each host resolves its own verified issuer:
bootno.de id.bootno.de bootnode-platform 302
web3.hanzo.ai iam.hanzo.ai hanzo-web3 302
cloud.pars.network pars.id pars-app 302
cloud.hanzo.network and cloud.zoo.network still answer 400: their callbacks
are not on the respective clients' redirect lists. Both hosts also do not
currently resolve, so this is not a regression — registering those two
redirect URIs in IAM is what remains before they can go live.
Zoo's issuer is zoolabs.id, not zoo.id: zoo.id serves no OIDC discovery and
no TLS.
This commit is contained in:
+12
-6
@@ -20,19 +20,25 @@ COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Production build-time environment variables
|
||||
# Brand is auto-detected from domain at runtime (see lib/brand.ts)
|
||||
# Override via K8s env vars: NEXT_PUBLIC_BRAND, NEXT_PUBLIC_API_URL, etc.
|
||||
# Brand is resolved from the hostname at RUNTIME against the injected brand
|
||||
# catalog (lib/brand.ts + /config.json). That is the only white-label mechanism.
|
||||
#
|
||||
# There are deliberately NO IAM build args. One image serves every white-label
|
||||
# host, so a build-time NEXT_PUBLIC_IAM_URL / _CLIENT_ID is one global value
|
||||
# shared by all brands — it cannot express per-host white-labeling, and because
|
||||
# NEXT_PUBLIC_* is inlined into the client bundle it silently outranks the
|
||||
# correct per-brand value with no way to fix it at deploy time. The defaults
|
||||
# that used to live here were also both wrong: iam.bootno.de does not serve TLS
|
||||
# (525) and bootnode-web is not the registered client, so any rebuild taking
|
||||
# the defaults would have broken sign-in outright. Issuer + client_id now come
|
||||
# from the brand catalog only.
|
||||
ARG NEXT_PUBLIC_BRAND=bootnode
|
||||
ARG NEXT_PUBLIC_API_URL=https://api.bootno.de
|
||||
ARG NEXT_PUBLIC_WS_URL=wss://ws.bootno.de
|
||||
ARG NEXT_PUBLIC_IAM_URL=https://iam.bootno.de
|
||||
ARG NEXT_PUBLIC_IAM_CLIENT_ID=bootnode-web
|
||||
|
||||
ENV NEXT_PUBLIC_BRAND=$NEXT_PUBLIC_BRAND
|
||||
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
|
||||
ENV NEXT_PUBLIC_WS_URL=$NEXT_PUBLIC_WS_URL
|
||||
ENV NEXT_PUBLIC_IAM_URL=$NEXT_PUBLIC_IAM_URL
|
||||
ENV NEXT_PUBLIC_IAM_CLIENT_ID=$NEXT_PUBLIC_IAM_CLIENT_ID
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN corepack enable pnpm && pnpm run build
|
||||
|
||||
+25
-1
@@ -179,7 +179,31 @@ function getBrandKey(): string {
|
||||
export function getBrand(): BrandConfig {
|
||||
const cat = activeCatalog()
|
||||
const key = getBrandKey()
|
||||
return cat.brands?.[key] ?? cat.brands?.[DEFAULT_KEY] ?? BOOTNODE_DEFAULT
|
||||
const entry = cat.brands?.[key] ?? cat.brands?.[DEFAULT_KEY]
|
||||
// Catalog entries are OVERRIDES on the complete built-in default, not
|
||||
// replacements. A brand that only needs a different IAM issuer writes just
|
||||
// its `iam` block; everything it does not mention (name, colours, logo, urls)
|
||||
// still resolves. Replacing outright meant any partial entry produced a
|
||||
// BrandConfig with undefined `colors`/`name` and crashed rendering, which is
|
||||
// why no catalog was ever shipped and every host fell back to bootnode.
|
||||
return entry ? mergeBrand(BOOTNODE_DEFAULT, entry) : BOOTNODE_DEFAULT
|
||||
}
|
||||
|
||||
// One level of nesting is enough for BrandConfig: the nested objects (iam,
|
||||
// colors, social) are flat records, and the arrays (footerLinks, ecosystemApps)
|
||||
// are replaced wholesale rather than concatenated so a brand can legitimately
|
||||
// present an empty footer.
|
||||
function mergeBrand(base: BrandConfig, over: Partial<BrandConfig>): BrandConfig {
|
||||
const out = { ...base } as Record<string, unknown>
|
||||
for (const [k, v] of Object.entries(over)) {
|
||||
if (v === undefined || v === null) continue
|
||||
const b = (base as Record<string, unknown>)[k]
|
||||
out[k] =
|
||||
!Array.isArray(v) && typeof v === "object" && !Array.isArray(b) && typeof b === "object" && b
|
||||
? { ...(b as object), ...(v as object) }
|
||||
: v
|
||||
}
|
||||
return out as BrandConfig
|
||||
}
|
||||
|
||||
// Export brand key for conditional rendering / per-brand local maps
|
||||
|
||||
+8
-5
@@ -18,11 +18,14 @@ import { getBrand } from "@/lib/brand"
|
||||
const RETURN_URL_KEY = "bootnode_return_url"
|
||||
|
||||
export function getIam(): IAM {
|
||||
const brand = getBrand()
|
||||
const serverUrl =
|
||||
process.env.NEXT_PUBLIC_IAM_URL || brand.iam.url
|
||||
const clientId =
|
||||
process.env.NEXT_PUBLIC_IAM_CLIENT_ID || brand.iam.clientId
|
||||
// The brand catalog is the ONE source for issuer + client. There is no env
|
||||
// override, by design: this deployment serves MANY white-label hosts, so a
|
||||
// build-time NEXT_PUBLIC_IAM_URL is a single global value that every brand
|
||||
// would share — it cannot express per-host white-labeling, and it silently
|
||||
// outranked the correct per-brand default. That is exactly what shipped:
|
||||
// bootno.de/dashboard bounced to hanzo.id and asked a Bootnode customer to
|
||||
// "Sign in to Hanzo ID" under the Hanzo mark.
|
||||
const { url: serverUrl, clientId } = getBrand().iam
|
||||
return new IAM({
|
||||
serverUrl,
|
||||
clientId,
|
||||
|
||||
+47
-3
@@ -1,6 +1,50 @@
|
||||
{
|
||||
"_comment": "Runtime brand catalog — NEUTRAL PLACEHOLDER. The deploy (universe / CF-Pages) OVERWRITES this file (and/or injects the same JSON as window.__BRAND_CATALOG__) with the full multi-brand catalog. Shipping empty `brands` makes the app fall back to bootnode's built-in neutral default. Shape mirrors the @hanzo/id SPA's /config.json contract. See lib/brand.ts (BrandCatalog / BrandConfig).",
|
||||
"_comment": "Runtime brand catalog — the ONE white-label mechanism, shipped in the image. Hostname is matched against `hosts` (first substring match wins) to pick a brand key, and that entry is merged OVER the built-in bootnode default (lib/brand.ts getBrand/mergeBrand), so each entry lists only what differs. There is deliberately no NEXT_PUBLIC_IAM_URL/_CLIENT_ID: one image serves every host below, so a build-time value is a single global that all brands share and cannot express per-host white-labeling — that is precisely how every host came to point at hanzo.id. Every issuer + client_id + callback triple below was verified against the live /v1/iam/oauth/authorize (HIP-0111): a registered pair answers 302, an unregistered redirect_uri answers 400.",
|
||||
"defaultKey": "bootnode",
|
||||
"hosts": [],
|
||||
"brands": {}
|
||||
"hosts": [
|
||||
{ "match": "bootno.de", "key": "bootnode" },
|
||||
{ "match": "web3.hanzo.ai", "key": "hanzo" },
|
||||
{ "match": "cloud.hanzo.network", "key": "hanzo" },
|
||||
{ "match": "cloud.pars.network", "key": "pars" },
|
||||
{ "match": "cloud.zoo.network", "key": "zoo" }
|
||||
],
|
||||
"brands": {
|
||||
"bootnode": {
|
||||
"name": "Bootnode",
|
||||
"domain": "bootno.de",
|
||||
"iam": {
|
||||
"url": "https://id.bootno.de",
|
||||
"clientId": "bootnode-platform",
|
||||
"domain": "id.bootno.de"
|
||||
}
|
||||
},
|
||||
"hanzo": {
|
||||
"name": "Hanzo",
|
||||
"domain": "web3.hanzo.ai",
|
||||
"iam": {
|
||||
"url": "https://iam.hanzo.ai",
|
||||
"clientId": "hanzo-web3",
|
||||
"domain": "iam.hanzo.ai"
|
||||
}
|
||||
},
|
||||
"pars": {
|
||||
"name": "Pars",
|
||||
"domain": "cloud.pars.network",
|
||||
"iam": {
|
||||
"url": "https://pars.id",
|
||||
"clientId": "pars-app",
|
||||
"domain": "pars.id"
|
||||
}
|
||||
},
|
||||
"zoo": {
|
||||
"_note": "zoolabs.id, NOT zoo.id — zoo.id serves no OIDC discovery and no TLS. zoo-web3 IS a registered client on zoolabs.id (302; an unknown client answers 400), but cloud.zoo.network/auth/callback is not yet on its redirect list (400) and that host currently does not resolve. Register the callback in IAM before bringing the host up.",
|
||||
"name": "Zoo",
|
||||
"domain": "cloud.zoo.network",
|
||||
"iam": {
|
||||
"url": "https://zoolabs.id",
|
||||
"clientId": "zoo-web3",
|
||||
"domain": "zoolabs.id"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user