Commit Graph
111 Commits
Author SHA1 Message Date
zooqueen 4f420977c1 fix(auth): thread OIDC nonce through the password-login path
Confidential OIDC clients that validate strictly (LibreChat openid-client with
OPENID_REUSE_TOKENS) send a nonce on authorize and require the id_token to echo
it. The portal's password-login path read code_challenge from the authorize URL
but DROPPED nonce, so the IAM minted a code (and id_token) with no nonce ->
openid-client 'unexpected JWT claim value encountered' -> hanzo.chat callback
HTTP 500. Thread nonce: Login.tsx reads ?nonce, LoginForm forwards it, and
client.login() puts it on the /v1/iam/login query (next to code_challenge).
Additive + forward-only (never defaulted); social login already rode authorize()
which carried nonce. Verified: id SPA builds green.
2026-06-23 18:17:30 -07:00
zooqueen fbb8e4c13f ci(docker): compute version without node (ARC runner has no node)
The ARC runner image ships git+docker but not node, so the Compute-tags step's
`node -p "require('./package.json').version"` died with exit 127 (node: command
not found), skipping the build. Parse the version from package.json with
portable sed instead. Verified locally -> 0.1.23.
2026-06-23 15:42:29 -07:00
zooqueen 4457fd637e ci(docker): target ARC scale set by name + mode=min cache
runs-on was [self-hosted, linux, amd64], which GitHub matched to the OFFLINE
classic evo-* org runners instead of the live ephemeral ARC scale set, so every
Docker run since the self-contained-build switch queued forever (never built).
Use runs-on: hanzo-build-linux-amd64 (the exact scaleSetName), matching the
hanzoai/iam convention, so jobs route to scale set 31.

Also switch buildx gha cache to mode=min: the runner builds via DinD on a 32G
node and mode=max exported every layer, ballooning the DinD store past the
kubelet eviction threshold (NodeHasDiskPressure -> build killed 'no space').
2026-06-23 15:40:29 -07:00
zooqueen aba085af6b fix(auth): org-agnostic password login — resolve real owner-org, not pinned brand
LoginForm no longer pins organization=<brand> on POST /v1/iam/login. It passes
the new (normally-unset) TenantConfig.loginOrg, and client.login() OMITS the
organization field when empty so IAM runs cross-org resolution and the session
encodes the user's REAL owner-org (GetOrganizationByUser), never the hint.

Fixes: global admins (z@/[email protected], [email protected] — owner=admin) were
truncated to a 1-org 'hanzo' session via the UI because the pinned org made
GetUserByFields hit the colliding hanzo/<name> row first, so the cross-org
fallback to admin/<name> never ran. IAM IsGlobalAdmin()==(Owner=='admin') and
get-organizations returns all orgs only for a global admin. Verified live on
hanzo.id: [email protected] -> owner=admin, 45 orgs; brand-only user -> 1 org.

Boundaries preserved: signup still sends a concrete org; per-app SSO
(client_id+redirect_uri, type=code) still mints a code bound to the resolved
user; a brand can force single-org login via loginOrg in the catalog ConfigMap.
Contract locked in pkgs/auth/src/client.test.ts.

Bump 0.1.22 -> 0.1.23.
2026-06-23 15:24:32 -07:00
19f04a02a7 fix(onboarding): never list other tenants' orgs; create-or-skip only (#15)
The org step fetched service.listOrgs() and rendered a pick-list of every
existing organization to a brand-new user — leaking the tenant directory to
anyone who signs up, and not the intended UX. A new user should only create
their own organization or skip; joining an existing org is invitation-based
and handled outside onboarding.

Drop the listOrgs() fetch and the pick-list entirely; the org step now always
shows the create form with a 'Skip for now' action.

Co-authored-by: Darkhorse7stars <[email protected]>
2026-06-23 12:56:31 -07:00
zeekay 00683e2b8b Merge branch 'feat/sms-consent-disclosure' 2026-06-23 11:32:36 -07:00
zeekay 36d17a4e7a feat(auth): A2P SMS consent disclosure on the SMS verification surface
Twilio A2P 10DLC requires the SMS consent disclosure wherever a user enters
or uses a phone number to receive messages. Add one shared, verbatim consent
component (matches hanzo.ai/sms-opt-in SMS_CONSENT_TEXT and the IAM phone-login
UI) and render it on the portal's SMS surface, gated to the SMS channel only.

- New pkgs/auth/src/ui/SmsConsent.tsx: single source of SMS_CONSENT_TEXT plus
  <SmsConsentNotice/> (disclosure + Terms/Privacy links). Exported from the ui
  barrel for reuse by any future phone-collection surface.
- OTPForm: render SmsConsentNotice beneath the code input only when
  channel === 'sms' (not for totp/email).
- app.css: muted styling for .hanzo-id-sms-consent, matching footer-links.

Note: this portal does not render its own phone-number field — phone-number
COLLECTION happens in the IAM-hosted UI (covered separately), where signup uses
a required, submit-gating consent checkbox. The portal's only SMS-facing step
today is the verification-code entry, so the disclosure-only notice applies
here. Typecheck + tests green (pnpm -r tc, @hanzo/id-auth test).
2026-06-23 09:38:27 -07:00
zeekay e609857aec ci(docker): self-contained build on self-hosted runners
The shared reusable workflow (hanzoai/.github docker-build.yml@main) fails graph
validation for EVERY caller right now (org-wide startup_failure, 0 jobs created),
so id never built via CI (0.1.x images were pushed manually). Build the amd64
image directly on our self-hosted runners instead — no shared-infra dependency.
Tags: sha-<short> + package.json version.
2026-06-22 23:30:19 -07:00
zeekay 58b74ae832 fix(social): real Google endpoint + registered redirect_uri (iam.hanzo.ai/callback)
The provider hop sent redirect_uri=${origin}/callback (= hanzo.id/callback),
which the shared Google/GitHub OAuth client does NOT accept — verified live the
Google client accepts ONLY https://iam.hanzo.ai/callback (every other URI →
redirect_uri_mismatch). It also used the invalid authorize endpoint
accounts.google.com/signin/oauth.

- social.ts: Google endpoint → https://accounts.google.com/o/oauth2/v2/auth
  (canonical/stable); add callbackOrigin param to buildProviderAuthUrl/
  startProviderLogin (defaults to origin — local/single-host unchanged).
- TenantConfig.oauthCallbackOrigin (catalog-driven; defaults to publicOrigin);
  SocialButtons passes it so the hop returns to the provider's REGISTERED
  /callback. iam.hanzo.ai serves the same @hanzo/id SPA: the headless Callback
  completes the exchange and forwards to the originating app.
- tests: lock the registered-callback override + canonical Google endpoint.

bump id 0.1.1→0.1.22, id-auth/id-shared 0.1.0→0.1.1
2026-06-22 23:26:35 -07:00
zooqueen 698b4ff4e0 fix(portal): root IS the login form (logged-out) / apps launcher (logged-in)
- '/' rendered a static 'Welcome' marketing hero; for an identity portal the
  root should BE the login form when signed out, and the org's apps launcher
  when signed in. Portal now reads /v1/iam/get-account (same-origin, first-party
  cookie) and renders <Login> (anon) or the apps grid (authed).
- Onboarding completion redirected to '/' → the hero, which read as 'looped back
  to the beginning' after the last (wallet) step's Skip. It now lands on
  '/?signed_in=1' → the authed apps launcher. (The step machine was already
  correct: nextStep('wallet')==='done'.)
- Port marketing.ts (appsFor/billingFor) from the restore-design line.
2026-06-22 22:05:14 -07:00
zeekay 20bf75282e fix(auth): use feat's clean LoginForm/SignupForm (getAppLogin via SocialButtons; password+PKCE) — auto-merge had mixed main's appLogin/sendLoginCode API with feat's client 2026-06-21 19:43:15 -07:00
zeekay 7078723397 fix(docker): COPY pkgs/onboarding/package.json — merged workspace needs it for pnpm install 2026-06-21 19:37:00 -07:00
zeekay 6060450715 merge(id): integrate feat/full-auth-onboarding — PKCE password-login fix + composable auth UI + social hop into main
# Conflicts:
#	Dockerfile
#	apps/web/src/App.tsx
#	apps/web/src/app.css
#	pkgs/auth/src/client.ts
#	pkgs/auth/src/index.ts
#	pkgs/auth/src/ui/index.ts
#	pkgs/shared/src/brand.ts
#	pkgs/shared/src/tenant.ts
2026-06-21 19:35:05 -07:00
zeekay f586d844ac feat(social): wire the provider hop + return exchange (gated until creds seeded)
Prep social login end-to-end so only OAuth-app registration + KMS creds remain.

- The hop (social.ts/startProviderLogin): redirects straight to GitHub/Google with a base64 state that round-trips the original authorize request — the Casdoor getAuthUrl contract. Replaces the @hanzo/iam signinRedirect that looped back to the login page. Pure URL builder is unit-tested (social.test.ts, 5 cases).

- SocialButtons now keeps the full provider records (type/clientId/scopes surfaced from get-app-login) and routes OAuth providers through the hop, wallet through the SDK.

- Callback gained a GATED provider-return branch: a provider state (base64 with provider+application) is exchanged via client.providerLogin at the IAM backend, then follows the continue-URL back into the normal OIDC path. The existing OIDC/password path is the untouched else-branch — and the social branch is unreachable until a configured provider exists, so zero impact today.

- Cred-sync already exists (init_data.json ${IAM_GITHUB_CLIENT_ID} <- iam-kms-sync KMSSecret, project hanzo-iam/prod). Runbook in LLM.md. End-to-end needs live verification once real creds are seeded.
2026-06-20 10:46:26 -07:00
zeekay 0bb8da025e fix(login): deliver tenant catalog via /config.json; correct pars/osage/lux client wiring
Root cause osage.id still showed Hanzo: the SPA read the catalog from window.__ID_CATALOG__, which the runtime never injects — so every catalog-only host silently fell back to the bundled Hanzo default. Read the catalog from /config.json (what the static server actually templates from SPA_IAM_TENANT_CONFIG_JSON), with the global as a fallback.

Also corrected verified-broken client wiring: built-in pars-id does not exist (use pars-console, which carries the /callback redirect); added an osage built-in so a catalog-load failure can never leak the Hanzo brand. lux uses lux-id (lux-cloud lacks the portal redirect) — fixed in the ConfigMap. Tests: 7/7.
2026-06-20 02:51:08 -07:00
zeekay d5bad782bf fix(tenant): catalog-only hosts resolve their OWN brand; drop orphan divider
osage.id (and zoolabs.id) rendered as Hanzo — a white-label leak. Root cause: the runtime catalog entries carry brandUrl (+orgId/clientId), but a host with NO built-in DEFAULT_TENANTS entry used DEFAULT_TENANTS['hanzo.id'] as its merge base, so brandPackage/iamUrl silently inherited Hanzo. Now a catalog-only host derives iamUrl/iamIssuer/publicOrigin from the host itself and maps brandUrl -> brandPackage; it can never inherit another brand. Unknown brand asset -> neutral wordmark, not the wrong brand. Regression test added (pkgs/shared now has a test runner).

Drop the orphan 'or' divider: it lived between the social block and the password form, but with unconfigured social hidden it dangled above the form on every brand. Moved the divider INTO SocialButtons so it renders only alongside actual buttons; removed the standalone one from Login + Signup.
2026-06-20 02:38:31 -07:00
zeekay db4ad64e5f fix(login): only show configured social providers; never show a broken brand logo
Two things made login feel broken across every brand on the portal (hanzo.id/lux.id/pars.id/osage.id):

1) Social SSO dead-ended. The GitHub/Google/Apple/Web3 providers are seeded with PLACEHOLDER credentials (clientId=GITHUB_CLIENT_ID_PLACEHOLDER, etc.), so the OAuth redirect can never complete — clicking 'Continue with GitHub' just looped back to the login form. Render ONLY providers IAM actually holds a real clientId for (AppProvider.configured, computed from the nested provider record). With placeholders that hides all social buttons, leaving the working methods (email/password + email/SMS code) — and the buttons reappear automatically once real OAuth creds are seeded, no code change. When get-app-login is unreadable, render none rather than risk a dead-end.

2) Broken logo. brand.json points logo/favicon at jsdelivr @hanzo/brand assets, but the published brand packages ship no assets/ dir (404). BrandHeader now falls back to the brand NAME as a text wordmark on image error/absence, so a missing logo never renders a broken-image icon.

Enabling real social login later needs: (a) register OAuth apps (GitHub/Google/Apple) with callback https://<brand>/v1/iam/callback, (b) put their client_id/secret in KMS → IAM provider records, (c) the SPA provider-redirect hop. (a)+(b) are external provisioning; until then social is correctly hidden.
2026-06-20 02:21:35 -07:00
zeekay 945cbaa8cb fix(auth): forward PKCE code_challenge on password login
login() built /v1/iam/login with clientId/responseType/redirectUri/state
but dropped the PKCE code_challenge/code_challenge_method that authorize()
already forwards. A downstream public SPA client (e.g. hanzo-platform)
sending a user to password sign-in got an auth code minted with an empty
stored challenge; the downstream /auth/callback token exchange then failed
PKCE validation and fell back to a client_secret check the public client
can't satisfy (token.CodeChallenge: empty -> invalid_client).

IAM's Login handler (controllers/account.go) reads code_challenge from the
query first, body fallback, so forward it on the query mirroring authorize().
Plumb it from the OAuth params already on the /login URL through Login page
-> LoginForm -> client.login(). Social login was unaffected (rides authorize).
2026-06-20 01:36:59 -07:00
zeekay 6439d12e8f fix(docker): bake SPA-appropriate CSP into the image (HANZO_STATIC_CSP)
hanzoai/static defaults CSP to `default-src 'none'` which blocks the
SPA's own bundle (no script-src) -> blank page. Bake a widened-but-locked
CSP that permits: self scripts + CF beacon, same-origin + jsDelivr brand
fetches, https/data images (brand logos), inline styles (@hanzo/gui).
Baking it (vs a deploy-time env) makes the image render correctly
standalone and survives universe reconcile.
2026-06-20 00:17:30 -07:00
zeekay 549d3e7921 fix(brand): emit + fetch brand.json at flat encoding-safe /brand/<scope>.json
Two coupled bugs broke runtime brand loading on the hanzoai/static
(scratch) image:

1. vite.config.ts is ESM ("type":"module") so the brandJsonPlugin's
   require.resolve('@scope/brand/brand.json') threw ReferenceError
   (no global require), was swallowed by the silent catch, and NO
   brand.json was ever emitted into dist/. Fixed with
   createRequire(import.meta.url).

2. The brand fetch path /brand/@hanzo/brand/brand.json carries a literal
   @ and an encoded %2F that the production static server cannot map to
   the on-disk file, so it falls through to the SPA catch-all and returns
   index.html — the SPA then parses HTML as JSON. Emit and fetch at a
   FLAT slug path instead: @hanzo/brand -> /brand/hanzo.json. The plugin
   and loadBrand derive the slug identically (npm scope).

With defensive loadBrand (prev commit) a miss degrades to a neutral
brand; this restores real per-brand branding.
2026-06-20 00:07:06 -07:00
zeekay 38647430ad fix(brand): never blank the login form on a transient brand.json failure
The browser brand loader threw on any non-ok /brand/<pkg>/brand.json fetch, so a single transient 502 on the cosmetic brand asset (intermittent behind Cloudflare) left the IAM login SPA blank — no form, just 'brand.json fetch failed: 502'. Retry the flaky asset up to 3x with backoff, then fall back to a neutral per-tenant brand so the form always renders. brand.json stays the source of truth on the happy path.
2026-06-19 23:54:11 -07:00
zeekay 57f22a1502 fix(docker): serve SPA from /public with --spa for hanzoai/static contract
hanzoai/static:0.4.1 is FROM scratch + ENTRYPOINT ["/static"]; the
binary defaults to -root /public -port 3000 and writes
/public/config.json from SPA_* env at boot. The previous final stage
copied the bundle to /spa and set dead ENV ROOT/PORT (the binary reads
flags, not env), so /public never existed and the runtime-config write
crashed (open /public/config.json.tmp: no such file or directory).

Copy the dist to /public (the default root + config.json target) and
pass --spa so client-routed paths (/auth/*, /callback) fall back to
index.html. Port 3000 matches the id Service and probes.
2026-06-19 23:48:00 -07:00
zeekay c3c5b8763e fix(docker): COPY pkgs/onboarding/package.json into install stage
apps/web depends on @hanzo/id-onboarding (workspace:*) since the
onboarding flow landed, but the Dockerfile's per-package manifest
COPY list (the layer feeding the dependency-resolution
`pnpm install` before the full `COPY pkgs pkgs`) was never
updated. In-cluster builds therefore failed at install with
ERR_PNPM_WORKSPACE_PKG_NOT_FOUND for @hanzo/id-onboarding.

Local builds masked this because node_modules was already
populated from a prior full install.
2026-06-19 23:40:16 -07:00
zeekay 45eb4fab54 test(onboarding): unit tests for step machine + IAM wire contracts; docs
9 tests via the Node built-in runner (--experimental-strip-types, no
test-framework dependency): step machine (org→project→wallet→done),
listOrgs mapping + error-resilience, createOrg request shape + error
surfacing, linkWallet address validation + get-account→update-user
(web3onboard, column-scoped) + fail-closed. tc excludes *.test.ts so the
build gate stays type-only.

LLM.md: corrected issuer to per-brand *.id host (not iam.hanzo.ai),
documented the full method set + get-app-login source-of-truth, the
onboarding pkg + its IAM routes + admin-gating reality, and the /v1/iam
OIDC paths.
2026-06-19 15:34:37 -07:00
zeekay b6605576dd fix(auth): social/Web3 sign-in honors downstream redirect_uri
Social + Web3 always return to the portal's own /callback (the SDK's fixed
redirectUri), so a downstream app's redirect_uri would be lost. SocialButtons
now stashes it in post_login_redirect before signinRedirect; Callback reads it
back and forwards the tokens there, else lands on /onboarding. Login + Signup
pass redirect_uri through. Password path already forwards directly via the
auth-code response — both methods now reach the same downstream target.
2026-06-19 15:31:21 -07:00
zeekay 6d6f303667 fix(onboarding): correct IAM contracts — get-account+web3onboard wallet, authz-aware org/project
linkWallet now resolves the signed-in user via /v1/iam/get-account (owner/name)
and writes the lowercase `web3onboard` column scoped via ?columns= so the rest
of the user row is untouched — IAM's update-user is keyed by owner/name, not a
self alias.

Org/project creation is admin-gated in IAM authz (add-organization needs the
admin role; add-project default-denies non-admins). The common path — pick the
org you signed up into, listed via the *-allowed get-organizations — works for
everyone; create-new surfaces a plain permission message and the step stays
skippable so onboarding never hard-blocks. ProjectStep tolerates an org-less
flow (continue-only).
2026-06-19 15:29:42 -07:00
zeekay ec510f3c29 feat(onboarding): post-login org → project → wallet flow + serverUrl=hanzo.id
@hanzo/id-onboarding: domain (serializable step machine + types) / service
(IAM-backed writes via /v1/iam/{get-organizations,add-organization,add-project,
update-user}, cookie+bearer) / UI (self-contained 3-step OnboardingFlow, no
router lib). Web app mounts it at /onboarding; both password (cookie) and
social/Web3 (SDK token) sign-in land there for a bare portal login.

Callback now completes via the @hanzo/iam SDK handleCallback (matches
SocialButtons' signinRedirect) and forwards tokens to a downstream app only
when one initiated the flow.

tenant.ts: iamUrl is the per-brand OIDC issuer host (hanzo.id / lux.id /
zoo.id / pars.id), never iam.hanzo.ai — HIP-0111 host-relative discovery.
clientId is the brand -id app (hanzo-id), matching init_data.json.
2026-06-19 15:25:04 -07:00
zeekay 91f0dc215c feat(auth): full method set — email/password + GitHub + Google + Web3
SocialButtons reads live enabled providers from /v1/iam/get-app-login and
drives the @hanzo/iam PKCE redirect (provider param) per method. Login/Signup
render the social row + divider above the email form. AppLogin/AppProvider
types model the get-app-login view; createIam() centralizes the one PKCE
client both SocialButtons and Callback share.
2026-06-19 15:19:50 -07:00
275fac6efe fix(brand): resilient brand loader — never blank the login page (#14)
loadBrand did res.json() on the SPA-fallback HTML (the spa server answers
unknown paths with index.html + HTTP 200 for client routing), so a missing
brand.json threw 'Unexpected token <' and crashed App boot -> blank login
page. brand.json is not bundled and the path also wrongly encoded the '/'
in '@hanzo/brand' (%2F), so it never resolved.

Fix:
- Prefer the tenant's brandUrl (the working jsDelivr brand.json from
  config.json), then the app-local path; reject non-JSON (SPA-fallback HTML)
  responses by content-type; on total failure return a minimal fallback brand
  derived from the package scope. Branding is cosmetic and must never block login.
- Add TenantConfig.brandUrl (already supplied by config.json) and pass it from App.
- Drop encodeURIComponent on the whole package (it mangled the scope slash).

Co-authored-by: Antje Worring <[email protected]>
2026-06-18 17:56:31 -07:00
c75b797a8c fix(docker): pin hanzoai/spa:1.2.0 (1.3.0 image not published) (#13)
The v1.3.0 git tag exists but no 1.3.0 image was published to ghcr
(build pipeline blocked). 1.2.0 is the latest published spa image and the
version the canonical RECIPE.md uses.

Co-authored-by: Antje Worring <[email protected]>
2026-06-18 16:18:13 -07:00
e8a6e09c70 fix(docker): serve SPA via hanzoai/spa, not hanzoai/static (#12)
hanzoai/static defaults to Content-Security-Policy: default-src 'none'
(no script-src) — built for static assets, not a SPA that loads its own
JS bundle. That CSP blocks index-*.js, so React never mounts and the
login page renders blank. hanzoai/spa is the purpose-built base: history-
API fallthrough for client-side routes + a SPA-safe CSP. Defaults
PORT=3000 / ROOT=/public, matching the id deploy probe.

Immediate prod was unblocked by setting HANZO_STATIC_CSP on the live
deployment; this is the durable, one-way fix so the override hack isn't
needed and every rebuild serves correctly.

Co-authored-by: Antje Worring <[email protected]>
2026-06-18 16:14:27 -07:00
0fc4bd57b3 fix(docker): static needs -root/-spa flags, not ROOT/PORT env (#11)
hanzoai/static:0.4.1 reads the serve root from the -root flag (default
/public) and port from -port (default 3000) — the ENV ROOT=/spa PORT=8080
were no-ops, so the built image served /public (crash) on :8080 while the
deploy probes :3000. Set ENTRYPOINT [/static -root /spa -spa] (default
port 3000). Matches the hand-built id:0.1.5 now live.

Co-authored-by: Antje Worring <[email protected]>
2026-06-18 15:33:36 -07:00
d5ff2294fb fix(id): appLogin uses /v1/iam/get-app-login (main dropped /api/*) (#10)
Merged IAM main serves the canonical /v1/iam/* only; /api/get-app-login
now returns the SPA shell. Point the providers/methods fetch at the
canonical path so social buttons + sign-in methods load.

Co-authored-by: Antje Worring <[email protected]>
2026-06-18 14:08:32 -07:00
c16735b033 feat(id): render social logins + email + SMS code on login/signup (#9)
The Vite portal rendered only email/password. Add — driven by the live
IAM app config (new AuthClient.appLogin -> get-app-login), no hardcoded
lists:
- social provider buttons (GitHub/Google/Apple/Web3) via authorize?provider=
- passwordless email / SMS code sign-in (send-verification-code + OTPForm)
- new ProviderButtons component; LoginForm + SignupForm render providers
Email/password sign-in unchanged. tsc --noEmit clean; vite build clean.

Functional prerequisites (display works now; these make it WORK):
- real OAuth client IDs for github/google/apple (IAM currently has
  GITHUB_CLIENT_ID_PLACEHOLDER etc.)
- an SMS gateway provider (Twilio/etc.) in IAM for SMS codes
- redirectUris backfill so the /callback OAuth return is allowed (iam#51)

Co-authored-by: Antje Worring <[email protected]>
2026-06-18 13:42:41 -07:00
82da4b868d chore(deps): clear Dependabot alerts — drop legacy-nextjs, patch esbuild/uuid (#8)
Resolves all 35 open Dependabot alerts on hanzoai/id:

- Delete legacy-nextjs/ (frozen Next.js predecessor, not wired into any
  build/Dockerfile/workspace glob; docs slated it for deletion after
  v0.1.0 — v0.1.1 has shipped). Removes 31 alerts (next, undici, ws,
  picomatch, postcss, js-yaml, cookie, defu, ...).

- pnpm overrides for the 3 active-workspace transitives:
    esbuild ^0.28.1  (was 0.27.7) — GHSA-gv7w-rqvm-qjhr (high), GHSA-g7r4-m6w7-qqqr (low)
    uuid    ^11.1.1  (was 7.0.3/10/x via [email protected]) — GHSA-w5hq-g745-h8pq (medium)

Verified: pnpm install + typecheck (4/4) + build green (vite 7.3.5,
esbuild 0.28.1, 52 modules); 'pnpm audit' → no known vulnerabilities.

Co-authored-by: Antje Worring <[email protected]>
2026-06-18 12:23:57 -07:00
8706be065b fix(tenant): use real IAM clientId (<org>-id, not <org>-id-portal) (#7)
The IAM apps are registered with clientId '<org>-id' (hanzo-id, lux-id,
zoo-id, pars-id) — there is no '-portal' variant. clientId 'hanzo-id-portal'
returns 'Invalid client_id' from IAM, breaking the login flow. Align all
four tenant clientIds with their appName and the IAM seed convention.

Co-authored-by: Antje Worring <[email protected]>
2026-06-18 11:34:50 -07:00
fee75936a9 fix(id): pin @hanzo/iam to published ^0.9.4 (0.10.0 unpublished, breaks pnpm install) (#6)
Co-authored-by: hanzo-dev <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-17 04:03:28 -07:00
788d6fb278 chore(decomplect): deploy + per-host catalog live in universe, not the app repo (#5)
The id repo is brand-neutral (zero brand data bundled), but apps/web/k8s/
carried the per-host brand catalog + Deployment/Ingress — duplicated in
hanzoai/universe infra/k8s/id/ (the single deploy source-of-truth). Removed
the overlay so app (brand-neutral image) and deploy (per-host catalog +
manifests, in universe) are separated. One way, one place.

Co-authored-by: hanzo-dev <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-16 18:05:31 -07:00
2bf12ff96e fix(auth): IAM wire contract (body params) + brand-app catalog (v0.1.1) (#4)
* fix(auth): IAM wire contract + brand-app catalog

- client.ts: send type/application/organization in the request BODY (IAM reads
  auth fields from body, OAuth params from query) so the branded login form
  authenticates; build the code-redirect from the response.
- canonical /v1/iam/oauth/{authorize,token,logout} paths.
- tenant-catalog: point each host at its brand's real IAM app
  (hanzo-console/lux-cloud/zoo-console/pars-console).

* chore(release): 0.1.1

---------

Co-authored-by: hanzo-dev <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-16 18:00:41 -07:00
zeekay c0810cfaa8 chore: update 2026-06-10 14:12:32 -07:00
zeekay 44223ec2c5 merge: id-vite-monorepo (-X theirs) 2026-06-01 18:09:45 -07:00
zeekay f7b92cfd6f merge: ci/canonical-docker-build-1776995843 (-X theirs) 2026-06-01 18:09:44 -07:00
zeekay 940c9f7e73 refactor: brand-neutral identity portal (zero brand-specific code)
Image carries no brand identity. Every per-tenant value comes from the
runtime catalog (K8s ConfigMap → /config.json → window.__ID_CATALOG__)
or is derived from the hostname.

Source changes:
- TenantConfig.brandPackage → brandUrl (absolute URL to brand.json).
  Brands self-host (npm + jsDelivr is convention; any URL works).
- DEFAULT_TENANTS = {} (deleted). Hostname-derivation is the new fallback:
    foo.id / www.foo.id / id.foo.net / iam.foo.net → orgId=foo,
    clientId=foo-id-portal, appName=foo-id,
    brandUrl=https://cdn.jsdelivr.net/npm/@foo/brand@latest/brand.json
  Works out-of-the-box when the npm scope matches the org. The catalog
  handles mismatches.
- brand.ts loadBrand(brandUrl) fetches the URL directly. localizeAssets()
  removed — brand.json's URLs are absolute, used as-is.
- vite.config.ts: removed brandJsonPlugin + BRAND_PACKAGES list. Vite
  bundles no brand assets. dist drops from ~250kB to 205kB (64kB gzip).
- apps/web/package.json: dropped @hanzo/brand, @luxfi/brand, @zooai/brand,
  @parsdao/brand deps. Zero brand packages in the image.
- main.tsx: fetches /config.json before mount; sets
  window.__ID_CATALOG__ from cfg.iamTenantConfigJson (templated by
  hanzoai/spa runtime from SPA_IAM_TENANT_CONFIG_JSON env var).
- App.tsx: loadBrand(t.brandUrl).
- LLM.md: documents the brand-neutral architecture and how to add a
  brand (publish brand.json + add to deploy's catalog ConfigMap; image
  never changes).

Deploy changes:
- apps/web/k8s/tenant-catalog.yaml: NEW ConfigMap carrying the Hanzo
  deployment's full host→tenant map for 11 hosts (hanzo / lux / zoo /
  pars / osage and their id.*.network / iam.*.network / www.* variants).
  Brand-specific knowledge lives ONLY here.
- apps/web/k8s/deployment.yaml: envFrom: configMapRef: id-tenant-catalog.
- apps/web/k8s/kustomization.yaml: includes tenant-catalog.yaml first.

Adding a brand from here on is:
  1. publish @<scope>/brand to npm with brand.json
  2. add the host(s) to the deployment's ConfigMap
  3. add the host to ingress.yaml (cert-manager auto-provisions TLS)
  4. DNS → cluster ingress IP

No source change. No image rebuild.
2026-05-29 11:37:05 -07:00
zeekay 6e6ab29f65 feat(k8s): add www.zoolabs.id + osage.id + www.osage.id to id ingress
cert-manager.io/cluster-issuer=letsencrypt-prod-cf provisions per-host
TLS via DNS-01 (the Traefik file-route certResolver path is non-functional
in this cluster — CF env vars on the ingress deployment are empty, ACME
in /data/acme.json is empty). cert-manager owns TLS for identity hosts.

After apply + cert issuance (~90s): all 7 identity hosts return their
own OIDC issuer with valid LE certs:
  lux.id, hanzo.id, pars.id, zoolabs.id, www.zoolabs.id, osage.id, www.osage.id
2026-05-29 11:07:17 -07:00
zeekay 29687dc896 feat(tenant): add osage.id + www.osage.id + www.zoolabs.id tenants
DEFAULT_TENANTS now resolves all 4 active identity hosts that didn't
have built-in entries:
- www.zoolabs.id → orgId=zoo, brand=@zooai/brand
- osage.id      → orgId=osage, brand=@osage/brand
- www.osage.id  → orgId=osage, brand=@osage/brand

zoolabs.id was already present. orgId, iamIssuer, clientId, appName,
publicOrigin, brandPackage all follow the existing per-host shape.

@osage/brand is unpublished (~/work/osage/brand v0.1.0 needs build +
publish). Until then, /brand/@osage/brand/brand.json 404s and the
SPA's loadBrand falls back to the @hanzo/brand default — adequate
until osage.id DNS flips off Cloudflare Pages.
2026-05-28 18:27:27 -07:00
zeekay 50966f6414 chore: update 2026-05-25 15:14:35 -07:00
zeekay 7710d427de feat: add zoolabs.id (canonical Zoo identity host; was zoo.id)
Per user clarification, the Zoo identity domain is zoolabs.id (zoo.id is
not owned). Add tenant entry + Ingress host + TLS secret. DNS A record
already repointed in CF to 129.212.164.5.
2026-05-24 14:10:33 -07:00
zeekay 48a4efde05 fix(tenant): hanzo.id clientId was mangled by previous sed (id-portal-portal → hanzo-id-portal) 2026-05-24 10:32:18 -07:00
zeekay 1b5aa6c3bc style: rename CSS classes hanzo-id-* → id-portal-*
Brand-neutral CSS class names match the public-facing identity of the
portal (id-portal). Cosmetic only; no rendered text changes. Per
playwright agent's source-view note.
2026-05-24 10:31:03 -07:00
zeekay 957ead2a7e feat: extend id portal to .network identity hosts
Add iam.lux.network + id.lux.network (Lux brand) + id.zoo.network
(Zoo brand) to the id Ingress + tenant catalog. Same image, same code
path — only DEFAULT_TENANTS + Ingress hosts/tls grow.

Cutover plan (post-merge):
  1. kubectl apply -k infra/k8s/id  (cert-manager issues 3 new TLS certs via DNS-01)
  2. Repoint CF DNS A records → 129.212.164.5
  3. Delete the 3 hanzo-id Worker routes
  4. Archive hanzoai/hanzo.id-worker repo (no routes remaining)
2026-05-24 10:12:23 -07:00