diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bf2d26b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +# Dependencies (installed fresh inside the image) +node_modules/ +server/node_modules/ + +# Build outputs (generated fresh inside the image) +dist/ +server/dist/ +server/public/ + +# Test outputs +coverage/ +playwright-report/ +test-results/ +e2e/ + +# VCS / CI / editor +.git/ +.github/ +.husky/ +.bob/ +.claude/ +.vscode/ +.idea/ +.DS_Store + +# Env files — never bake secrets/config into the image; env comes from +# `docker run -e` / compose at runtime. +*.env +*.env.* +!.env*.example +server/.env + +# Docs (not needed at build time) +*.md +LICENSE diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4134f0d --- /dev/null +++ b/.env.example @@ -0,0 +1,62 @@ +# BFF config — the single .env for both ways of running this app. Copy to +# .env at the repo root: +# cp .env.example .env +# Both `cd server && npm run dev`/`npm start` (via `--env-file-if-exists=../.env`) +# and `docker compose up` read this same file. Every var maps 1:1 to +# server/src/config.ts. For a production-ready template, see +# .env.prod.example. + +PORT=3000 +HOST=0.0.0.0 + +# Upstream ContextForge/mcpgateway instance. Set this to your instance's +# address; 0.0.0.0:8000 is the default local dev address. +# Docker: if not on the same docker network, use the host.docker.internal +# line instead. +# CONTEXTFORGE_URL=http://host.docker.internal:8000 +CONTEXTFORGE_URL=http://0.0.0.0:8000 + +# Must match mcpgateway's own AUTH_HEADER_NAME. +CONTEXTFORGE_AUTH_HEADER_NAME=Authorization + +# Left UNSET on purpose — behaves correctly either way this file is used: +# Native: config.ts's own default applies (memory:// — in-process, +# lost on restart, single-instance only, no Redis needed). +# Docker: docker-compose.yml defaults REDIS_URL to its own `redis` +# service, so sessions are Redis-backed automatically. +# Set a value here to override either default. +# REDIS_URL=redis://localhost:6379/0 + +# Opaque session_id -> bearer token TTL in Redis, seconds. +SESSION_TTL_SECONDS=86400 + +# Redis key namespace. Only needs changing if multiple BFF deployments +# (e.g. staging and prod) ever share one Redis instance. +REDIS_KEY_PREFIX=bff + +# Leave unset for a host-only cookie (recommended unless the BFF and its +# subdomains genuinely need to share the session cookie). +COOKIE_DOMAIN= + +# REQUIRED false for a zero-config boot (native or Docker) — the default +# (true) is for prod and fails closed on memory:// Redis and on a missing +# PUBLIC_ORIGIN/TRUST_PROXY. See config.ts's two fail-closed startup checks. +COOKIE_SECURE=false + +# Only safe behind a trusted reverse proxy that overwrites (not appends to) +# X-Forwarded-For. Leave "false" for a directly-exposed BFF. +TRUST_PROXY=false + +# Exact scheme://host the BFF is publicly reached at (e.g. +# https://app.example.com), used for Origin-header validation on login/SSE. +# Leave unset to derive it from the request itself — fine for a +# single-hostname deployment; set explicitly behind a reverse proxy where +# that derivation isn't trustworthy (e.g. TLS-terminated without +# TRUST_PROXY=true). +PUBLIC_ORIGIN= + +# How often an open SSE connection re-checks Redis for session revocation, +# as a fallback to the pub/sub-based instant revocation. +SSE_SESSION_RECHECK_SECONDS=15 + +LOG_LEVEL=info diff --git a/.env.prod.example b/.env.prod.example new file mode 100644 index 0000000..a63cf08 --- /dev/null +++ b/.env.prod.example @@ -0,0 +1,54 @@ +# BFF config — production-ready template. Copy to .env: +# cp .env.prod.example .env +# Unlike .env.example, nothing here has a safe zero-config default — every +# blank value below MUST be set before this will boot (server/src/config.ts +# fails closed rather than serving traffic insecurely). See DOCKER.md's +# production checklist. + +PORT=3000 +HOST=0.0.0.0 + +# Your real upstream ContextForge/mcpgateway instance. Set this to your +# instance's address; 0.0.0.0:4444 is the default local address. +# Docker: if not on the same docker network, use the host.docker.internal +# line instead. +# CONTEXTFORGE_URL=http://host.docker.internal:4444 +CONTEXTFORGE_URL=http://0.0.0.0:4444 + +# Must match mcpgateway's own AUTH_HEADER_NAME. +CONTEXTFORGE_AUTH_HEADER_NAME=Authorization + +# Real, persistent Redis — required. Set to your instance's address. +# Docker, different network: use the host.docker.internal line instead. +# REDIS_URL=redis://host.docker.internal:6379/0 +REDIS_URL=redis://0.0.0.0:6379/0 + +# Opaque session_id -> bearer token TTL in Redis, seconds. +SESSION_TTL_SECONDS=86400 + +# Redis key namespace. Change if this deployment shares one Redis instance +# with another BFF deployment (e.g. staging). +REDIS_KEY_PREFIX=bff + +# Leave unset for a host-only cookie (recommended unless the BFF and its +# subdomains genuinely need to share the session cookie). +COOKIE_DOMAIN= + +# Required true in production — config.ts fails closed if this is true +# without a real REDIS_URL, or without PUBLIC_ORIGIN/TRUST_PROXY below. +COOKIE_SECURE=true + +# Set true ONLY if directly TLS-terminated with no reverse proxy in front. +# Otherwise leave false and set PUBLIC_ORIGIN instead. +TRUST_PROXY=false + +# Exact scheme://host this deployment is publicly reached at, e.g. +# https://app.example.com. Required unless TRUST_PROXY=true — origin-guard.ts +# can't validate Origin behind a TLS-terminating proxy without one of these. +PUBLIC_ORIGIN= + +# How often an open SSE connection re-checks Redis for session revocation, +# as a fallback to the pub/sub-based instant revocation. +SSE_SESSION_RECHECK_SECONDS=15 + +LOG_LEVEL=info diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..4179c07 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,141 @@ +# Running in Docker + +This repo ships as **one image**: the BFF (`server/`, Fastify) serves the +built UI (root, Vite/React SPA) as static files and proxies `/api/*`, so +the whole client stack — UI + BFF — is a single container. Redis is a +separate service, wired in via `docker-compose.yml`. + +`.env`/`.env.example` are shared with native (non-Docker) dev — see the +root README's Getting Started section. `docker-compose.yml` and +`server`'s native `npm run dev`/`start` both read the same repo-root +`.env`. + +The upstream ContextForge/mcpgateway API is **not** part of this repo or +this compose file — it's expected to already be running somewhere you +point `CONTEXTFORGE_URL` at. + +## Quick start + +```bash +cp .env.example .env +# edit .env: CONTEXTFORGE_URL defaults to 0.0.0.0:8000, which is only +# correct for native dev. If your gateway runs on the host, set: +# CONTEXTFORGE_URL=http://host.docker.internal:8000 +docker compose up --build +``` + +Visit `http://localhost:3000/` — redirects to `/app/login`. `GET /healthz` +returns `{"ok":true}`. + +By default this boots with `COOKIE_SECURE=false` and sessions backed by +this compose file's own `redis` service (`docker-compose.yml` defaults +`REDIS_URL` to it; see below to override). + +## Environment variables + +Full reference: `.env.example` (each var has an inline comment). +Summary, grouped the same way: + +| Group | Vars | Notes | +|---|---|---| +| Works out of the box | `COOKIE_SECURE=false` | Required (or set `PUBLIC_ORIGIN`/`TRUST_PROXY`) for a zero-config boot — `server/src/config.ts` fails closed otherwise. | +| Must be set | `CONTEXTFORGE_URL` | No safe default reaches your gateway from inside the container. **No boot-time check catches a missing/wrong value** — it just fails every `/api/*` call at request time. Top thing to check if API calls all connection-refuse. | +| Fine as-is for dev | `PORT`, `HOST`, `CONTEXTFORGE_AUTH_HEADER_NAME`, `SESSION_TTL_SECONDS`, `REDIS_KEY_PREFIX`, `COOKIE_DOMAIN`, `TRUST_PROXY`, `PUBLIC_ORIGIN`, `SSE_SESSION_RECHECK_SECONDS`, `LOG_LEVEL` | Defaults match `server/src/config.ts`. | + +The image itself (`Dockerfile`) sets **none** of these — it ships +respecting `config.ts`'s own defaults untouched. All configuration comes +from the environment at run time. + +## Redis + +`docker-compose.yml` defaults `REDIS_URL` to its own `redis` service, so +sessions are Redis-backed out of the box — no `.env` edit needed. Confirm +it: hit the login route, then + +```bash +docker compose exec redis redis-cli KEYS 'bff:*' +``` + +should show keys, and a session survives `docker compose restart app`. + +To use something else instead, set `REDIS_URL` in `.env` — e.g. a +different Redis, or `REDIS_URL=memory://` for the in-process, +lost-on-restart, single-instance-only fallback (`.env`'s value overrides +the compose default). If you see the `memory-redis` warning in +`docker compose logs app` and didn't ask for it, check `.env` isn't +setting `REDIS_URL=memory://`. + +## Production checklist + +Before this leaves a laptop: + +- `COOKIE_SECURE=true` +- `REDIS_URL=redis://...` pointing at a real, persistent Redis (not `memory://`) +- `PUBLIC_ORIGIN=https://your-domain.example.com`, or `TRUST_PROXY=true` if + directly TLS-terminated with no reverse proxy in front +- `CONTEXTFORGE_URL` pointing at your real gateway + +Get any of the first two wrong and the container won't boot at all — +`server/src/config.ts` throws at startup rather than serving traffic +insecurely. That's intentional; don't work around it by setting +`NODE_ENV=production` or similar in the image itself. + +## Joining an existing stack / network + +The provided `docker-compose.yml` is a standalone reference stack (app + +redis). If you already have your own Redis, network, or reverse proxy: + +**Option A — run the image directly**, pointing at your own infra: + +```bash +docker build -t contextforge-web-ui . +docker run -p 3000:3000 \ + --network your-existing-network \ + -e COOKIE_SECURE=true \ + -e REDIS_URL=redis://your-redis-host:6379/0 \ + -e CONTEXTFORGE_URL=http://your-gateway:4444 \ + -e PUBLIC_ORIGIN=https://your-domain.example.com \ + contextforge-web-ui +``` + +**Option B — override compose**, attaching to an external network instead +of the bundled `redis` service: + +```yaml +# docker-compose.override.yml +services: + app: + networks: [external_net] + environment: + REDIS_URL: redis://your-existing-redis:6379/0 + +networks: + external_net: + external: true +``` + +```bash +docker compose -f docker-compose.yml -f docker-compose.override.yml up --build +``` + +## Troubleshooting + +- **Every `/api/*` request (including login) fails with `ECONNREFUSED`**: + `CONTEXTFORGE_URL` is unreachable from inside the container — most often + because it's still set to `.env.example`'s native-dev default + (`0.0.0.0:8000`/`127.0.0.1:...`), which inside a container points at the + container's own loopback, not the host. If your gateway runs on the + host, set `CONTEXTFORGE_URL=http://host.docker.internal:8000` instead + (`docker-compose.yml` maps that hostname to the host on both Docker + Desktop and Linux). There's no boot-time check for this — the app + starts fine either way. +- **Container crash-loops on startup**: check `docker compose logs app` — + `config.ts` throws a specific error for each fail-closed case + (`memory://` Redis with `COOKIE_SECURE=true`, or `COOKIE_SECURE=true` + with neither `PUBLIC_ORIGIN` nor `TRUST_PROXY` set). The message tells + you exactly which var to set. +- **Multi-arch builds** (e.g. building on Apple Silicon for an amd64 + target): `docker buildx build --platform linux/amd64,linux/arm64 -t contextforge-web-ui .` + — the UI stage's native dependency (`lightningcss`, via + `@tailwindcss/vite`) ships prebuilt musl binaries for both architectures, + so no Dockerfile changes should be needed. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..025e22f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +# Builds the UI (Vite/React SPA) and the BFF (Fastify) into one runtime +# image. The BFF serves the built SPA as static files — see +# server/src/plugins/static.ts and vite.config.ts's `build.outDir`. +# +# Deliberately sets no NODE_ENV/COOKIE_SECURE/REDIS_URL here: server/src/config.ts +# owns those defaults (and fails closed on insecure combinations by design). +# Supply the right values at `docker run -e` / compose time instead. + +# ---- UI dependencies ---- +FROM node:22-alpine AS ui-deps +WORKDIR /ui +COPY package.json package-lock.json ./ +RUN npm ci --no-audit --no-fund + +# ---- UI build ---- +# npm run build = "npm run generate && tsc -b && vite build". `generate` +# runs orval against the committed openapi.json (no network call). vite's +# outDir is "server/public", so output lands at /ui/server/public here. +FROM ui-deps AS ui-build +WORKDIR /ui +COPY openapi.json orval.config.ts index.html vite.config.ts ./ +COPY tsconfig.json tsconfig.app.json tsconfig.node.json ./ +COPY public ./public +COPY src ./src +RUN npm run build + +# ---- BFF dependencies ---- +# Full (non-prod) install here — tsc is a devDependency needed to build. +FROM node:22-alpine AS bff-deps +WORKDIR /app +COPY server/package.json server/package-lock.json ./ +RUN npm ci --no-audit --no-fund + +# ---- BFF build ---- +FROM bff-deps AS bff-build +WORKDIR /app +COPY server/tsconfig.json ./ +COPY server/src ./src +RUN npm run build + +# ---- Runtime ---- +FROM node:22-alpine AS runtime +WORKDIR /app +COPY server/package.json server/package-lock.json ./ +RUN npm ci --omit=dev --no-audit --no-fund +COPY --from=bff-build /app/dist ./dist +COPY --from=ui-build /ui/server/public ./public +RUN addgroup -S app && adduser -S app -G app && chown -R app:app /app +USER app +EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" +CMD ["node", "dist/index.js"] diff --git a/README.md b/README.md index 3175a7f..8c9661b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ This UI targets **ContextForge API v1.0.7**, matching [`openapi.json`](./openapi ## Getting Started +> Running this in Docker instead? See [DOCKER.md](./DOCKER.md). + ### Prerequisites - Node.js 20+ and npm @@ -46,28 +48,28 @@ Bring them up in this order: https://github.com/IBM/mcp-context-forge/issues/2503 Note whatever port it ends up listening on for the next step. -2. **Configure and start the BFF** (terminal B, this repo's `server/`): +2. **Configure and start the BFF** (terminal B, from the repo root): ```bash - cd server cp .env.example .env ``` - Edit `server/.env`: - - `FASTAPI_URL` — point it at whatever host:port ContextForge is - listening on from step 1 (`.env.example`'s default is `4444`; confirm - against your ContextForge run rather than assuming). + Edit `.env`: + - `CONTEXTFORGE_URL` — point it at whatever host:port ContextForge is + listening on from step 1 (`.env.example`'s default is `0.0.0.0:8000`; + confirm against your ContextForge run rather than assuming). - `COOKIE_SECURE=false` — needed for local HTTP; the default (`true`) is for prod and silently drops the session cookie over plain HTTP. Other values (`PORT`, `REDIS_URL`, `SESSION_TTL_SECONDS`, etc.) have - dev-safe defaults — see comments in `server/.env.example`. - `REDIS_URL=memory://` (the default) is an in-process store, no Redis - process needed for local dev — state resets on restart. + dev-safe defaults — see comments in `.env.example`. `REDIS_URL` is left + unset, which falls back to an in-process store (no Redis process needed + for local dev — state resets on restart). ```bash + cd server npm install - npm run dev # :3000, tsx watch + npm run dev # :3000, tsx watch, reads ../.env ``` 3. **Build the frontend for the BFF to serve**, from the repo root: @@ -326,6 +328,9 @@ client/ ├── tsconfig.app.json # TypeScript app config ├── vite.config.ts # Vite configuration (builds to server/public/) ├── package.json # Dependencies and scripts +├── .env.example # Shared BFF config — copy to .env (see Getting Started) +├── .env.prod.example # Production-ready template — copy to .env +├── Dockerfile / docker-compose.yml / DOCKER.md # see DOCKER.md └── server/ # BFF (Fastify): session/CSRF boundary in front of ContextForge ├── src/ │ ├── index.ts # Entrypoint @@ -333,7 +338,6 @@ client/ │ ├── plugins/ # cookie, redis, session, csrf, static │ └── routes/ # auth/, proxy/ (catch-all to ContextForge), sse/ ├── public/ # Built SPA (npm run build output), served by BFF - ├── .env.example # Copy to .env and configure FASTAPI_URL etc. └── package.json ``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fb84128 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "${PORT:-3000}:${PORT:-3000}" + env_file: + - .env + environment: + # `-` not `:-`: only substitute when REDIS_URL is unset, not when + # it's present-but-blank (e.g. an unfilled .env.prod.example). + REDIS_URL: ${REDIS_URL-redis://redis:6379/0} + extra_hosts: + # Lets CONTEXTFORGE_URL=http://host.docker.internal: reach a + # gateway running on the host. Docker Desktop (mac/Windows) resolves + # this automatically; Linux needs this mapping explicitly. + - "host.docker.internal:host-gateway" + depends_on: + redis: + condition: service_healthy + restart: unless-stopped + + redis: + image: redis:7-alpine + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 5 + restart: unless-stopped + volumes: + - redis-data:/data + +volumes: + redis-data: diff --git a/server/.env.example b/server/.env.example deleted file mode 100644 index 80168df..0000000 --- a/server/.env.example +++ /dev/null @@ -1,51 +0,0 @@ -# BFF server config. Copy to .env and adjust for your environment. - -PORT=3000 -HOST=0.0.0.0 - -# Upstream ContextForge API (FastAPI). Server-to-server only. -FASTAPI_URL=http://127.0.0.1:4444 - -# Must match mcpgateway's own AUTH_HEADER_NAME. -FASTAPI_AUTH_HEADER_NAME=Authorization - -# memory:// = in-process store, no Redis process needed (dev only — state is -# lost on restart, not shared across instances). Use a real redis:// URL for -# anything beyond a single local dev process, e.g. redis://localhost:6379/0. -REDIS_URL=memory:// - -# Opaque session_id -> bearer token TTL in Redis, seconds. -SESSION_TTL_SECONDS=86400 - -# Redis key namespace. Only needs changing if multiple BFF deployments -# (e.g. staging and prod) ever share one Redis instance. -REDIS_KEY_PREFIX=bff - -# Leave unset for a host-only cookie (recommended unless the BFF and its -# subdomains genuinely need to share the session cookie). -COOKIE_DOMAIN= -# "false" is the local-HTTP dev value, and is what this file ships with so a -# fresh `cp .env.example .env` boots against the REDIS_URL=memory:// default -# above. Set to "true" in prod — config.ts fails closed on COOKIE_SECURE=true -# paired with either memory:// or an unset PUBLIC_ORIGIN/TRUST_PROXY, so a prod -# deployment must set REDIS_URL and PUBLIC_ORIGIN (or TRUST_PROXY) alongside it. -COOKIE_SECURE=false - -# Only safe behind a trusted reverse proxy that overwrites (not appends to) -# X-Forwarded-For. Leave "false" for a directly-exposed BFF. -TRUST_PROXY=false - -# Exact scheme://host the BFF is publicly reached at (e.g. -# https://app.example.com), used for Origin-header validation on login/SSE. -# Leave unset to derive it from the request itself — fine for a -# single-hostname deployment; set explicitly behind a reverse proxy where -# that derivation isn't trustworthy (e.g. TLS-terminated without -# TRUST_PROXY=true). -PUBLIC_ORIGIN= - -# How often an open SSE connection re-checks Redis for session revocation, -# as a fallback to the pub/sub-based instant revocation. See -# agent-output/bff-proxy-and-sse-plan.md. -SSE_SESSION_RECHECK_SECONDS=15 - -LOG_LEVEL=info diff --git a/server/package.json b/server/package.json index f778290..80b38e9 100644 --- a/server/package.json +++ b/server/package.json @@ -8,9 +8,9 @@ "node": ">=18.0.0" }, "scripts": { - "dev": "tsx watch --env-file-if-exists=.env src/index.ts", + "dev": "tsx watch --env-file-if-exists=../.env src/index.ts", "build": "tsc -p tsconfig.json", - "start": "node --env-file-if-exists=.env dist/index.js", + "start": "node --env-file-if-exists=../.env dist/index.js", "test": "vitest", "test:run": "vitest run", "lint": "tsc -p tsconfig.json --noEmit" diff --git a/server/src/config.ts b/server/src/config.ts index e72ac98..4bc102d 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -3,8 +3,8 @@ // SPDX-License-Identifier: Apache-2.0 // // Env-driven config for the BFF. All values have dev-safe defaults; override -// via env in every non-local deployment (COOKIE_SECURE and FASTAPI_URL in -// particular). +// via env in every non-local deployment (COOKIE_SECURE and CONTEXTFORGE_URL +// in particular). function optional(name: string, fallback: string): string { return process.env[name] ?? fallback; @@ -24,17 +24,20 @@ export const config = { port: Number(optional("PORT", "3000")), host: optional("HOST", "0.0.0.0"), - // Upstream ContextForge API (FastAPI). All bearer-token traffic goes here, + // Upstream ContextForge API. All bearer-token traffic goes here, // server-to-server only — the browser never talks to this origin directly. - fastapiUrl: optional("FASTAPI_URL", "http://127.0.0.1:4444"), + // Not named after the upstream's current framework (FastAPI) since that's + // an implementation detail ContextForge could change independently. + contextforgeUrl: optional("CONTEXTFORGE_URL", "http://127.0.0.1:4444"), // Header mcpgateway reads the bearer token from — must match its own AUTH_HEADER_NAME. - fastapiAuthHeaderName: optional("FASTAPI_AUTH_HEADER_NAME", "Authorization"), + contextforgeAuthHeaderName: optional("CONTEXTFORGE_AUTH_HEADER_NAME", "Authorization"), // memory:// (default) = in-process store, no Redis needed — dev only. // See lib/memory-redis.ts. Use a real redis:// URL beyond a single - // local dev process. - redisUrl: optional("REDIS_URL", "memory://"), + // local dev process. optionalUnset so REDIS_URL="" also falls through + // to this default and trips the fail-closed check below. + redisUrl: optionalUnset("REDIS_URL") ?? "memory://", // Opaque session_id -> { bearerToken, user } TTL in Redis. Independent of // the upstream JWT's own expiry; the BFF just stops trusting a stale @@ -84,9 +87,9 @@ if ( throw new Error("REDIS_URL=memory:// is dev-only — set a real redis:// URL in production"); } -if (!HTTP_TOKEN_RE.test(config.fastapiAuthHeaderName)) { +if (!HTTP_TOKEN_RE.test(config.contextforgeAuthHeaderName)) { throw new Error( - `FASTAPI_AUTH_HEADER_NAME "${config.fastapiAuthHeaderName}" is not a valid HTTP header token`, + `CONTEXTFORGE_AUTH_HEADER_NAME "${config.contextforgeAuthHeaderName}" is not a valid HTTP header token`, ); } diff --git a/server/src/lib/upstream-auth.ts b/server/src/lib/upstream-auth.ts index 97460f4..5aee918 100644 --- a/server/src/lib/upstream-auth.ts +++ b/server/src/lib/upstream-auth.ts @@ -3,11 +3,11 @@ // SPDX-License-Identifier: Apache-2.0 // // Bearer header for calls to mcpgateway — name configurable via -// FASTAPI_AUTH_HEADER_NAME (see config.ts), so proxy/SSE/logout stay in sync. +// CONTEXTFORGE_AUTH_HEADER_NAME (see config.ts), so proxy/SSE/logout stay in sync. import { config } from "../config.js"; -const AUTH_HEADER_KEY = config.fastapiAuthHeaderName.toLowerCase(); +const AUTH_HEADER_KEY = config.contextforgeAuthHeaderName.toLowerCase(); export function upstreamAuthHeader(bearerToken: string): Record { return { [AUTH_HEADER_KEY]: `Bearer ${bearerToken}` }; diff --git a/server/src/lib/upstream-http-client.ts b/server/src/lib/upstream-http-client.ts index b0ab028..7ca632a 100644 --- a/server/src/lib/upstream-http-client.ts +++ b/server/src/lib/upstream-http-client.ts @@ -12,7 +12,7 @@ import { Pool } from "undici"; import { config } from "../config.js"; -export const sseUpstreamPool = new Pool(config.fastapiUrl, { +export const sseUpstreamPool = new Pool(config.contextforgeUrl, { headersTimeout: 0, bodyTimeout: 0, }); diff --git a/server/src/routes/auth/login.ts b/server/src/routes/auth/login.ts index 82bd5af..e9cee69 100644 --- a/server/src/routes/auth/login.ts +++ b/server/src/routes/auth/login.ts @@ -46,7 +46,7 @@ export default async function loginRoute(fastify: FastifyInstance): Promise { try { - const response = await fetch(`${config.fastapiUrl}/auth/logout`, { + const response = await fetch(`${config.contextforgeUrl}/auth/logout`, { method: "POST", headers: upstreamAuthHeader(bearerToken), signal: AbortSignal.timeout(UPSTREAM_REVOKE_TIMEOUT_MS), diff --git a/server/src/routes/proxy/catch-all.ts b/server/src/routes/proxy/catch-all.ts index 841234f..8d71943 100644 --- a/server/src/routes/proxy/catch-all.ts +++ b/server/src/routes/proxy/catch-all.ts @@ -30,7 +30,7 @@ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE"]); // (Cookie) are BFF-only secrets; the rest are infra/auth headers mcpgateway // trusts for request-URL construction (Forwarded/X-Forwarded-*, including // OAuth redirect URLs) or for the bearer token itself (Authorization / the -// configured FASTAPI_AUTH_HEADER_NAME). None of these are on the Fetch +// configured CONTEXTFORGE_AUTH_HEADER_NAME). None of these are on the Fetch // spec's forbidden-header list, so a browser tab can set them via fetch() // directly — strip all of them and let the BFF inject its own values below, // rather than only overwriting the ones we happen to already set. @@ -48,7 +48,7 @@ const STRIPPED_INBOUND_HEADERS = new Set([ function stripInboundHeaders( headers: Record, ): Record { - const authHeaderKey = config.fastapiAuthHeaderName.toLowerCase(); + const authHeaderKey = config.contextforgeAuthHeaderName.toLowerCase(); const result: Record = {}; for (const [key, value] of Object.entries(headers)) { if (STRIPPED_INBOUND_HEADERS.has(key) || key === authHeaderKey) continue; @@ -73,10 +73,10 @@ function rewriteUpstreamLocation( const { "set-cookie": _dropped, ...rest } = headers; const location = rest.location; - if (typeof location !== "string" || !location.startsWith(config.fastapiUrl)) { + if (typeof location !== "string" || !location.startsWith(config.contextforgeUrl)) { return rest; } - const upstreamPath = location.slice(config.fastapiUrl.length); + const upstreamPath = location.slice(config.contextforgeUrl.length); return { ...rest, location: `/api${upstreamPath}` }; } @@ -92,7 +92,7 @@ function csrfIfUnsafe( } export default async function catchAllProxyRoute(fastify: FastifyInstance): Promise { - await fastify.register(replyFrom, { base: config.fastapiUrl }); + await fastify.register(replyFrom, { base: config.contextforgeUrl }); // Fastify's default JSON parser throws FST_ERR_CTP_EMPTY_JSON_BODY on an // empty body with Content-Type: application/json — before preHandler, so diff --git a/server/test/auth.test.ts b/server/test/auth.test.ts index 23c8594..e1ec71c 100644 --- a/server/test/auth.test.ts +++ b/server/test/auth.test.ts @@ -253,7 +253,7 @@ describe("POST /auth/logout", () => { }); expect(response.statusCode).toBe(200); - const revokeCall = fetchCalls.find((call) => call.url === `${config.fastapiUrl}/auth/logout`); + const revokeCall = fetchCalls.find((call) => call.url === `${config.contextforgeUrl}/auth/logout`); expect(revokeCall).toBeTruthy(); // The stored bearer token, minted at login — never a session/cookie value. expect(revokeCall?.authorization).toBe("Bearer upstream-jwt"); diff --git a/server/test/proxy.test.ts b/server/test/proxy.test.ts index 3050258..d24f23d 100644 --- a/server/test/proxy.test.ts +++ b/server/test/proxy.test.ts @@ -2,9 +2,9 @@ // Copyright contributors to the MCP-CONTEXT-FORGE project // SPDX-License-Identifier: Apache-2.0 // -// FASTAPI_URL must be set before src/config.ts (and anything importing it) +// CONTEXTFORGE_URL must be set before src/config.ts (and anything importing it) // is first evaluated, so the fake upstream server is spun up and -// process.env.FASTAPI_URL set in beforeAll, with every module under test +// process.env.CONTEXTFORGE_URL set in beforeAll, with every module under test // dynamic-imported afterwards rather than statically at the top of the file. import { createServer, type IncomingMessage, type Server } from "node:http"; @@ -57,7 +57,7 @@ beforeAll(async () => { await new Promise((resolve) => upstream.listen(0, "127.0.0.1", () => resolve())); const { port } = upstream.address() as AddressInfo; upstreamOrigin = `http://127.0.0.1:${port}`; - process.env.FASTAPI_URL = upstreamOrigin; + process.env.CONTEXTFORGE_URL = upstreamOrigin; }); afterAll(() => new Promise((resolve) => upstream.close(() => resolve()))); diff --git a/server/test/sse.test.ts b/server/test/sse.test.ts index 07ff7a5..b1a46aa 100644 --- a/server/test/sse.test.ts +++ b/server/test/sse.test.ts @@ -7,7 +7,7 @@ // Fastify/light-my-request's normal capture path — inject() would hang // waiting for a stream that's designed to live indefinitely. // -// Same env-ordering constraint as proxy.test.ts: FASTAPI_URL must be set +// Same env-ordering constraint as proxy.test.ts: CONTEXTFORGE_URL must be set // before anything importing src/config.ts (transitively, the SSE upstream // pool) is first evaluated, so every module under test is dynamic-imported // after the fake upstream server is listening. @@ -39,7 +39,7 @@ beforeAll(async () => { }); await new Promise((resolve) => upstream.listen(0, "127.0.0.1", () => resolve())); const { port } = upstream.address() as AddressInfo; - process.env.FASTAPI_URL = `http://127.0.0.1:${port}`; + process.env.CONTEXTFORGE_URL = `http://127.0.0.1:${port}`; process.env.SSE_SESSION_RECHECK_SECONDS = "3600"; // keep the recheck timer out of the way of these tests });