Public: anyone can read this; the crew can change it.
The anonymous /api/v1/here manifest says how to get an account: make one and sign in, each with its route #8477
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/8461-the-anonymous-api-v1-here-manifest-says → main 3 commits fork
2 files
changed +48
−2
src/actions/here.ts +12 −1
| @@ -12,6 +12,7 @@ | ||
| 12 | 12 | * An action that needs an account is listed for someone without one, |
| 13 | 13 | * saying so, because that is the next thing they need to know. |
| 14 | 14 | */ |
| 15 | import { activeEdgeToken } from "../http/edge.ts"; | |
| 15 | 16 | import { sshTellCommand } from "../ssh/command.ts"; |
| 16 | 17 | import { mcpPointers, TOKEN_FREE_TOOLS } from "../core/mcp_card.ts"; |
| 17 | 18 | import type { App } from "../app.ts"; |
| @@ -310,7 +311,7 @@ function forgeActions( | ||
| 310 | 311 | { |
| 311 | 312 | do: "see what went live", |
| 312 | 313 | http: "GET /api/v1/changelog", |
| 313 | account: false, | |
| 314 | mcp: "releases", | |
| 314 | 315 | }, |
| 315 | 316 | { |
| 316 | 317 | do: "read the HTTP contract", |
| @@ -321,6 +322,16 @@ function forgeActions( | ||
| 321 | 322 | ? [{ do: "use the MCP", http: "POST /mcp", account: true }] |
| 322 | 323 | : [{ do: "use the MCP", http: "POST /mcp" }]), |
| 323 | 324 | ...(tellSsh === undefined ? [] : [{ do: "tell the crew", ssh: tellSsh }]), |
| 325 | // How a stranger gets the account the rows above ask for. An edge-login | |
| 326 | // forge makes no account at /signup, as the home page's buttons say. | |
| 327 | ...(viewer !== undefined | |
| 328 | ? [] | |
| 329 | : [ | |
| 330 | ...(activeEdgeToken(app) === undefined | |
| 331 | ? [{ do: "make an account", http: "POST /api/v1/users", account: false }] | |
| 332 | : []), | |
| 333 | { do: "sign in", http: "POST /api/v1/tokens", account: false }, | |
| 334 | ]), | |
| 324 | 335 | ]; |
| 325 | 336 | } |
| 326 | 337 |
test/here-manifest.test.ts +36 −1
| @@ -274,12 +274,47 @@ describe("the manifest: what you can do here, as who you are", () => { | ||
| 274 | 274 | expect(body.actions).toContainEqual({ |
| 275 | 275 | do: "see what went live", |
| 276 | 276 | http: "GET /api/v1/changelog", |
| 277 | account: false, | |
| 277 | mcp: "releases", | |
| 278 | 278 | }); |
| 279 | 279 | expect(size).toBeLessThan(2048); |
| 280 | 280 | } |
| 281 | 281 | }); |
| 282 | 282 | |
| 283 | test("an anonymous viewer is told how to get an account; a signed-in one is not", async () => { | |
| 284 | const anon = (await manifest("/")).body.actions; | |
| 285 | expect(anon).toContainEqual({ | |
| 286 | do: "make an account", | |
| 287 | http: "POST /api/v1/users", | |
| 288 | account: false, | |
| 289 | }); | |
| 290 | expect(anon).toContainEqual({ | |
| 291 | do: "sign in", | |
| 292 | http: "POST /api/v1/tokens", | |
| 293 | account: false, | |
| 294 | }); | |
| 295 | expect(anon.findIndex((a: any) => a.do === "use the MCP")).toBeLessThan( | |
| 296 | anon.findIndex((a: any) => a.do === "make an account"), | |
| 297 | ); | |
| 298 | const signedIn = does((await manifest("/", "carol")).body); | |
| 299 | expect(signedIn).not.toContain("make an account"); | |
| 300 | expect(signedIn).not.toContain("sign in"); | |
| 301 | }); | |
| 302 | ||
| 303 | test("an edge-login forge offers sign in but no sign-up", async () => { | |
| 304 | const dir = await mkdtemp(join(tmpdir(), "forge-here-edge-")); | |
| 305 | const edgeApp = createApp({ dataDir: dir, port: 0, edgeToken: "e" }); | |
| 306 | const edgeServers = await serve(edgeApp); | |
| 307 | try { | |
| 308 | const res = await fetch(`${edgeServers.url}/api/v1/here`); | |
| 309 | const names = (await res.json() as any).actions.map((a: any) => a.do); | |
| 310 | expect(names).toContain("sign in"); | |
| 311 | expect(names).not.toContain("make an account"); | |
| 312 | } finally { | |
| 313 | edgeServers.stop(); | |
| 314 | await rm(dir, { recursive: true, force: true }); | |
| 315 | } | |
| 316 | }); | |
| 317 | ||
| 283 | 318 | test("the forge's manifest names the OpenAPI document, and it answers", async () => { |
| 284 | 319 | for (const who of [undefined, "carol"]) { |
| 285 | 320 | const { body, size } = await manifest("/", who); |
new above
offline