mirror of
https://github.com/hanzoai/id.git
synced 2026-10-16 18:38:15 +00:00
c372f0e6a9b2b38b23b35ca9b67c952b878b650b
`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]>
@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
- Publish or workspace-link the new per-org brand pkg (must ship
brand.jsonat the package root and match theBrandContractshape inpkgs/shared/src/types.ts). - Add a
DEFAULT_TENANTSentry inpkgs/shared/src/tenant.tsOR put the override in the runtime catalog (IAM_TENANT_CONFIG_JSONenv) so no rebuild is needed. - Add the hostname to
apps/web/vite.config.ts::BRAND_PACKAGES(lets dev + build serve/brand/<pkg>/brand.json). - Add the hostname + TLS secret to
apps/web/k8s/ingress.yaml. - 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.
Languages
TypeScript
92.1%
CSS
4.5%
JavaScript
2.6%
Dockerfile
0.6%
HTML
0.2%