tenant-registry's API is INTERNAL_SERVICE_ONLY: once its AUTH_ENABLED
flips, every route but /healthz and /readyz requires a Keycloak token
whose audience contains `tenant-registry`. The portal calls it as a
service principal — machine-to-machine, distinct from the visitor's SSO
session, which authenticates a human against dev-portal.
New src/lib/service-token.ts fetches and caches a client_credentials
token for the realm's portal-svc client, refreshing 30s before the
5-minute expiry and de-duping concurrent fetches so parallel renders
share one request. A failed request throws rather than silently falling
back to an unauthenticated call, and does not poison the cache.
Wired into the single req() choke point in tenant-registry.ts, so every
call is covered. Inert until configured: with PORTAL_SVC_CLIENT_ID /
PORTAL_SVC_CLIENT_SECRET unset the helper returns null, no header is
sent, and behaviour is exactly as today — which is what keeps this
deployable before tenant-registry starts enforcing.
9 new tests; full suite 79 green; typecheck, lint and build clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>