Skip to content

fix(agent-bff): publish BFF_PUBLIC_URL as the openapi server url - #1866

Open
Tonours wants to merge 7 commits into
mainfrom
fix/prd-1109-openapi-servers
Open

fix(agent-bff): publish BFF_PUBLIC_URL as the openapi server url#1866
Tonours wants to merge 7 commits into
mainfrom
fix/prd-1109-openapi-servers

Conversation

@Tonours

@Tonours Tonours commented Aug 31, 2026

Copy link
Copy Markdown
Member

fixes PRD-1109

What changed

Optional BFF_PUBLIC_URL now carries the deployment's external base URL into servers[0].url of every document shape: the generic document, the served unfolded document, and the forest-bff openapi export. Unset, servers[0].url stays /. A malformed value fails the boot. Trailing slashes, query strings, and fragments are rejected, because generated clients concatenate servers[0].url + path and anything behind a ? or # swallows the path.

Why these choices

  • Not a templated server variable. @hey-api/openapi-ts (the reference generator in test/openapi/openapi-generated-client.test.ts) returns no base URL at all when a server url holds a {variable}. The / it resolves today is worth more than nothing.
  • Not derived from the request. The unfolded document is memoized on the read model, so one caller's Host header would end up in every other caller's document.
  • Not FOREST_SERVER_URL. That one points at the Forest SaaS API, not at this BFF.
  • Unset keeps / on purpose. Per OAS, a relative server url resolves against the retrieval url, so Swagger UI or Postman importing the served document resolves the right host today. Only the offline export has no origin to resolve against, and that is the case BFF_PUBLIC_URL exists for.

How to test

BFF_PUBLIC_URL=https://bff.example.com yarn workspace @forestadmin/agent-bff start:dev &
curl -s -H "Authorization: Bearer $TOKEN" localhost:3450/agent/openapi.json | jq .servers
BFF_PUBLIC_URL='https://x?tenant=1' forest-bff openapi   # exits with a ConfigurationError

yarn workspace @forestadmin/agent-bff test passes 79 suites / 1363 tests, lint and build are clean. New cases cover parsing (valid, malformed, trailing slash, query, fragment, unset) and servers[0].url in the document builder, the served route, and both export branches.

Known limitation

Without BFF_PUBLIC_URL, a client generated from the offline export still has no base URL. The fallback server entry names the variable to set.

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-1109

@qltysh

qltysh Bot commented Aug 31, 2026

Copy link
Copy Markdown

3 new issues

Tool Category Rule Count
qlty Structure Function with many returns (count = 5): createOpenApiRoutes 2
qlty Structure Function with many parameters (count = 4): buildUnfoldedDocument 1

Comment thread packages/agent-bff/src/openapi/openapi-routes.ts
Comment thread packages/agent-bff/src/cli-dispatch.ts
@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 (3)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
packages/agent-bff/src/openapi/openapi-document.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent-bff/src/cli-dispatch.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent-bff/src/config/env-config.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.

@nbouliol nbouliol left a comment

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.

BFF_PUBLIC_URL is missing from .env.example, which lists the other optional BFF vars, and the forest-bff openapi export section of the README doesn't point at it — that export being the case the variable exists for.

Comment thread packages/agent-bff/test/openapi/openapi-cli.test.ts Outdated
Comment thread packages/agent-bff/test/openapi/openapi-document.test.ts Outdated
Comment thread packages/agent-bff/src/cli-dispatch.ts
@Tonours

Tonours commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Both added.

.env.example gains # BFF_PUBLIC_URL=https://bff.example.com, commented like the other optional vars.

The forest-bff openapi section now says why it matters there specifically:

Set BFF_PUBLIC_URL for this command in particular. A document fetched over HTTP resolves its relative servers[0].url against the URL it came from; an exported file has no such origin, so without it a client generated from the export has no base URL at all.

I also widened the env-table row, which said only "a malformed value fails the boot" and did not name the two rejections that matter: a URL carrying credentials would publish them to every reader of the document, and anything behind a ? or # swallows the path a generated client appends.

@Tonours

Tonours commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

The credential check I added last push did not hold. Fixed properly now.

It was a raw-string regex, /^https?:\/\/[^\/?#]*@/, while isHttpUrl validates through zod, which checks the normalised url.protocol. Anything WHATWG parses as https://user:pass@host without literally starting with lowercase http:// slipped through both. Measured:

HTTPS://admin:s3cret@bff.example.com   protocol https:  user "admin"  regex blocks: false
https:\\admin:s3cret@bff.example.com   protocol https:  user "admin"  regex blocks: false
https:/admin:s3cret@bff.example.com    protocol https:  user "admin"  regex blocks: false
https:admin:s3cret@bff.example.com     protocol https:  user "admin"  regex blocks: false
https://admin:s3cret@bff.example.com   protocol https:  user "admin"  regex blocks: true

So BFF_PUBLIC_URL=HTTPS://admin:s3cret@bff.example.com booted and published the password in servers[0].url of every served document and of the forest-bff openapi export — the export being the artefact most likely to end up committed to a repository. Exactly the class the previous commit claimed to close.

The check now runs on the parsed URL, which is the only thing that agrees with the validator:

const url = new URL(value);

if (url.username !== '' || url.password !== '') throw ;

return url.href.replace(/\/+$/, '');

Emitting url.href rather than the raw string closes the second half of the same problem: https:/bff.example.com was being published verbatim, so a generated client concatenated onto a single-slash URL. It now normalises to https://bff.example.com, and HTTPS://BFF.Example.COM to https://bff.example.com.

Four bypass forms added to the rejection table, plus a canonicalisation case. The ?/# check stays a string check on purpose — that one has to catch bare delimiters, which the parsed object reports as empty.

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