mirror of
https://github.com/hanzoai/id.git
synced 2026-10-16 18:38:15 +00:00
4ca3ff494e1ffc1f9fee393ea86a2a4c4b6aa1e0
0.2.14 adopted @hanzo/design and worked around it in three places. Those findings are fixed IN @hanzo/design 0.3.0, so the workarounds go and this surface takes the system's answer. - ONE import: `@hanzo/design/styles.css`. app.css cherry-picked four of nine token groups, so --z-*, --shadow-*, --space-*, --font-* and the element defaults did not exist here. Nothing broke visibly, because an unresolved var() paints nothing and reports no error. @hanzo/iam's account menu alone reaches for --z-popover, --shadow-floating and --space-1..3. - Geist is self-hosted in 0.3.0, so the reason for cherry-picking is gone: fonts.css no longer requests fonts.googleapis.com. Measured on the built bundle: rendered face is Geist, zero third-party font requests. - The :focus-visible rule is deleted from this file. tokens/base.css ships it to every consumer and --ring is now var(--neutral-500): measured 2px solid rgb(115,115,115), 4.43:1 on --background (WCAG 2.4.11 wants 3:1). The local --primary override existed only because --ring was 1.66:1. - Control boundaries are --border-strong, not --white-40. 0.3.0 moved it to var(--neutral-500) — 4.43:1 in BOTH themes. --white-40 cleared 3:1 on black and measures 1.00:1 on white. - @hanzo/iam 0.13.1 -> 0.21.1 in apps/web, pkgs/auth, pkgs/onboarding, and the signed-in portal mounts <UserMenu> in place of a hand-rolled Billing/Sign out link row. Identity comes from resolveIdentity, so the portal cannot disagree with the console about who you are. No `brand` prop: omitting markSvg would put the Hanzo mark on lux.id and zoo.id. - Portal.tsx's inline style object is gone — rgba(255,255,255,0.14), borderRadius 12, fontSize 13, fontWeight 600 and two bare opacities, six invented values for facts the token layer already states. - apps/web/src/tokens.test.ts gates RESOLUTION, not declaration. The reference is built at runtime from a string, so it is invisible to the compiler and to grep. Verified by reintroducing both original defects: it names the 5 dropped groups and the 8 unresolved iam tokens. Measured in Chromium, fresh context, 1440 and 390, on the built bundle served as hanzo.id / lux.id / zoolabs.id: 0 unresolved of 76 tokens referenced on every host; input edge and focus ring both 4.43:1; the account menu paints #0a0a0a / 12px / --shadow-floating / z-index 700. Brand switches with zero per-brand code and no Hanzo mark reaches the Lux menu. Open, against layers below this surface: the menu's own panel edge is --border at 1.27:1 on --popover (0.3.0 raised --ring and --border-strong, not --border), and BrandHeader loads the mark from cdn.jsdelivr.net/@latest — a third-party request on the sign-in path, which is what this file refuses for fonts. 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%