Files
id/legacy-nextjs/app/signup/page.tsx
zeekayandGitHub a24ed753a4 rewrite: Vite + @hanzo/gui monorepo (drops CF Worker + Next.js) (#2)
Replaces the Cloudflare Worker (hanzo.id-worker) and the Next.js portal
with a pnpm monorepo following the a-monorepo/id pattern.

Layout:
  apps/web/        Vite + React 19 SPA, embeds @hanzo/gui shell
    k8s/           Deployment(2) + Service + Ingress (4 hosts, 4 TLS)
  pkgs/shared/     @hanzo/id-shared — TenantConfig, resolveTenant,
                                       loadBrand (browser + node)
  pkgs/auth/       @hanzo/id-auth   — AuthClient (wraps @hanzo/iam REST)
                                       + LoginForm/SignupForm/ForgotForm/OTPForm
  pkgs/idv/        @hanzo/id-idv    — pluggable IDV (stub, persona,
                                       onfido, veriff) behind one
                                       IDVProvider interface
  legacy-nextjs/   Frozen — Next.js predecessor. Delete after v0.1.0 ships.
  Dockerfile       Two-stage: pnpm build → hanzoai/static:0.4.1 serves /spa
  README.md
  LLM.md           Architecture, dev, deploy, cutover plan

Tenant resolution: hostname → TenantConfig (orgId, iamUrl, clientId,
appName, publicOrigin, brandPackage). Built-in defaults for
hanzo.id/lux.id/zoo.id/pars.id; runtime override via
IAM_TENANT_CONFIG_JSON env (served as /config.json at pod startup).

Brand resolution: each per-org brand pkg (@hanzo/brand, @luxfi/brand,
@zooai/brand, @parsdao/brand) ships brand.json. The Vite plugin
brandJsonPlugin emits /brand/<pkg>/brand.json verbatim; the browser
fetches the right one based on the resolved tenant. No bundle bloat.

IDV: stub (default for dev), persona, onfido, veriff. Each adapter
implements `IDVProvider` from pkgs/idv/src/provider.ts. Swap providers
with one registration call at boot — portal code unchanged.

Cutover (separate ops PR — not in this commit):
  1. Tag + push image ghcr.io/hanzoai/id:0.1.0
  2. Apply k8s manifests, cert-manager issues TLS
  3. Remove CF Worker routes for hanzo.id/lux.id/zoo.id/pars.id
  4. CF A records → 129.212.164.5 (hanzo ingress LB)
  5. Archive hanzo.id-worker repo
2026-05-24 06:00:20 -07:00

57 lines
1.9 KiB
TypeScript

import { Suspense } from 'react'
import { headers } from 'next/headers'
import { getBranding, staticBranding, defaultBranding, resolveBrandingDomain, BrandingConfig } from '@/lib/branding'
import SignUpForm from '@/components/SignUpForm'
import MarketingPanel from '@/components/MarketingPanel'
export const runtime = 'edge'
async function getBrandingForDomain(): Promise<BrandingConfig> {
const headersList = await headers()
const host = headersList.get('host') || 'hanzo.id'
const domain = resolveBrandingDomain(host)
const staticConfig = staticBranding[domain]
if (staticConfig) {
return { ...defaultBranding, ...staticConfig, domain }
}
return getBranding(domain)
}
export default async function SignUpPage() {
const branding = await getBrandingForDomain()
const cssVars = {
'--color-primary': branding.colors.primary,
'--color-primary-text': branding.colors.primaryText,
'--color-background': branding.colors.background,
'--color-surface': branding.colors.surface,
'--color-text': branding.colors.text,
'--color-text-muted': branding.colors.textMuted,
'--color-border': branding.colors.border,
'--color-error': branding.colors.error,
} as React.CSSProperties
return (
<div className="min-h-screen flex" style={cssVars}>
<div className="w-full lg:w-1/2 flex items-center justify-center p-8">
<div className="login-card w-full max-w-md p-8">
<div className="flex items-center justify-between mb-8">
<img
src={branding.logo}
alt={branding.logoAlt || branding.orgName}
className="h-10"
/>
</div>
<Suspense><SignUpForm branding={branding} /></Suspense>
</div>
</div>
<div className="hidden lg:flex w-1/2 items-center justify-center p-12 bg-gradient-to-br from-black via-zinc-900 to-black">
<MarketingPanel branding={branding} />
</div>
</div>
)
}