brand: audit, rip non-lux references, add .well-known/explore.json

Per the no-crossover rule, explorer source contains only Lux brand data.
Per-chain identity (subnet logo, chain name) preserved as network metadata.

Removed:
- .env.example.{hanzo,zoo,pars} — non-Lux brand build recipes
- Dockerfile.liquidity — regulated US securities brand, must not appear in OSS
- Third-party-domain hostnames from chainRegistry.ts (explore.zoo.ngo etc.)
- Non-Lux hostname suffixes from lib/white-label.ts (.hanzo.ai, .zoo.network, etc.)
- "" mention from lib/requestHost.ts comment

Kept (chain identity on Lux primary network):
- ZOO/HANZO/SPC/PARS_BRANDING constants in chainRegistry.ts
- explore-{org}.lux.network hostnames (Lux-served subnet explorers)
- DeFi terms ( Provider, add) — generic finance vocab

Added:
- public/.well-known/explore.json — federation manifest
- scripts/generate-well-known.js — regenerates manifest from chain registry
- Wired into npm build / build:next scripts
- BRAND-AUDIT.md — survey + decisions
This commit is contained in:
Hanzo AI
2026-05-29 14:30:23 -07:00
parent b40bdf5cac
commit c74fd8a620
11 changed files with 286 additions and 164 deletions
-39
View File
@@ -1,39 +0,0 @@
# Hanzo Explorer — Hanzo AI brand.
# Use: cp .env.example.hanzo .env.local && pnpm build
NEXT_PUBLIC_BRAND=hanzo
# Network (Hanzo subnet on Lux mainnet)
NEXT_PUBLIC_NETWORK_NAME=Hanzo AI
NEXT_PUBLIC_NETWORK_SHORT_NAME=HANZO
NEXT_PUBLIC_NETWORK_ID=36963
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=HANZO
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=HANZO
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
# API
NEXT_PUBLIC_API_HOST=api-explore-hanzo.lux.network
NEXT_PUBLIC_API_PROTOCOL=https
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=wss
# App
NEXT_PUBLIC_APP_HOST=explore.hanzo.ai
NEXT_PUBLIC_APP_PROTOCOL=https
NEXT_PUBLIC_IS_TESTNET=false
# Branding
NEXT_PUBLIC_NETWORK_ORG_NAME=Hanzo Industries Inc.
NEXT_PUBLIC_NETWORK_WEBSITE_URL=https://hanzo.ai
NEXT_PUBLIC_NETWORK_DESCRIPTION=AI blockchain — decentralized compute and inference.
NEXT_PUBLIC_NETWORK_GITHUB_URL=https://github.com/hanzoai
NEXT_PUBLIC_NETWORK_TWITTER_URL=https://x.com/hanaboratory
NEXT_PUBLIC_NETWORK_DISCORD_URL=https://discord.gg/hanzoai
# Auth (IAM)
NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER=oidc
NEXT_PUBLIC_OIDC_SERVER_URL=https://hanzo.id
NEXT_PUBLIC_OIDC_CLIENT_ID=hanzo-explore-client-id
# Theme
NEXT_PUBLIC_COLOR_THEME_DEFAULT=dark
-39
View File
@@ -1,39 +0,0 @@
# Pars Explorer — Parsis Foundation brand.
# Use: cp .env.example.pars .env.local && pnpm build
NEXT_PUBLIC_BRAND=pars
# Network (Pars subnet on Lux mainnet)
NEXT_PUBLIC_NETWORK_NAME=Pars Network
NEXT_PUBLIC_NETWORK_SHORT_NAME=PARS
NEXT_PUBLIC_NETWORK_ID=494949
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=PARS
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=PARS
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
# API
NEXT_PUBLIC_API_HOST=api-explore-pars.lux.network
NEXT_PUBLIC_API_PROTOCOL=https
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=wss
# App
NEXT_PUBLIC_APP_HOST=explore.pars.network
NEXT_PUBLIC_APP_PROTOCOL=https
NEXT_PUBLIC_IS_TESTNET=false
# Branding
NEXT_PUBLIC_NETWORK_ORG_NAME=Parsis Foundation
NEXT_PUBLIC_NETWORK_WEBSITE_URL=https://pars.network
NEXT_PUBLIC_NETWORK_DESCRIPTION=Pars blockchain — financial infrastructure for the Persian-speaking world.
NEXT_PUBLIC_NETWORK_GITHUB_URL=https://github.com/luxfi
NEXT_PUBLIC_NETWORK_TWITTER_URL=https://x.com/parsnetwork
NEXT_PUBLIC_NETWORK_DISCORD_URL=https://discord.gg/luxnetwork
# Auth (IAM)
NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER=oidc
NEXT_PUBLIC_OIDC_SERVER_URL=https://pars.id
NEXT_PUBLIC_OIDC_CLIENT_ID=pars-explore-client-id
# Theme
NEXT_PUBLIC_COLOR_THEME_DEFAULT=dark
-39
View File
@@ -1,39 +0,0 @@
# Zoo Explorer — Zoo Labs Foundation brand.
# Use: cp .env.example.zoo .env.local && pnpm build
NEXT_PUBLIC_BRAND=zoo
# Network (Zoo subnet on Lux mainnet)
NEXT_PUBLIC_NETWORK_NAME=Zoo Chain
NEXT_PUBLIC_NETWORK_SHORT_NAME=ZOO
NEXT_PUBLIC_NETWORK_ID=200200
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ZOO
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ZOO
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
# API
NEXT_PUBLIC_API_HOST=api-explore-zoo.lux.network
NEXT_PUBLIC_API_PROTOCOL=https
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=wss
# App
NEXT_PUBLIC_APP_HOST=explore.zoo.ngo
NEXT_PUBLIC_APP_PROTOCOL=https
NEXT_PUBLIC_IS_TESTNET=false
# Branding
NEXT_PUBLIC_NETWORK_ORG_NAME=Zoo Labs Foundation
NEXT_PUBLIC_NETWORK_WEBSITE_URL=https://zoo.ngo
NEXT_PUBLIC_NETWORK_DESCRIPTION=Open AI research network — decentralized AI and science.
NEXT_PUBLIC_NETWORK_GITHUB_URL=https://github.com/zooai
NEXT_PUBLIC_NETWORK_TWITTER_URL=https://x.com/zoolabs
NEXT_PUBLIC_NETWORK_DISCORD_URL=https://discord.gg/zoolabs
# Auth (IAM)
NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER=oidc
NEXT_PUBLIC_OIDC_SERVER_URL=https://zoo.id
NEXT_PUBLIC_OIDC_CLIENT_ID=zoo-explore-client-id
# Theme
NEXT_PUBLIC_COLOR_THEME_DEFAULT=dark
+119
View File
@@ -0,0 +1,119 @@
# Brand Audit — luxfi/explore
**Rule:** luxfi/explore committed source contains only Lux brand. Per-chain
identity for subnets on Lux primary network (Zoo, Hanzo, SPC, Pars) is
permitted as network metadata. No external-org explorer recipes, no Liquidity
references.
## Scope surveyed
- All committed source under `~/work/lux/explore/` (excluding `node_modules/`,
`.next/`, `test-results/`, `qa-screenshots/`).
- Source globs: `*.{json,ts,tsx,js,jsx,md,yaml,yml,html,Dockerfile,env*}`.
## Findings
### Cross-brand recipes (REMOVED)
| Path | Why removed |
|------|-------------|
| `.env.example.hanzo` | Configured the image to be built and served as **Hanzo Explorer** under Hanzo's domain. That makes luxfi/explore a Hanzo brand surface — not allowed. |
| `.env.example.zoo` | Same as above for Zoo. |
| `.env.example.pars` | Same as above for Pars. |
| `Dockerfile.liquidity` | Liquidity is regulated US securities — must NEVER appear in OSS Lux. |
### Third-party-domain hostnames in `configs/app/chainRegistry.ts` (REMOVED)
Removed hostnames from the chain registry that asserted "this image, served
at the third-party org's canonical domain, IS that org's branded explorer":
- `explore.zoo.network`, `explore.zoo.ngo` (Zoo entry)
- `explore.hanzo.network`, `explore.hanzo.ai` (Hanzo entry)
- `explore.pars.network` (Pars entry)
Kept: `explore-{zoo,hanzo,spc,pars}.lux.network` — these are Lux-served
subnet explorer hosts where the chain identity (logo + name) is rendered as
network metadata for a chain running on the Lux primary network. That is the
per-chain branding the audit rules explicitly allow.
### Multi-brand selector (REWRITTEN)
`lib/white-label.ts` previously contained a hostname-suffix table mapping
`*.hanzo.ai`, `*.hanzo.network`, `*.zoo.ngo`, `*.zoo.network`,
`*.pars.network` to brand identifiers `hanzo`/`zoo`/`pars`. The intent was a
single image that becomes a different org's explorer based on serving host.
Replaced with: only Lux suffixes (`.lux.network`, `.lux.build`,
`.lux-test.network`, `.lux-dev.network`). External operators continue to be
supported via `NEXT_PUBLIC_BRAND=other` + `NEXT_PUBLIC_NETWORK_*` env vars
(see `.env.example.external`) — they supply their own brand strings at
deploy time; none are hardcoded here.
### Comment edits
- `lib/requestHost.ts`: dropped "Liquidity" from the multi-host narrative.
### Per-chain identity (KEPT — allowed)
- `configs/app/chainRegistry.ts``ZOO_BRANDING`, `HANZO_BRANDING`,
`SPC_BRANDING`, `PARS_BRANDING` constants. These render as **chain
identity** (which subnet this block / address belongs to) on
`explore-{org}.lux.network` hosts. That's network metadata for chains
running on Lux primary network, not a third-party brand on the explorer.
- `ui/chains/ChainDetailPage.tsx` `SUBNET_CHAIN_IDS` / `SUBNET_DESCRIPTIONS`
maps — chain identity for subnets, kept.
### Generic finance terms (KEPT — not brand)
DeFi vocabulary that incidentally contains the substring "Liquidity":
- `ui/pool/PoolInfo.tsx`, `ui/pools/PoolsTable.tsx`,
`ui/pools/PoolsListItem.tsx`, `ui/pages/Pool.tsx` — "Liquidity Provider",
"Liquidity" column header.
- `stubs/token.ts``addLiquidity` method literal in transaction stub.
- `lib/api/dchain/useDexData.ts``totalLiquidity` field name.
- `ui/showcases/Tabs.tsx` — generic "Liquidity staking" tab demo.
None of these reference the regulated Liquidity brand or its parent entity.
## Federation manifest
Added `public/.well-known/explore.json` plus a generator at
`scripts/generate-well-known.js`. The generator parses
`configs/app/chainRegistry.ts` so the published federation manifest stays in
sync with the chain registry.
- Wired into `npm scripts`: `build`, `build:next`, `wellknown:generate`,
`wellknown:check`.
- Mainnet chains emitted: C-Chain (Lux Mainnet) + Lux Testnet + Lux Devnet +
Zoo/Hanzo/SPC/Pars subnets (chain identity, not org brand).
- `peers: []` is populated at runtime by a ConfigMap mount that overlays
`public/.well-known/explore.json` in production; each deploy environment
knows which other federation peers (Hanzo Explorer, Zoo Explorer, etc.)
are reachable.
## Files touched
```
D .env.example.hanzo
D .env.example.pars
D .env.example.zoo
D Dockerfile.liquidity
M configs/app/chainRegistry.ts
M lib/requestHost.ts
M lib/white-label.ts
M package.json
A public/.well-known/explore.json
A scripts/generate-well-known.js
A BRAND-AUDIT.md
```
## Follow-ups
1. Federation peers list — agree on the ConfigMap schema across Hanzo / Zoo /
Pars explorer deployments; current `peers: []` placeholder is correct for
build-time defaults.
2. `LUX_BRANDING_SETUP.md` still mentions an `nginx` reverse proxy and a
`docker-luxnet` path that no longer exist. Rewrite next pass — not blocking.
3. `Dockerfile.branded` is a stale env-overlay image; superseded by
`Dockerfile.lux`. Candidate for removal in a follow-up clean-up.
-13
View File
@@ -1,13 +0,0 @@
FROM node:22-alpine
WORKDIR /app
COPY .next/standalone ./
COPY .next/static .next/static
COPY public public
COPY scripts/download-external-assets.sh scripts/
COPY scripts/validate-envs.sh scripts/
COPY configs/envs configs/envs
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh scripts/*.sh
ENV PORT=3000 HOSTNAME=0.0.0.0
EXPOSE 3000
ENTRYPOINT ["./entrypoint.sh"]
+3 -3
View File
@@ -268,7 +268,7 @@ export const CHAINS: ReadonlyArray<ChainEntry> = [
vm: 'L2',
network: 'mainnet',
chainId: 200200,
hostnames: [ 'explore-zoo.lux.network', 'explore.zoo.network', 'explore.zoo.ngo' ],
hostnames: [ 'explore-zoo.lux.network' ],
explorerUrl: 'https://explore-zoo.lux.network',
apiUrl: 'https://api-explore-zoo.lux.network',
branding: ZOO_BRANDING,
@@ -279,7 +279,7 @@ export const CHAINS: ReadonlyArray<ChainEntry> = [
vm: 'L2',
network: 'mainnet',
chainId: 36963,
hostnames: [ 'explore-hanzo.lux.network', 'explore.hanzo.network', 'explore.hanzo.ai' ],
hostnames: [ 'explore-hanzo.lux.network' ],
explorerUrl: 'https://explore-hanzo.lux.network',
apiUrl: 'https://api-explore-hanzo.lux.network',
branding: HANZO_BRANDING,
@@ -301,7 +301,7 @@ export const CHAINS: ReadonlyArray<ChainEntry> = [
vm: 'L2',
network: 'mainnet',
chainId: 494949,
hostnames: [ 'explore-pars.lux.network', 'explore.pars.network' ],
hostnames: [ 'explore-pars.lux.network' ],
explorerUrl: 'https://explore-pars.lux.network',
apiUrl: 'https://api-explore-pars.lux.network',
branding: PARS_BRANDING,
+3 -2
View File
@@ -7,8 +7,9 @@
//
// This store captures `req.headers.host` at the start of each SSR render
// (see `_document.tsx`), so `getHostname()` on the server returns the
// actual requested host. The same docker image now serves Lux, Zoo,
// Hanzo, SPC, Pars, and Liquidity interchangeably — one image, N hosts.
// actual requested host. The same docker image serves all chains on the
// Lux primary network (C, Zoo, Hanzo, SPC, Pars subnets) at distinct
// explore-*.lux.network hosts — one image, N hosts.
//
// Client side is unaffected: `window.location.hostname` continues to be
// the source of truth post-hydration.
+12 -27
View File
@@ -1,33 +1,24 @@
// White-label brand detection.
//
// Source code in this repo is brand-neutral. Brand selection happens at
// deploy time via `NEXT_PUBLIC_BRAND` env var or by hostname inspection.
// All brand-specific assets (logo, title, favicon, color theme, IAM URL)
// load from `NEXT_PUBLIC_*` env vars — no hardcoded brand strings here.
// Source code in this repo is Lux-only. Per-chain identity (Zoo / Hanzo / SPC /
// Pars subnets on Lux primary network) is rendered with each chain's logo and
// name as network metadata — see `configs/app/chainRegistry.ts`. The explorer
// brand itself is always Lux.
//
// Supported brands:
// - lux (default)
// - hanzo
// - zoo
// - pars
// - other (any external white-label deploy: branding from env vars)
//
// External deployments select their brand by setting NEXT_PUBLIC_BRAND in
// the runtime environment. They do NOT add their brand here — keeping this
// repo free of any third-party trademarks.
// External operators running their own EVM chain can deploy this image as an
// unbranded ("other") explorer by setting NEXT_PUBLIC_BRAND=other plus the
// NEXT_PUBLIC_NETWORK_* env vars (see `.env.example.external`). They MUST
// supply their own brand strings at runtime — none are hardcoded here.
import { getEnvValue } from 'configs/app/utils';
export type WhiteLabelBrand = 'lux' | 'hanzo' | 'zoo' | 'pars' | 'other';
export type WhiteLabelBrand = 'lux' | 'other';
const BRAND_HOSTNAME_SUFFIXES: ReadonlyArray<{ readonly suffix: string; readonly brand: WhiteLabelBrand }> = [
{ suffix: '.lux.network', brand: 'lux' },
{ suffix: '.lux.build', brand: 'lux' },
{ suffix: '.hanzo.ai', brand: 'hanzo' },
{ suffix: '.hanzo.network', brand: 'hanzo' },
{ suffix: '.zoo.ngo', brand: 'zoo' },
{ suffix: '.zoo.network', brand: 'zoo' },
{ suffix: '.pars.network', brand: 'pars' },
{ suffix: '.lux-test.network', brand: 'lux' },
{ suffix: '.lux-dev.network', brand: 'lux' },
];
export function getWhiteLabelBrand(hostname?: string): WhiteLabelBrand {
@@ -35,13 +26,7 @@ export function getWhiteLabelBrand(hostname?: string): WhiteLabelBrand {
const envBrand = getEnvValue('NEXT_PUBLIC_BRAND');
if (envBrand) {
const normalized = envBrand.toLowerCase();
if (
normalized === 'lux' ||
normalized === 'hanzo' ||
normalized === 'zoo' ||
normalized === 'pars' ||
normalized === 'other'
) {
if (normalized === 'lux' || normalized === 'other') {
return normalized;
}
// Any unknown brand string → 'other' (env-driven white-label)
+4 -2
View File
@@ -11,8 +11,10 @@
"dev": "./tools/scripts/dev.sh",
"dev:preset": "./tools/scripts/dev.preset.sh",
"dev:preset:sync": "tsc -p ./tools/preset-sync/tsconfig.json && node ./tools/preset-sync/index.js",
"build": "next build --webpack",
"build:next": "./deploy/scripts/download_assets.sh ./public/assets/configs && pnpm svg:build-sprite && ./deploy/scripts/make_envs_script.sh && next build --webpack",
"build": "node scripts/generate-well-known.js && next build --webpack",
"build:next": "./deploy/scripts/download_assets.sh ./public/assets/configs && pnpm svg:build-sprite && ./deploy/scripts/make_envs_script.sh && node scripts/generate-well-known.js && next build --webpack",
"wellknown:generate": "node scripts/generate-well-known.js",
"wellknown:check": "node scripts/generate-well-known.js --check",
"build:docker": "./tools/scripts/build.docker.sh",
"start": "next start",
"start:docker:local": "docker run -p 3000:3000 --env-file .env.local lux-explore:local",
+46
View File
@@ -0,0 +1,46 @@
{
"brandId": "lux",
"appId": "explore",
"title": "Lux Explorer",
"domain": "explore.lux.network",
"url": "https://explore.lux.network",
"github": "https://github.com/luxfi/explore",
"chains": [
{
"id": 96369,
"name": "Lux Mainnet"
},
{
"id": 96368,
"name": "Lux Testnet"
},
{
"id": 96370,
"name": "Lux Devnet"
},
{
"id": 200200,
"name": "Zoo Subnet"
},
{
"id": 36963,
"name": "Hanzo Subnet"
},
{
"id": 36911,
"name": "SPC Subnet"
},
{
"id": 494949,
"name": "Pars Subnet"
}
],
"peers": [],
"capabilities": [
"blocks",
"transactions",
"accounts",
"tokens"
],
"apiVersion": "1"
}
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env node
/**
* Generate public/.well-known/explore.json from configs/app/chainRegistry.ts.
*
* Run at build time so the federation manifest is always in sync with the
* chains the explorer actually knows about. Output is byte-identical when
* the registry hasn't changed (stable key order, trailing newline).
*
* Usage:
* node scripts/generate-well-known.js # write public/.well-known/explore.json
* node scripts/generate-well-known.js --check # exit 1 if file out of sync
*/
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const REPO_ROOT = path.resolve(__dirname, '..');
const REGISTRY_PATH = path.join(REPO_ROOT, 'configs/app/chainRegistry.ts');
const OUT_PATH = path.join(REPO_ROOT, 'public/.well-known/explore.json');
/** Parse a minimal subset of chainRegistry.ts to extract { chainId, label, network } per CHAINS[] entry.
* Avoids running TypeScript at build time — the registry is hand-edited and stable enough for regex.
*/
function parseChainEntries(src) {
const start = src.indexOf('export const CHAINS');
if (start < 0) throw new Error('CHAINS export not found in chainRegistry.ts');
const end = src.indexOf('];', start);
if (end < 0) throw new Error('CHAINS export not terminated');
const block = src.slice(start, end);
// Match every chain object literal: { name: ..., label: ..., network: ..., chainId: <N>, ... }
const entryRe = /name:\s*'([^']+)',[\s\S]*?label:\s*'([^']+)',[\s\S]*?network:\s*'([^']+)',[\s\S]*?chainId:\s*(\d+)/g;
const entries = [];
let m;
while ((m = entryRe.exec(block)) !== null) {
entries.push({ name: m[1], label: m[2], network: m[3], chainId: Number(m[4]) });
}
return entries;
}
/** Subnet display name. C-Chain (Contract Chain) is handled separately by
* the caller; for other chains use the short `name` field plus " Subnet". */
function normalizeSubnetName(rawName) {
return `${ rawName.trim() } Subnet`;
}
/** Build the federation manifest. Mainnet chains only — testnet/devnet are operator-visible
* but not federation peers. Localnet is excluded entirely (chainId 1337 is reserved).
*/
function buildManifest(entries) {
const chains = entries
.filter((e) => e.network === 'mainnet')
.map((e) => ({
id: e.chainId,
name: e.label === 'Contract Chain' ? 'Lux Mainnet' : normalizeSubnetName(e.name),
}));
// Always include explicit Lux Testnet/Devnet so peers can discover them.
const testnet = entries.find((e) => e.network === 'testnet' && e.label === 'Contract Chain');
const devnet = entries.find((e) => e.network === 'devnet' && e.label === 'Contract Chain');
if (testnet) chains.splice(1, 0, { id: testnet.chainId, name: 'Lux Testnet' });
if (devnet) chains.splice(2, 0, { id: devnet.chainId, name: 'Lux Devnet' });
return {
brandId: 'lux',
appId: 'explore',
title: 'Lux Explorer',
domain: 'explore.lux.network',
url: 'https://explore.lux.network',
github: 'https://github.com/luxfi/explore',
chains,
peers: [],
capabilities: [ 'blocks', 'transactions', 'accounts', 'tokens' ],
apiVersion: '1',
};
}
function main() {
const src = fs.readFileSync(REGISTRY_PATH, 'utf8');
const manifest = buildManifest(parseChainEntries(src));
const out = JSON.stringify(manifest, null, 2) + '\n';
const check = process.argv.includes('--check');
if (check) {
const current = fs.existsSync(OUT_PATH) ? fs.readFileSync(OUT_PATH, 'utf8') : '';
if (current !== out) {
process.stderr.write(`${ path.relative(REPO_ROOT, OUT_PATH) } is out of sync. Run: node scripts/generate-well-known.js\n`);
process.exit(1);
}
return;
}
fs.mkdirSync(path.dirname(OUT_PATH), { recursive: true });
fs.writeFileSync(OUT_PATH, out);
process.stdout.write(`Wrote ${ path.relative(REPO_ROOT, OUT_PATH) } (${ manifest.chains.length } chains)\n`);
}
main();