import { Inject, Injectable } from '@nestjs/common'; import type { JWK } from 'jose'; import { BFF_SIGNING_KEY, type BffSigningKey } from './bff-signing-key'; /** * Builds the JWKS payload served at `/.well-known/jwks.json` per * [ADR-0014](../../../../docs/decisions/0014-downstream-api-access-obo-pattern.md) * §"Service strategy". v1 returns a single public key; the rotation * chantier extends `keys` to a window of currently-valid material * so a downstream that cached a previous JWK keeps verifying during * cut-over. * * **Why a service, not a `@Controller`.** Path-to-regexp v8 (used * by Nest 11) parses controller paths through a grammar that does * not cleanly route a leading-dot segment like `.well-known/...` * to a bare-root URL — the original `@Controller('.well-known/jwks.json')` * + `setGlobalPrefix('api', { exclude: ... })` combination landed * the route at neither `/api/.well-known/jwks.json` nor * `/.well-known/jwks.json`. We sidestep the whole class of bug by * registering the JWKS handler at the Express layer in `main.ts` * (before Nest's router) and letting this service produce the * payload. The Nest DI graph still owns the BFF signing key — only * the route wiring lives outside. * * **No auth / no CSRF.** Public by design — the JWKS is the * downstream's verification anchor; gating it would defeat the * purpose. The CSRF middleware exempts GET, and the route is * mounted ahead of the rate limiter so a hammered JWKS endpoint * can't lock the rest of the BFF out — discovery endpoints are * meant to be polled. */ @Injectable() export class JwksPublisher { constructor(@Inject(BFF_SIGNING_KEY) private readonly key: BffSigningKey) {} jwks(): { keys: readonly JWK[] } { return { keys: [this.key.publicJwk] }; } }