Static-asset bundling — closes /openapi.json 503 + /.well-known/agents.json 404 + brk_ docs typo

2026-05-09

apibuilddocs

LOCKSTEP TRACEABILITY MATRIX --- api_endpoints: ["GET /openapi.json", "GET /.well-known/agents.json"] sdk_methods_updated: ["none — no public surface change"] mcp_tools_updated: ["none — no new tools"] ---

What We Built

/openapi.json and /.well-known/agents.json both returned errors against api.brainstormrouter.com (503 ENOENT and 404 respectively) because their source files (docs/openapi.yaml, site/public/.well-known/agents.json) weren't bundled into the runtime container — the Dockerfile's runtime stage only copies dist/, node_modules/, package.json, pnpm-workspace.yaml, and packages/. The handlers tried to read those files at request time and silently failed. Every developer-tool integration that follows the Link: <…>; rel="service-desc" header (Postman, Scalar, MCP/A2A clients, Claude/GPT tool-use) hit a wall as the first thing they tried.

This change inlines both assets at build time into a generated TypeScript module (src/api/static-assets.generated.ts), the same pattern that LLMS_TXT_CONTENT already uses in src/api/server.ts:72. The module is written by scripts/generate-static-assets.ts, which runs before tsdown in the build chain — so if either source file is missing or malformed, the build fails. The container deploy can no longer regress this class of bug.

While in the file, fixed the brk_br_live_ API-key prefix typo across the docs that LLMs read first: scripts/generate-llms-full.ts (the generator), README.md, both SDK READMEs, site/public/llms.txt, and site/public/llms-agent-playbook.txt. The regenerated llms-full.txt and llms.txt artifacts now also use the canonical prefix.

Why It Matters

The Link header on every BR API response advertises both endpoints. Before this change, every one of those advertisements was a broken promise — agents following the header for capability discovery hit a 503 or 404 on the first hop. This is the failure mode that makes a gateway look unfinished to anyone running a probe (which is exactly what surfaced this work).

The durable-fix shape matters more than the individual asset fixes: the generator + import pattern means a future asset (whatever the next discovery endpoint turns out to be) can be added as a one-line entry and bundled correctly by construction. The runtime fs-read path that caused this — and its findPackageRoot walking-up-tree path resolution — is gone for both assets and shouldn't return.

How It Works

scripts/generate-static-assets.ts reads the canonical source files, parses them (YAML for OpenAPI, JSON for the agent card), and writes a TypeScript module that exports them as Readonly> consts:

// src/api/static-assets.generated.ts (auto-generated, gitignored)
export const OPENAPI_SPEC: Readonly<Record<string, unknown>> = { … } as const;
export const AGENTS_JSON: Readonly<Record<string, unknown>> = { … } as const;

The handlers import these directly:

// src/api/routes/openapi.ts
import { OPENAPI_SPEC } from "../static-assets.generated.js";
export function handleOpenApiSpec(c: Context) {
  return c.json(OPENAPI_SPEC);
}

// src/api/server.ts (agents.json handler)
import { AGENTS_JSON } from "./static-assets.generated.js";
app.get("/.well-known/agents.json", (c) => {
  // …rate-limit gate…
  return c.json(AGENTS_JSON);
});

tsdown then bundles the consts into the production JS. No runtime fs I/O, no findPackageRoot walk, no async dynamic imports. Verified via grep -l "OPENAPI_SPEC\|AGENTS_JSON" dist/.js: both dist/server-.js bundles reference the inlined module.

src/api/static-assets.test.ts adds 6 assertions that:

  • Both consts are non-empty objects with their required shape (info/paths

for OpenAPI, name/auth for the agent card).

  • The handleOpenApiSpec handler returns a 200 with valid JSON shape via

in-process app.request("/openapi.json").

  • AGENTS_JSON serializes to JSON without throwing and contains the

openapi_url it advertises.

The Numbers

MetricBeforeAfter
GET /openapi.json (live API)503 ENOENT200 with full spec (post-deploy)
GET /.well-known/agents.json (live API)404 not_found200 with full agent card (post-deploy)
brk_ references in source files8 occurrences across 7 files0
Container fs-reads at first request2 dynamic imports + readFile0
Bundled asset size in distn/a166 KB (142 KB OpenAPI JSON + 24 KB agent card)

Lockstep Notes

No SDK or MCP tool changes — this is a build/bundle fix on existing public endpoints. site/public/routes.json is unchanged (git diff clean), so the pre-push hook doesn't require SDK updates. The Link header advertisements in src/api/server.ts:416,427 and src/gateway/health-http.ts:135 already referenced these paths; they now resolve.

Verification

  • npx vitest run src/api/static-assets.test.ts: 6/6 passed.
  • pnpm tsgo: exit 0.
  • oxfmt --check on edited files: "All matched files use the correct format."
  • oxlint on edited files: "0 warnings and 0 errors."
  • pnpm build: completes; grep "OPENAPI_SPEC" dist/server-*.js matches

both bundles; site/public/llms.txt regenerates with 0 brk_ remaining.

  • Live API verification deferred until merge + deploy.

What This Doesn't Fix (from the gap analysis)

  • P1.3 / P1.4 (auth-routing endpoints /routing/decisions, /ops/guardian-stats)

are mounted under /auth/ not /v1/, which is a discoverability problem worth addressing separately — pre-existing per route-manifest-coverage.test.ts exception list. Not in this PR.

  • P1.5 audit chain genesis HMAC bug (brokenAt: 0) — separate PR, requires

reading the signing path before scoping.

  • Group B/C/D items from the gap analysis remain open.