diff --git a/packages/agent-bff/README.md b/packages/agent-bff/README.md index fdfda47b8f..124f3d2749 100644 --- a/packages/agent-bff/README.md +++ b/packages/agent-bff/README.md @@ -1,6 +1,6 @@ # @forestadmin/agent-bff -Standalone REST BFF (Backend-For-Frontend) that lets a trusted third-party UI call a Forest Admin +Standalone REST BFF (Backend-For-Frontend) that lets a trusted third-party UI call a Forest agent from a browser without learning MCP or JSON:API. It is a bootable Koa 3 server with a `/health` endpoint, a version header, env-driven config diff --git a/packages/agent-bff/package.json b/packages/agent-bff/package.json index 97b3c1ac5c..3ce77fcc0b 100644 --- a/packages/agent-bff/package.json +++ b/packages/agent-bff/package.json @@ -22,8 +22,9 @@ "dist/**/*.d.ts" ], "scripts": { - "build": "tsc", + "build": "tsc && yarn build:copy", "build:watch": "tsc --watch", + "build:copy": "node -e \"require('fs').copyFileSync(require.resolve('redoc/bundles/redoc.standalone.js'), 'dist/docs/redoc.standalone.js')\"", "start": "node dist/cli.js", "start:dev": "node --env-file=.env dist/cli.js", "clean": "rm -rf coverage dist", @@ -47,6 +48,7 @@ "@types/koa": "^2.13.5", "@types/supertest": "^6.0.2", "openapi3-ts": "4.6.1", + "redoc": "2.5.3", "supertest": "^7.1.3" } } diff --git a/packages/agent-bff/src/cli-core.ts b/packages/agent-bff/src/cli-core.ts index 01732f8da2..a8caffa19e 100644 --- a/packages/agent-bff/src/cli-core.ts +++ b/packages/agent-bff/src/cli-core.ts @@ -19,6 +19,7 @@ import { parseConfig } from './config/env-config'; import createCorsMiddleware from './cors/cors-middleware'; import createPerKeyOriginMiddleware from './cors/per-key-origin'; import createDataRoutesMiddleware from './data/data-routes-middleware'; +import createDocsRoutes from './docs/docs-routes'; import { extractErrorMessage } from './errors'; import { unauthorized } from './http/bff-http-error'; import BFFHttpServer from './http/bff-http-server'; @@ -28,7 +29,7 @@ import ForestServerClient from './oauth/forest-server-client'; import createOAuthRoutes from './oauth/oauth-routes'; import createInMemorySessionStore from './oauth/session-store'; import createTokenCipher from './oauth/token-cipher'; -import createOpenApiRoutes from './openapi/openapi-routes'; +import createOpenApiRoutes, { OPENAPI_PATH } from './openapi/openapi-routes'; import PermissionsCache from './permissions/permissions-cache'; import PermissionsClient from './permissions/permissions-client'; import createPermissionsRoutesMiddleware from './permissions/permissions-routes-middleware'; @@ -313,6 +314,14 @@ export default async function runCli( ...agentErrorMiddleware, bodyParser({ jsonLimit: BODY_LIMIT }), ...oauthMiddlewares, + // Outside the agent-scoped chain on purpose: the viewer is a public page, the document it fetches + // is not. Gated on the edge being mounted too, like the error middleware above: with no agent + // chain there is no document to fetch, and the page would only ever reach a bare Koa 404. + createDocsRoutes({ + enabled: config.openapiEnabled && agentMiddlewares.length > 0, + documentPath: OPENAPI_PATH, + logger, + }), ...agentMiddlewares, ]; const server = new BFFHttpServer({ diff --git a/packages/agent-bff/src/docs/docs-page.ts b/packages/agent-bff/src/docs/docs-page.ts new file mode 100644 index 0000000000..82fb20e388 --- /dev/null +++ b/packages/agent-bff/src/docs/docs-page.ts @@ -0,0 +1,166 @@ +/** + * The page is served WITHOUT credentials, so it must carry no schema: it is an empty shell that asks + * the caller for a BFF API key, fetches the document with it, and hands the parsed object to Redoc. + * That is the only design that is both openable in a browser — which sends no header when it + * navigates — and compatible with a document that is never reachable unauthenticated. + * + * The key is never persisted: it is read from the input, passed down as an argument, and the input is + * cleared. Once the document is fetched the page has no further use for it. + * + * Deliberately NOT a `