zeekayandHanzo Dev c372f0e6a9 one front door per brand, and no Hanzo hostname on anyone else's
`oauthCallbackOrigin` was dead — computed, typed, asserted by tests, read by no
production code. It named the host whose /callback the social hop should target,
and the hop stopped choosing one when federation moved to IAM's own fixed
callback. Measured against production before deleting: the authorize hop already
sends https://lux.id/v1/iam/oauth/callback on lux.id and https://zoolabs.id/...
on zoolabs.id — correct, per-brand, and nothing to do with this field.

It survived on all thirteen catalog rows reading `https://iam.hanzo.ai`: one
brand's hostname pinned onto every white-label host, doing nothing. Dead config
is not inert to a reader — its doc comment still stated the deleted rule, which
is the sentence that would talk the next person into rebuilding the hop.

An org also answered on several hostnames and every one of them rendered a full
login page. lux.id, id.lux.network and iam.lux.network were three front doors
for one brand, so iam.lux.network showed its own Lux page instead of sending
people to lux.id. One entry per org now says `canonical: true` and aliasRedirect
sends the rest there before render, path and query intact so a sign-in keeps the
OAuth request that sent it. /callback never moves — a provider returns to the
exact URI it was given, and redirecting it discards the code mid-exchange — and
the front door does not redirect to itself, which is what a loop is made of.

That replaced a guess: a scan of DEFAULT_TENANTS for a host ending in `.id`,
which answered for hanzo, lux and pars and returned NOTHING for zoo, whose front
door is zoolabs.id and which has no DEFAULT_TENANTS row. Which host is the front
door is a fact about the ORG and the catalog is keyed by HOST, so the catalog
states it the one way a host-keyed table can.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 16:43:21 -07:00
2026-02-14 05:26:48 -08:00
2026-06-28 20:06:13 -07:00

id

@hanzo/id

White-label login + identity verification portal. One Vite SPA, four hosts (hanzo.id, lux.id, zoo.id, pars.id), per-tenant brand resolved from the request hostname at runtime.

Layout

apps/
  web/                Vite + React 19 + @hanzo/gui — the actual SPA
    k8s/              Deployment + Service + Ingress (4 hosts, 4 TLS secrets)
pkgs/
  shared/             @hanzo/id-shared  — TenantConfig + brand resolver
  auth/               @hanzo/id-auth    — composable login/signup/OTP flows
                                          on top of @hanzo/iam SDK
  idv/                @hanzo/id-idv     — pluggable identity verification
                                          (Persona, Onfido, Veriff, stub)
legacy-nextjs/        Frozen — predecessor Next.js implementation. Kept
                     for reference until v0.1.0 ships to production.

Local dev

pnpm install
pnpm dev                 # http://localhost:5173 (defaults to hanzo brand)

To preview a different brand locally, edit /etc/hosts:

127.0.0.1  lux.id zoo.id pars.id

then visit http://lux.id:5173.

Build

pnpm build               # builds apps/web -> dist/
docker build -t ghcr.io/hanzoai/id:0.1.0 .

Adding a brand

  1. Publish or workspace-link the new per-org brand pkg (must ship brand.json at the package root and match the BrandContract shape in pkgs/shared/src/types.ts).
  2. Add a DEFAULT_TENANTS entry in pkgs/shared/src/tenant.ts OR put the override in the runtime catalog (IAM_TENANT_CONFIG_JSON env) so no rebuild is needed.
  3. Add the hostname to apps/web/vite.config.ts::BRAND_PACKAGES (lets dev + build serve /brand/<pkg>/brand.json).
  4. Add the hostname + TLS secret to apps/web/k8s/ingress.yaml.
  5. DNS: CNAME or A record → cluster ingress IP.

That's it — no per-brand Worker, no per-brand image, no per-brand deployment. One binary, four brands.

Plugging an IDV provider

import { registerProvider, createPersonaProvider } from '@hanzo/id-idv'
registerProvider(createPersonaProvider({
  templateId: import.meta.env.VITE_PERSONA_TEMPLATE_ID,
  apiKey: import.meta.env.VITE_PERSONA_API_KEY,
  environment: 'production',
}))

The portal stays unchanged — switching providers is a single registration call at boot.

S
Description
No description provided
Readme MIT
3.7 MiB
Languages
TypeScript 92.1%
CSS 4.5%
JavaScript 2.6%
Dockerfile 0.6%
HTML 0.2%