PR #1625 shipped the /api/v1/sme/billing/vouchers/* proxies but the
SME gateway (core/services/gateway/proxy.go) rejects RS256 outright
— it only accepts HS256 signed with sme-secrets/JWT_SECRET. Result
on every fresh Sovereign: operator clicks on /bss/vouchers returned
silent 401 with no upstream audit trail.
This commit ships the bridge:
- core/services/shared/auth/mint_sme.go (new)
- MintSMEAccessToken(secret, sub, email, role) → 5-min HS256 JWT
in the wire shape billing's requireVoucherIssuer expects.
- SMERoleFor(realmRoles, tier) → maps Keycloak roles + tier claim
onto SME vocab (superadmin | sovereign-admin | member).
- Pure, no IO, fully unit-tested (mint_sme_test.go).
- products/catalyst/bootstrap/api/internal/handler/sme_billing_vouchers.go
- proxySMEVoucher now mints a fresh HS256 token per upstream hop
from the operator's already-validated RS256 session claims and
forwards that as Bearer to the SME gateway. RS256 header is no
longer leaked upstream.
- Unwired bridge (CATALYST_SME_JWT_SECRET empty) surfaces 503
`sme-jwt-bridge-unwired` instead of the silent 401.
- products/catalyst/bootstrap/api/internal/handler/handler.go
- h.smeJWTSecret field + SetSMEJWTSecret(secret) setter.
- products/catalyst/bootstrap/api/cmd/api/main.go
- Reads CATALYST_SME_JWT_SECRET on startup and wires it.
- Log line includes byte count only (never the secret value, per
INVIOLABLE-PRINCIPLES.md #10).
- products/catalyst/chart/templates/api-deployment.yaml
- New env CATALYST_SME_JWT_SECRET sourced from sme-secrets/JWT_SECRET
in the same namespace (catalyst-system). optional: true so
Sovereigns without marketplace surface a 503 rather than
CreateContainerConfigError.
- products/catalyst/chart/templates/sme-services/sme-secrets.yaml
- emberstack/reflector annotation block mirroring sme-secrets
from `sme` ns into `catalyst-system` (Kubernetes secretKeyRef
is same-namespace-only). Same pattern as cnpg-cluster.yaml
and provisioning-github-token.yaml.
Operator-visible behaviour: the bridge is transparent on the happy
path (operator with sovereign-admin tier on a Sovereign with
marketplace enabled clicks /bss/vouchers → list returns). On the
unhappy paths the operator now sees a real status code:
- 503 sme-jwt-bridge-unwired (chart wire missing) — actionable
- 503 sme-gateway-unreachable (DNS NXDOMAIN) — pre-existing
- 403 from billing's requireVoucherIssuer (role insufficient)
— was silent 401 before, now propagates the real authz result.
Tests: core/services/shared/auth `go test ./...` PASS. catalyst-api
`go build ./...` PASS.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>