Skip to content

feat(agent-bff): log origin guard rejections - #1868

Open
Tonours wants to merge 2 commits into
mainfrom
feat/prd-1114-origin-rejection-logs
Open

feat(agent-bff): log origin guard rejections#1868
Tonours wants to merge 2 commits into
mainfrom
feat/prd-1114-origin-rejection-logs

Conversation

@Tonours

@Tonours Tonours commented Aug 31, 2026

Copy link
Copy Markdown
Member

fixes PRD-1114

What

Both CORS guards in agent-bff now log one Warn per rejection. Layer 2 (per-key origin) logs
{ origin, path, renderingId } right before the 403. Layer 1 logs { origin, path } when a
preflight carries an origin that is not allow-listed. Identifiers only, never the key or the
payload, per the logging convention. Both middleware factories take a required logger
option, wired from cli-core.ts.

Why

Until now every 403 origin_not_allowed and every rejected preflight left zero server-side
evidence. The browser reports a generic "blocked by CORS policy" and the BFF logs nothing, so
an operator troubleshooting a broken allowedOrigins setting had nothing to start from. This
is the most common operator-facing failure of this stack.

Scope and safety

No behavior change on pass paths, no change to response bodies or status codes. The signature
change stays internal: neither middleware is exported from the package barrel, and the three
test call sites move in the same commit. There is no dedup or rate limit on the log; a
misconfigured deployment emits one Warn per rejected request, which is the telemetry this
ticket asks for.

How to test

  • yarn workspace @forestadmin/agent-bff test : 79 suites, 1353 tests, green.
  • yarn build at the repo root: tsc over 24 projects, green.
  • eslint on the changed files: clean.

The rejection and pass behaviors are pinned by the supertest suites in
test/cors/. I did not drive a live server by hand; the suites mount the real Koa middleware
stack, which covers the same path.

Definition of Done

General

  • Write an explicit title for the Pull Request, following Conventional Commits specification
  • Test manually the implemented changes
  • Validate the code quality (indentation, syntax, style, simplicity, readability)

Security

  • Consider the security impact of the changes made

@linear-code

linear-code Bot commented Aug 31, 2026

Copy link
Copy Markdown

PRD-1114

@qltysh

qltysh Bot commented Aug 31, 2026

Copy link
Copy Markdown

Qlty


Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (2)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
packages/agent-bff/src/cors/cors-middleware.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent-bff/src/cors/per-key-origin.ts100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

@Tonours Tonours left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec (PRD-1114): conforms. Both guards emit one Warn per rejection carrying identifiers only (origin, path, renderingId where the identity exists), wired in cli-core.ts, pinned by tests including the zero-log pass paths.

Applies to: the PR's scope. Preferential. Layer-1 telemetry fires for preflights only, as the ticket scopes it, so three rejection shapes still leave no server-side trace: OAuth requests with a disallowed origin (no per-key identity), API keys with an empty allowedOrigins list, and origins allowed per-key but rejected by BFF_ALLOWED_ORIGINS. For a non-preflight request the browser then blocks the response after the server has run it, with nothing in the logs. Pre-existing and out of this ticket's scope; a follow-up could log layer-1 disallowed origins on simple requests too, distinguishing preflight from actual request.

ctx.set('Access-Control-Allow-Headers', ALLOWED_HEADERS);
ctx.set('Access-Control-Max-Age', String(PREFLIGHT_MAX_AGE_SECONDS));
} else if (origin) {
logger('Warn', 'BFF preflight origin rejected', { origin, path: ctx.path });

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preferential. The preflight classification keys on OPTIONS + Origin only. A spec-strict preflight also carries Access-Control-Request-Method (fetch spec), so a plain OPTIONS probe with a forwarded Origin (curl, monitoring) is logged as a preflight rejection. An operator grepping logs then hunts a browser preflight problem that never happened. Consider keying the classification on Access-Control-Request-Method as well. The existing preflight tests in this suite use OPTIONS + Origin without it, so this matches the established test contract; flagged as hardening, not a defect.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in faeda53: the rejection log now requires Access-Control-Request-Method alongside OPTIONS + Origin, so a plain OPTIONS probe with only an Origin stays silent. A test pins that shape.

return async function perKeyOriginMiddleware(ctx, next) {
const identity = ctx.state.apiKeyIdentity as { allowedOrigins?: string[] } | undefined;
const origin = ctx.get('Origin');
const identity = ctx.state.apiKeyIdentity as

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

permissions-routes-middleware.ts:38 already casts this same state slot to ResolvedApiKeyIdentity — reusing it keeps one shape, and lets identity.renderingId drop the ?.

logger('Warn', 'BFF per-key origin rejected', {
origin,
path: ctx.path,
renderingId: identity?.renderingId,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renderingId is not key-unique and the console logger emits no request id, so this can't be joined back to the Resolved BFF API key line. What about keyHash: fingerprintApiKey(ctx.get(BFF_KEY_HEADER)) alongside, like api-key-middleware.ts does?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants