mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-10-25 02:31:30 +00:00
Hanzo CI/CD / cicd (push) Failing after 1h10m29s
CI/CD / cicd (push) Failing after 2h37m43s
A static export has no host when it prerenders, so BrandProvider renders the
tree only once the browser has resolved the host, pages that show the brand
are client components, the static head names no brand, and the title and
favicon come from the host. The chain page keeps its static params and
metadata on the server and renders its body on the client, with RPC endpoints
on the brand's API origin at /v1/rpc/{chain}/{network}.
Hanzo's API origin is web3.hanzo.ai itself. The node fleet view, its landing
strip and its nav link belong to the brand that operates a fleet, which is
Lux alone. llms.txt, one file for every host, names no brand and points at
each host's own /docs/api.
Marketing copy states no certification the company does not hold and no
unmeasured uptime, latency, volume or response-time figure. Every internal
link points at a page that exists, every catalog chain is prerendered, and
the routes test holds page bodies to zero dead links.
Co-authored-by: Hanzo Dev <[email protected]>
307 lines
12 KiB
TypeScript
307 lines
12 KiB
TypeScript
// White-label by HOSTNAME. One build, one image, every brand.
|
|
//
|
|
// The brand — identity, theme, API origin, IAM issuer and OIDC client — is a
|
|
// pure function of `window.location.hostname`. There is no per-deployment
|
|
// config: no NEXT_PUBLIC_BRAND, no injected `window.__BRAND_CATALOG__`, no
|
|
// fetched /config.json. Those were the same defect wearing three hats — a
|
|
// value chosen at deploy time, while ONE deployment serves every host, so
|
|
// whichever brand the last deploy picked was wrong for the other four.
|
|
//
|
|
// The catalog lives HERE, in source, because it is neither a secret nor an
|
|
// environment: it is a fact about which domain belongs to which network, and a
|
|
// fact belongs next to the code that reads it.
|
|
//
|
|
// Every issuer + client_id + callback triple below was verified against the
|
|
// live /v1/iam/oauth/authorize (HIP-0111) on 2026-07-26 — a registered pair
|
|
// answers 302, an unregistered redirect_uri answers 400:
|
|
//
|
|
// id.bootno.de bootnode-platform https://bootno.de/auth/callback 302
|
|
// hanzo.id hanzo-web3 https://web3.hanzo.ai/auth/callback 302
|
|
// lux.id lux-cloud https://lux.cloud/auth/callback 302
|
|
// zoolabs.id zoo-cloud https://zoo.cloud/auth/callback 302
|
|
// zoolabs.id zoo-cloud https://cloud.zoo.network/auth/callback 302
|
|
// pars.id pars-app https://cloud.pars.network/auth/callback 302
|
|
// zoolabs.id zoo-web3 https://cloud.zoo.network/auth/callback 400 ← retired
|
|
//
|
|
// Adding a host = one line in HOSTS. Adding a brand = one entry in BRANDS plus
|
|
// registering `https://<host>/auth/callback` on that brand's IAM. Nothing else
|
|
// in the app carries a brand string, an issuer, or a client id.
|
|
|
|
import { resolveWhiteLabel } from "@luxfi/ui/white-label"
|
|
|
|
export type BrandKey = "bootnode" | "lux" | "zoo" | "hanzo" | "pars"
|
|
|
|
export type BrandConfig = {
|
|
key: BrandKey
|
|
name: string // wordmark + document.title, e.g. "Lux Cloud"
|
|
shortName: string // compact refs, e.g. "Lux" ("Try Lux")
|
|
tagline: string
|
|
description: string
|
|
logo: string
|
|
logoIcon: string
|
|
logoWhite: string
|
|
favicon: string
|
|
// Is the mark a single-colour silhouette? Measured from the assets:
|
|
// bootnode-icon.svg is fill="currentColor" and lux-icon.svg is fill="white",
|
|
// so as <img> (an isolated document, where currentColor resolves to black)
|
|
// one is invisible on black and the other on white. Those two are painted in
|
|
// the current text colour via a CSS mask instead — which is also exactly what
|
|
// lux.cloud does with its own mark. Zoo's CMYK Venn, Hanzo's badge and Pars'
|
|
// gold star carry their own palette and are rendered as images, untouched.
|
|
monoMark: boolean
|
|
domain: string // canonical public host for this brand
|
|
apiUrl: string // bootnode control-plane origin
|
|
wsUrl: string
|
|
statusUrl: string
|
|
networkName: string // e.g. "Lux Network" — header/footer chrome
|
|
networkUrl: string
|
|
fleet: "lux" | null // the node fleet this brand operates, shown on the landing and at /fleet
|
|
skillsOrg: string // GitHub org for the skills repo
|
|
defaultTheme: "light" | "dark"
|
|
colors: {
|
|
primary: string
|
|
primaryForeground: string
|
|
accent: string // hero gradient start
|
|
accentEnd: string // hero gradient end
|
|
}
|
|
social: { twitter?: string; github?: string; discord?: string }
|
|
iam: {
|
|
url: string // OIDC origin the SDK runs discovery against
|
|
clientId: string // public PKCE client, always <org>-<app>
|
|
domain: string // bare host, for display
|
|
idLabel: string // identity brand, e.g. "Lux ID"
|
|
}
|
|
footerLinks: { name: string; href: string; external?: boolean }[]
|
|
// Ecosystem apps — retail-facing services linked from the console launcher.
|
|
ecosystemApps: {
|
|
name: string
|
|
href: string
|
|
description: string
|
|
icon: string // lucide icon name
|
|
category: "explore" | "defi" | "tools" | "governance"
|
|
}[]
|
|
}
|
|
|
|
// The bootnode control plane. Every brand without an origin of its own reaches
|
|
// the one service at api.bootno.de.
|
|
//
|
|
// Hanzo is served from its own host: web3.hanzo.ai routes /v1 to the same
|
|
// service, so the console calls its API on the origin it was loaded from. A
|
|
// second-level name such as api.web3.hanzo.ai has no edge certificate, and a
|
|
// Hanzo page has no reason to reach a bootno.de address.
|
|
const API = "https://api.bootno.de"
|
|
const WS = "wss://ws.bootno.de"
|
|
|
|
// Presentation only. The OIDC triple is attached below, from the one shared
|
|
// table — see CONSOLE_HOST.
|
|
const BRANDS: Record<BrandKey, Omit<BrandConfig, "iam">> = {
|
|
// DEFAULT. bootno.de, localhost, and any unmatched host resolve here.
|
|
bootnode: {
|
|
key: "bootnode",
|
|
name: "Bootnode",
|
|
shortName: "Bootnode",
|
|
tagline: "Blockchain Infrastructure for Developers",
|
|
description:
|
|
"The complete blockchain development platform. Multi-chain RPC, Token APIs, NFT APIs, Smart Wallets, Webhooks, and more.",
|
|
logo: "/logo/bootnode-logo.svg",
|
|
logoIcon: "/logo/bootnode-icon.svg",
|
|
logoWhite: "/logo/bootnode-logo-white.svg",
|
|
favicon: "/logo/bootnode-icon.svg",
|
|
monoMark: true,
|
|
domain: "bootno.de",
|
|
apiUrl: API,
|
|
wsUrl: WS,
|
|
statusUrl: "https://status.bootno.de",
|
|
networkName: "Bootnode",
|
|
networkUrl: "https://bootno.de",
|
|
fleet: null,
|
|
skillsOrg: "bootnode",
|
|
defaultTheme: "dark",
|
|
colors: { primary: "#f5f5f5", primaryForeground: "#000000", accent: "#a8a8ff", accentEnd: "#8bffd1" },
|
|
social: { github: "https://github.com/bootnode" },
|
|
footerLinks: [],
|
|
ecosystemApps: [],
|
|
},
|
|
|
|
lux: {
|
|
key: "lux",
|
|
name: "Lux Cloud",
|
|
shortName: "Lux",
|
|
tagline: "Blockchain Infrastructure",
|
|
description:
|
|
"Enterprise blockchain infrastructure for the Lux Network ecosystem. Multi-chain RPC, MPC wallets, bridges, DEX, and white-label chain deployment.",
|
|
logo: "/logo/lux-logo.svg",
|
|
logoIcon: "/logo/lux-icon.svg",
|
|
logoWhite: "/logo/lux-logo-white.svg",
|
|
favicon: "/logo/lux-icon.svg",
|
|
monoMark: true,
|
|
domain: "lux.cloud",
|
|
apiUrl: API,
|
|
wsUrl: WS,
|
|
statusUrl: "https://status.lux.network",
|
|
networkName: "Lux Network",
|
|
networkUrl: "https://lux.network",
|
|
fleet: "lux",
|
|
skillsOrg: "luxfi",
|
|
defaultTheme: "dark",
|
|
colors: { primary: "#f5f5f5", primaryForeground: "#000000", accent: "#a8a8ff", accentEnd: "#8bffd1" },
|
|
social: { github: "https://github.com/luxfi" },
|
|
footerLinks: [],
|
|
ecosystemApps: [],
|
|
},
|
|
|
|
// zoo.cloud + cloud.zoo.network. id.zoo.network is an ALIAS, not the issuer:
|
|
// its discovery document advertises issuer/authorize/token all on zoolabs.id
|
|
// (measured 2026-07-26), so pointing the SDK at zoolabs.id removes a hop and
|
|
// matches the `iss` the token is actually stamped with.
|
|
zoo: {
|
|
key: "zoo",
|
|
name: "Zoo Cloud",
|
|
shortName: "Zoo",
|
|
tagline: "Blockchain Infrastructure",
|
|
description:
|
|
"Enterprise blockchain infrastructure for the Zoo Network ecosystem. Multi-chain RPC, MPC wallets, bridges, DEX, and white-label chain deployment.",
|
|
logo: "/logo/zoo-logo.svg",
|
|
logoIcon: "/logo/zoo-icon.svg",
|
|
logoWhite: "/logo/zoo-logo-white.svg",
|
|
favicon: "/logo/zoo-icon.svg",
|
|
monoMark: false,
|
|
domain: "zoo.cloud",
|
|
apiUrl: API,
|
|
wsUrl: WS,
|
|
statusUrl: "https://status.zoo.network",
|
|
networkName: "Zoo Network",
|
|
networkUrl: "https://zoo.network",
|
|
fleet: null,
|
|
skillsOrg: "zooai",
|
|
defaultTheme: "dark",
|
|
colors: { primary: "#f5f5f5", primaryForeground: "#000000", accent: "#8bffd1", accentEnd: "#a8a8ff" },
|
|
social: { github: "https://github.com/zooai" },
|
|
footerLinks: [],
|
|
ecosystemApps: [],
|
|
},
|
|
|
|
hanzo: {
|
|
key: "hanzo",
|
|
name: "Hanzo",
|
|
shortName: "Hanzo",
|
|
tagline: "Blockchain Infrastructure",
|
|
description:
|
|
"Enterprise blockchain infrastructure for the Hanzo Network ecosystem. Multi-chain RPC, MPC wallets, bridges, DEX, and white-label chain deployment.",
|
|
logo: "/logo/hanzo-logo.svg",
|
|
logoIcon: "/logo/hanzo-icon.svg",
|
|
logoWhite: "/logo/hanzo-logo-white.svg",
|
|
favicon: "/logo/hanzo-icon.svg",
|
|
monoMark: false,
|
|
domain: "web3.hanzo.ai",
|
|
apiUrl: "https://web3.hanzo.ai",
|
|
wsUrl: "wss://web3.hanzo.ai",
|
|
statusUrl: "https://status.hanzo.ai",
|
|
networkName: "Hanzo Network",
|
|
networkUrl: "https://hanzo.ai",
|
|
fleet: null,
|
|
skillsOrg: "hanzoai",
|
|
defaultTheme: "dark",
|
|
colors: { primary: "#f5f5f5", primaryForeground: "#000000", accent: "#a8a8ff", accentEnd: "#ff8bc0" },
|
|
social: { github: "https://github.com/hanzoai" },
|
|
footerLinks: [],
|
|
ecosystemApps: [],
|
|
},
|
|
|
|
pars: {
|
|
key: "pars",
|
|
name: "Pars",
|
|
shortName: "Pars",
|
|
tagline: "Blockchain Infrastructure",
|
|
description:
|
|
"Enterprise blockchain infrastructure for the Pars Network ecosystem. Multi-chain RPC, MPC wallets, bridges, DEX, and white-label chain deployment.",
|
|
logo: "/logo/pars-logo.svg",
|
|
logoIcon: "/logo/pars-icon.svg",
|
|
logoWhite: "/logo/pars-logo-white.svg",
|
|
favicon: "/logo/pars-icon.svg",
|
|
monoMark: false,
|
|
domain: "cloud.pars.network",
|
|
apiUrl: API,
|
|
wsUrl: WS,
|
|
statusUrl: "https://status.pars.network",
|
|
networkName: "Pars Network",
|
|
networkUrl: "https://pars.network",
|
|
fleet: null,
|
|
skillsOrg: "parsdao",
|
|
defaultTheme: "dark",
|
|
colors: { primary: "#f5f5f5", primaryForeground: "#000000", accent: "#ffd18b", accentEnd: "#ff8bc0" },
|
|
social: { github: "https://github.com/parsdao" },
|
|
footerLinks: [],
|
|
ecosystemApps: [],
|
|
},
|
|
}
|
|
|
|
// Host → brand. Matched EXACTLY or as a dot-suffix, never as a substring: a
|
|
// substring match ("lux.cloud" is a substring of "lux.cloud.attacker.example")
|
|
// hands an attacker-controlled origin a real brand's IAM client id. First match
|
|
// wins, so the more specific host is listed first.
|
|
const HOSTS: { host: string; key: BrandKey }[] = [
|
|
{ host: "bootno.de", key: "bootnode" },
|
|
{ host: "web3.hanzo.ai", key: "hanzo" },
|
|
{ host: "cloud.hanzo.network", key: "hanzo" },
|
|
{ host: "lux.cloud", key: "lux" },
|
|
{ host: "zoo.cloud", key: "zoo" },
|
|
{ host: "zoo.network", key: "zoo" }, // covers cloud.zoo.network
|
|
{ host: "pars.network", key: "pars" }, // covers cloud.pars.network
|
|
]
|
|
|
|
const DEFAULT_KEY: BrandKey = "bootnode"
|
|
|
|
// The host each brand's console is REGISTERED under — the one key into the
|
|
// shared white-label table.
|
|
//
|
|
// Issuer, client id and IAM host are NOT this file's to know. They are the same
|
|
// answer @luxfi/ui/white-label already gives every Lux surface from the Host
|
|
// header, and a second copy here is a second thing to get wrong — which it was:
|
|
// this table and that one disagreed about Zoo's issuer, and the surfaces on the
|
|
// losing side of that disagreement reject every token they are handed.
|
|
//
|
|
// This file keeps what is genuinely its own: logos, taglines, palettes, footer
|
|
// links, the ecosystem launcher, and the API origins that answer.
|
|
const CONSOLE_HOST: Record<BrandKey, string> = {
|
|
bootnode: "bootno.de",
|
|
lux: "lux.cloud",
|
|
zoo: "zoo.cloud",
|
|
hanzo: "web3.hanzo.ai",
|
|
pars: "cloud.pars.network",
|
|
}
|
|
|
|
/** Presentation + the shared OIDC triple, resolved once per brand. */
|
|
const RESOLVED = Object.fromEntries(
|
|
(Object.keys(BRANDS) as BrandKey[]).map((key) => {
|
|
const wl = resolveWhiteLabel(CONSOLE_HOST[key])
|
|
return [
|
|
key,
|
|
{
|
|
...BRANDS[key],
|
|
iam: { url: wl.issuer, clientId: wl.clientId, domain: wl.iamDomain, idLabel: `${wl.name} ID` },
|
|
},
|
|
]
|
|
})
|
|
) as Record<BrandKey, BrandConfig>
|
|
|
|
/**
|
|
* Resolve the brand key for a hostname. Pass one explicitly to resolve for a
|
|
* host other than the current page; omit it in the browser to use
|
|
* `window.location.hostname`. During the static export there IS no host, so
|
|
* this returns the default and BrandProvider re-resolves after hydration.
|
|
*/
|
|
export function getBrandKey(hostname?: string): BrandKey {
|
|
const host = (hostname ?? (typeof window === "undefined" ? "" : window.location.hostname)).toLowerCase()
|
|
if (!host) return DEFAULT_KEY
|
|
for (const rule of HOSTS) {
|
|
if (host === rule.host || host.endsWith("." + rule.host)) return rule.key
|
|
}
|
|
return DEFAULT_KEY
|
|
}
|
|
|
|
/** The active brand. Synchronous and total — every key has a complete entry. */
|
|
export function getBrand(hostname?: string): BrandConfig {
|
|
return RESOLVED[getBrandKey(hostname)]
|
|
}
|