// Canonical IAM auth (HIP-0111) via @hanzo/iam SPA PKCE. // // One source of truth for the browser IAM client, white-labelled per brand: // serverUrl + clientId come from the active brand (lib/brand.ts), resolved // from window.location.hostname. There are no env overrides — see below. // // HIP-0111: authenticate ONLY through @hanzo/iam against the canonical OIDC // endpoints. The SDK resolves /v1/iam/oauth/* via live discovery (with a // canonical-path fallback), drives PKCE S256, and owns `state` for CSRF. No // hand-rolled authorize URLs, no genericOAuth({discoveryUrl}), no /api/auth. "use client" import { IAM } from "@hanzo/iam/browser" import { getBrand } from "@/lib/brand" // Stash the post-login destination across the IAM round-trip. The SDK owns // `state` for CSRF; the app owns where to land after sign-in. const RETURN_URL_KEY = "bootnode_return_url" export function getIam(): IAM { // The brand is the ONE source for issuer + client, and it is resolved from // the hostname of the page the user is standing on. There is no env // override, by design: this deployment serves MANY white-label hosts, so a // build-time NEXT_PUBLIC_IAM_URL is a single global value that every brand // would share — it cannot express per-host white-labeling, and it silently // outranked the correct per-brand default. That is exactly what shipped: // bootno.de/dashboard bounced to hanzo.id and asked a Bootnode customer to // "Sign in to Hanzo ID" under the Hanzo mark. // // `getBrand()` and not `useBrand()`: this runs imperatively at click time, // when `window` exists, so it reads the true host rather than the prerender // default — and it must stay callable outside React. const { url: serverUrl, clientId } = getBrand().iam return new IAM({ serverUrl, clientId, redirectUri: `${window.location.origin}/auth/callback`, scope: "openid profile email", }) } export function setReturnUrl(url: string): void { try { sessionStorage.setItem(RETURN_URL_KEY, url) } catch { /* sessionStorage unavailable — fall back to default destination */ } } export function takeReturnUrl(): string { try { const url = sessionStorage.getItem(RETURN_URL_KEY) sessionStorage.removeItem(RETURN_URL_KEY) return url || "/dashboard" } catch { return "/dashboard" } }