feat(agent-bff): BFF API key resolver wiring#1730
Conversation
Read the X-Forest-Bff-Key header, parse fbff_<keyId>_<secret>, resolve it
server-to-server against the SaaS, cache the result (positive 60s / negative
10s, per-instance, bounded), and mint a fresh 5m agent JWT per request from the
resolved identity. The minted JWT is never cached.
Errors map to a nested { error: { type, status, message } } contract:
invalid_api_key (401), forest_identity_not_allowed (403), invalid_request
(400), key_resolution_unavailable (503, + Retry-After; also covers SaaS 429).
fixes PRD-663
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- middleware: confine the try to authenticate() so downstream handler errors propagate to Koa instead of being rewritten as API-key failures - client: parse Retry-After defensively (HTTP-date form -> undefined, no NaN) and guard the success-path JSON parse (malformed 200 -> 503) - resolve-cache: purge expired entries before reporting size() - tests: restore global.fetch between tests; cover the new guards Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7 new issues
|
| const middlewares = await buildOAuthMiddlewares(config, logger); | ||
| const oauthMiddlewares = await buildOAuthMiddlewares(config, logger); | ||
| const apiKeyMiddleware = buildApiKeyMiddleware(config, logger); | ||
| const middlewares = [...oauthMiddlewares, ...(apiKeyMiddleware ? [apiKeyMiddleware] : [])]; |
There was a problem hiding this comment.
🟡 Medium src/cli-core.ts:128
When API-key-only configuration is provided (e.g. FOREST_APP_URL or BFF_TOKEN_ENCRYPTION_KEY missing), runCli still builds and registers the API-key middleware so requests authenticate successfully. But /health is driven by config.hasAllRequired, which requires all OAuth fields, so it returns 503 degraded. Readiness/liveness probes then keep the service out of rotation or restart it indefinitely despite valid authenticated traffic. Consider making the health check reflect the auth modes actually enabled, or document that OAuth config is required for a healthy status.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/agent-bff/src/cli-core.ts around line 128:
When API-key-only configuration is provided (e.g. `FOREST_APP_URL` or `BFF_TOKEN_ENCRYPTION_KEY` missing), `runCli` still builds and registers the API-key middleware so requests authenticate successfully. But `/health` is driven by `config.hasAllRequired`, which requires all OAuth fields, so it returns `503 degraded`. Readiness/liveness probes then keep the service out of rotation or restart it indefinitely despite valid authenticated traffic. Consider making the health check reflect the auth modes actually enabled, or document that OAuth config is required for a healthy status.
|
Coverage Impact ⬆️ Merging this pull request will increase total coverage on Modified Files with Diff Coverage (10)
🛟 Help
|
- client: an HTTP 200 whose body is missing user/renderingId is an unusable resolution, not a caller error; surface it as unavailable (503) instead of letting a later user deref throw a 500 - tests: incomplete-200 payloads, and the middleware non-ApiKeyError 500 branch Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cover the SaaS 400 -> invalid_request (not negatively cached), the out-of-taxonomy status -> 503 default branch, and the rethrow of a non-ApiKeyResolveError. Brings api-key-authenticator + api-key-error to full line coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review feedback: - client: validate every field the mint reads (id/email/team/ permissionLevel/tags array) plus renderingId/allowedOrigins, so a partial 200 fails as unavailable instead of caching a body that later throws while minting - authenticator: mint before caching so an unmintable identity is never cached - resolve-cache: evict the oldest entry when full instead of dropping new ones, so a burst of negatives cannot freeze the cache - client: Retry-After must be a positive integer (drop date/negative/zero) - cli-core: require AGENT_URL to enable API key auth Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Revert the AGENT_URL requirement from resolveApiKeyConfig: the middleware only uses server URL + env secret + auth secret, and gating on AGENT_URL would silently disable auth when it is missing. AGENT_URL is already a REQUIRED_KEY, so /health reports degraded without it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# @forestadmin/agent-bff [1.3.0](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent-bff@1.2.0...@forestadmin/agent-bff@1.3.0) (2026-07-01) ### Features * **agent-bff:** BFF API key resolver wiring ([#1730](#1730)) ([df806aa](df806aa))

Context
Mode 2 (developer path) for the BFF: a request presents an
fbff_<keyId>_<secret>key in theX-Forest-Bff-Keyheader. The BFF resolves it server-to-server against the SaaS (POST /liana/v1/bff-api-keys/resolve, already shipped in forestadmin-server), caches the result briefly, and mints a fresh 5m agent JWT per request from the resolved identity. The minted JWT is never cached.Slice 2 of the auth foundation. The actual agent call is Slice 3 — here the JWT is asserted as correctly minted and attached (
ctx.state.agentToken/ctx.state.apiKeyIdentity), with the agent client out of scope.fixes PRD-663
What's in it
New
packages/agent-bff/src/api-key/:api-key.ts— parsefbff_<16hex>_<64hex>,hashApiKey(cache key = sha256 of keyId+secret),fingerprintApiKey(logs)api-key-client.ts/api-key-resolve-error.ts— S2S resolve call, typed errorsagent-token.ts— mint 5m JWT (camelCase + snake_case aliases, tags→Record, null→'')resolve-cache.ts— per-instance TTL cache (positive 60s / negative 10s),now()injected, bounded (10k)api-key-error.ts— nested{ error: { type, status, message } }contractapi-key-authenticator.ts— orchestrates parse → cache → resolve → mintapi-key-middleware.ts— reads the header, attaches toctx.state,Retry-Afteron 503Wired in
cli-core.ts(mounted independently of OAuth) and exported fromindex.ts.Error mapping (SaaS → BFF)
invalid_api_key(negative-cached ≤10s)forest_identity_not_allowed(negative-cached ≤10s)invalid_request(not cached)503 key_resolution_unavailable+Retry-After(not cached; SaaSRetry-Afterpropagated on 429)401 invalid_api_keylocally, no SaaS callNotes / follow-ups
role; the minted JWT omits it. To be revisited in Slice 3 when the agent call is wired.allowedOriginsenforcement and cross-cutting contract tests are owned by T6 (PRD-666). This PR only exposesallowedOriginson the attached identity for T6 to consume.Tests
49 focused tests (
test/api-key/): parse, cache TTLs + bound, mint claims/expiry, resolve error mapping, authenticator cache behavior, middleware attach/error/passthrough, no raw secret in logs. Lint + tsc clean.Includes a second commit addressing local review findings (downstream-error masking,
Retry-AfterNaN, unguarded success JSON,size()staleness, testfetchrestore).🤖 Generated with Claude Code
Note
Add Mode 2 BFF API key authentication middleware to
agent-bffX-Forest-Bff-Keyheader.Cache-Control: no-store.ApiKeyErrorresponses (400/401/403/503) with optionalRetry-After; unexpected errors return a generic 500.cli-core.tswhenforestServerUrl,forestEnvSecret, andforestAuthSecretare all present in config; logs a warning and skips otherwise.Changes since #1730 opened
ApiKeyClient.isResolvedIdentityand addedApiKeyClient.isIdentityUsermethod to require complete identity structure [86caf50]createResolveCachestore utility to remove oldest entry when cache reaches capacity [86caf50]ApiKeyClient.parseRetryAfterto only accept positive integer values for Retry-After headers [86caf50]agentUrlrequirement toresolveApiKeyConfigconfiguration validation [86caf50]createApiKeyAuthenticator.authenticatehandler to mintAuthenticatedApiKeyonce before caching [86caf50]agentUrlrequirement fromresolveApiKeyConfigfunction inagent-bffpackage [8c99477]Macroscope summarized bb63ce1.