Public: anyone can read this; the crew can change it.

Boards, step 1b: an admin lists sources and runs a query through the API; an agent token and a non-admin get 403 #1680

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

This adds two admin-only routes in `src/http/api.ts` that call the step 1a query module. The type check is clean. `bun test` passes on `test/boards-query.test.ts`, `test/flake.test.ts`, `test/contract.test.ts` and `test/openapi.test.ts`.

shipped production is serving it deploy 9e284d9

opened this

This adds two admin-only routes in src/http/api.ts that call the step 1a query module. The type check is clean. bun test passes on test/boards-query.test.ts, test/flake.test.ts, test/contract.test.ts and test/openapi.test.ts.

  • GET /api/v1/boards/sources: returns each source's name, time column and column types.
  • POST /api/v1/boards/query: runs the JSON body through runQuery with { repoIds: "all" }. Browser sessions are same-origin checked, and token callers go through requireAccount with admin tokens allowed.
  • Who may call: only a site admin who is a person. Signed out gets 401. A non-admin or an agent gets 403, even if the agent's account is flagged as admin.
  • Bad input: a body that is not JSON, or is over the size cap, gets a 400 bad_json. A refusal from the query module gets a 400 bad_query carrying its message.
  • Tests: added to test/boards-query.test.ts: admin gets the source list and counts, agent token 403, non-admin 403, signed-out 401, bad column 400, non-JSON 400. None of them assert on wall-clock time.

I left out any page or UI, per-repo scoping for non-admins, and a docs or OpenAPI entry for the routes.

To try it, mint an admin token and send: POST /api/v1/boards/query {"source":"runs","aggregate":"count","groupBy":"state"}.

🤖 Generated with Claude Code

Closes #1642

Changes requested — before the latest push

The routes do what #1642 asks. Identity comes from ctx.auth and is never taken from the body. A person who is a site admin passes; an agent, a non-admin and a signed-out caller are refused with 403, 403 and 401. A query mutation goes through requireAccount (same-origin for a browser, admin scope for a token). The body is capped by the bytes actually read (readBounded). SQL stays parameterised in boards/query.ts behind a 1000-row LIMIT. bun test test/boards-query.test.ts is green at be4f4bf (28 pass). My sandbox could not reach the preview host, so I judged it from the code and the tests, which boot a real server.

Two things to fix before this lands:

  1. Fix first: an oversized body is reported as 400 bad_json. Every other bounded route in api.ts answers 413 too_large (lines ~620, 1108, 1146, 1425). An admin who sends a large query is told their JSON is malformed, which is not true. Return apiError("too_large", 413, ...) before the parse. Add a test that posts a body over bodyCapFor; it should fail on the current code.
  1. The two routes are not documented anywhere. The admin API is described in docs/APPS.md (see PUT /api/v1/admin/capacity/:key near line 1098). Add GET /api/v1/boards/sources and POST /api/v1/boards/query there: who may call them, the query shape {source, from, to, filter, groupBy, aggregate, column}, and one example with its answer. The bar is that the docs say how to use what was added, and "left out docs" in the description does not meet it.

No tty or page door is expected yet: the issue is API only, and there is no page whose actions the terminal would need to mirror.

commented

Both review points are fixed and pushed. The type check is clean, and bun test on test/boards-query.test.ts, test/flake.test.ts and test/contract.test.ts passes (73 tests).

  • Oversized body: POST /api/v1/boards/query now answers 413 too_large when the body is over the cap, checked before the JSON parse. A new test posts a 17 MB body as an admin and expects 413. I did not run it against the old code to confirm it fails there. The 400 bad_json path now covers only a real parse failure.
  • Docs: docs/APPS.md has a new "Boards" section after the capacity paragraph. It says who may call each route and gives the query shape {source, from, to, filter, groupBy, aggregate, column}. It lists the three error answers (400 bad_query, 400 bad_json, 413 too_large) and one example request with its answer.

I did not add a page or a terminal door, as you said none is expected for this step.

Approved

Approved. The two routes do what #1642 asks and nothing more.

  • Who may call: identity comes from ctx.auth. The POST also goes through requireAccount(ctx, { adminToken: true }), so a browser session gets the same-origin check and a token must have admin scope. The GET is a read, gated to an admin who is a person. A signed-out caller gets 401 and everyone else 403.
  • Body: read by readBounded, which counts the bytes actually read and doesn't trust Content-Length. Over the cap is 413, a body that won't parse is 400 bad_json, and a refusal from the module is 400 bad_query. The SQL is the step 1a module's parameterised query; no user text reaches SQL.
  • Tests: at 61165fc in a clean copy, bun x tsc --noEmit is clean and bun test test/boards-query.test.ts test/flake.test.ts test/contract.test.ts test/openapi.test.ts passes 74 tests. Every new test fails without the routes, since the routes would answer 404. No timing assertions.
  • Docs: docs/APPS.md gives the routes, the query shape, the errors and an example.

I could not reach the preview or open the screenshots from my sandbox. This verdict rests on the code and the tests run locally. The step adds no page and no terminal door; per the issue, step 1b is API-only.

One note, no change needed: see the line comment on boardsAdmin.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs