zooqueenandHanzo Dev 4ca3ff494e 0.2.15 — take the whole token layer, mount the ONE account control, gate on resolution
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]>
2026-07-27 17:49:37 -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%