mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-10-25 02:31:30 +00:00
Two duplications, both removed. 1. The header's account cluster was an email in a <span> and a Sign out button. It is now <OrgSwitcher/> + <UserMenu/> from @luxfi/ui — the same controls the MPC dashboard and lux.market render, reading the same IAM verbs (get-account, get-organizations, get-organization-projects), with the same phone sheet, which this bar had no equivalent of at all. Signed out they offer sign-in rather than showing an empty list that reads as an account with no organizations. 2. lib/brand.ts held a SECOND host→brand table. It kept what is genuinely its own — logos, taglines, palettes, footer links, the ecosystem launcher, the API origins that answer — and now takes issuer, client id and IAM host from @luxfi/ui/white-label, which is the same resolver every Lux surface already uses. Two copies of one fact is two things to get wrong, and these two DID disagree: this table had Zoo on zoolabs.id (right) and the shared one on id.zoo.network (an alias whose own discovery document names zoolabs.id, so a client on it rejects every token). The shared table is fixed; this file no longer has an opinion to be wrong about. The bundler wiring for the gui engine comes from `withLuxUi` — the one place it lives, rather than a hand-rolled transpile list per app. `next build` passes (static export, every route emitted). `pnpm test` 26/26, including the assertion that lux.cloud + lux-cloud + https://lux.cloud/auth/callback — the one login that works today — is unchanged. Co-authored-by: Hanzo Dev <[email protected]>
301 lines
12 KiB
TypeScript
301 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
|
|
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. api.bootno.de, api.web3.hanzo.ai and
|
|
// api.cloud.lux.network are the SAME service behind three names — verified
|
|
// 2026-07-26, /v1/chains byte-identical across all three
|
|
// (sha256 2aa472c11e47d3f67e53be17fb24230fccc94630b485e6f3864d51fa2a4e1400).
|
|
// api.cloud.zoo.network and api.cloud.pars.network do NOT resolve, so Zoo and
|
|
// Pars point at the one origin that answers rather than at a derived name that
|
|
// would fail. Per-brand vanity origins can be added the day they exist.
|
|
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",
|
|
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",
|
|
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",
|
|
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: API,
|
|
wsUrl: WS,
|
|
statusUrl: "https://status.hanzo.ai",
|
|
networkName: "Hanzo Network",
|
|
networkUrl: "https://hanzo.ai",
|
|
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",
|
|
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)]
|
|
}
|