Public: anyone can read this; the crew can change it.
GET /api/v1/openapi.json answers the OpenAPI document, the same as /contract/openapi.json #4159
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4149-get-api-v1-openapi-json-answers-the-open → main 1 commit fork
3 files
changed +22
−0
src/http/api.ts +13
| @@ -5796,6 +5796,19 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 5796 | 5796 | return json(readFleetGraph(ctx.app, ctx.auth.user, asked.value)); |
| 5797 | 5797 | }); |
| 5798 | 5798 | |
| 5799 | // The address an agent tries first. | |
| 5800 | router.add( | |
| 5801 | "GET", | |
| 5802 | "/api/v1/openapi.json", | |
| 5803 | () => | |
| 5804 | new Response(null, { | |
| 5805 | status: 308, | |
| 5806 | headers: { | |
| 5807 | location: "/contract/openapi.json", | |
| 5808 | "cache-control": "public, max-age=86400", | |
| 5809 | }, | |
| 5810 | }), | |
| 5811 | ); | |
| 5799 | 5812 | // What the caller can do on a forge page, as who they are (#604): the |
| 5800 | 5813 | // manifest every page links to. It differs by caller, so it is never cached. |
| 5801 | 5814 | router.add("GET", "/api/v1/here", (ctx) => { |
src/web/standards.ts +1
| @@ -283,6 +283,7 @@ export function llmsTxt(ctx: Ctx): Response { | ||
| 283 | 283 | "", |
| 284 | 284 | `- MCP: ${o}/mcp (OAuth discovery: ${o}/.well-known/oauth-protected-resource)`, |
| 285 | 285 | `- API: ${o}/api/v1, Authorization: Bearer <token>`, |
| 286 | `- OpenAPI: GET ${o}/api/v1/openapi.json (redirects to ${o}/contract/openapi.json)`, | |
| 286 | 287 | "", |
| 287 | 288 | "## Public repositories", |
| 288 | 289 | "", |
test/mcp.test.ts +8
| @@ -1140,6 +1140,14 @@ describe("the forge over MCP", () => { | ||
| 1140 | 1140 | expect(doc.openapi).toBe("3.1.0"); |
| 1141 | 1141 | expect(doc.paths["/mcp"].post).toBeDefined(); |
| 1142 | 1142 | }); |
| 1143 | test("GET /api/v1/openapi.json redirects to the document", async () => { | |
| 1144 | const res = await fetch(`${base}/api/v1/openapi.json`, { redirect: "manual" }); | |
| 1145 | expect(res.status).toBe(308); | |
| 1146 | expect(res.headers.get("location")).toBe("/contract/openapi.json"); | |
| 1147 | expect(res.headers.get("cache-control")).toContain("public"); | |
| 1148 | const target = await fetch(new URL(res.headers.get("location")!, base)); | |
| 1149 | expect(target.status).toBe(200); | |
| 1150 | }); | |
| 1143 | 1151 | test("feed_chips reads and switches the chips the feed answers through", async () => { |
| 1144 | 1152 | app.bus.append({ kind: "app.serving", repoId: game.id, actorId: alice.id, payload: {} }); |
| 1145 | 1153 | const kinds = async () => { |
new above
offline