Skip to content

Add API.md, the HTTP and WebSocket API reference - #83

Open
AsLY4 wants to merge 1 commit into
jsnjack:masterfrom
AsLY4:api-documentation
Open

AsLY4 wants to merge 1 commit into
jsnjack:masterfrom
AsLY4:api-documentation

Conversation

@AsLY4

@AsLY4 AsLY4 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

The README links to API.md at the repository root, but the file was never added — the link is currently dead. This is that reference, written against master as of 6c6223d.

It covers the REST API under /api and /auth, the WebSocket API at /ws, the storage endpoint under /storage/build/, the unauthenticated static and /docs routes, and a conventions section (path matching, null vs [], build vs task statuses, task kinds and ordering, units).

The emphasis is on what a client actually observes and on the behaviour that is easy to get wrong, for example:

  • GET /api/build/{id} answers 500 — not 404 — for an id that was never used, because the build plan is read from disk before the history is queried; the 404 is reserved for a build whose files exist but whose history entry is gone.
  • POST /api/build/{id}/flush answers 404 for a build that is merely queued, and both abort and flush are best effort: the signal is dropped when no task command is executing, and the response is still 200.
  • POST /api/settings applies its fields one at a time and returns on the first failure, so a 500 does not mean nothing changed — posting only password= stores the new password and then fails on the missing concurrentBuilds.
  • params, artifacts and build_artifacts are null rather than [], and so is the whole body of /api/feed and /api/jobs/ when there is nothing to return.
  • WebSocket subscriptions are anchored: only a subscription ending in : matches by prefix.
  • Several WebSocket messages may arrive in one frame, separated by \n.
  • The failed-authentication rate limiter (429 with Retry-After and X-RateLimit-*) applies to POST /auth/login and to Basic auth, and blocks even a request carrying the correct password.

Two observations about the code, not addressed here

While documenting units and the generated spec I ran into two things that look like small bugs. I have left the code alone and documented the current behaviour; happy to open separate PRs if you want them fixed:

  1. eta is in nanoseconds, not seconds. SetBuildStatus records RecordBuildDuration(b.Job.Name, int(b.Duration)) — a time.Duration — and GetJobETA/calcAvg just average those integers. The // seconds comment on Build.ETA is stale, and the frontend confirms the real unit by dividing by 10**9 in SimpleDuration.vue.
  2. POST /api/build/{id}/start is missing from the generated Swagger spec. HandleStartBuild carries a copy-pasted @Router /build/{id}/abort [post] annotation, so the emitted spec has no /build/{id}/start path. A few other annotations also spell paths with a trailing slash the router rejects (/settings/, /job/{name}/).

How it was checked

Written by reading the handlers, middleware, queue, build and WebSocket sources, cross-checked against the existing _test.go files and the frontend's own use of the API. A few serialisation details that are hard to be sure of by reading — nil slice marshalling to null, the zero-time rendering, the saturated duration of a build aborted while queued, the exact filter match string — were confirmed by running equivalent code in a golang:1.25 container. No source file is modified by this PR.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Several authentication, form-parsing, and CSP behaviors are documented inaccurately.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds the missing client-focused API reference for wakeci.

Changes:

  • Documents REST, authentication, storage, and static routes.
  • Defines WebSocket protocol behavior and API conventions.
  • Records current implementation quirks and response semantics.
File summaries
File Description
API.md Adds the complete HTTP and WebSocket API reference.
Review details

Suppressed comments (2)

API.md:219

  • A blocked client does not necessarily receive 429 here. With a valid session cookie, AuthMi bypasses the password limiter and this probe still returns 200; 429 occurs when the probe takes the Basic-auth path.
Lightweight authentication probe. Returns `200 OK` with an empty body when the
request is authenticated, `403 Forbidden` with the body `Forbidden` when it is
not, or `429 Too Many Requests` with the body `Too many authentication attempts`
when the client is blocked by the rate limiter.

API.md:746

  • This CSP does not prevent every credentialed callback into the API. Because default-src 'self' still permits same-origin subresources and navigation is unrestricted, an artifact can issue authenticated GET requests (for example through an image) or navigate to /auth/logout; only connect APIs and form submission are blocked. Avoid presenting the policy as a complete request boundary.
**Response headers.** `/storage/build/*` gets a Content-Security-Policy of its
own — loose enough to preview HTML artifacts, locked down so a previewed
artifact can never use the caller's credentials to call back into the API:
  • Files reviewed: 1/1 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread API.md Outdated
Comment thread API.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Several documented behaviors are inaccurate, including a security-critical claim about artifact isolation.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 4
  • Review effort level: Balanced

Comment thread API.md Outdated
Comment thread API.md Outdated
Comment thread API.md Outdated
Comment thread API.md Outdated
The README already links to `API.md` at the repository root, but the file was
never added. This is that reference, written against the current code.

*   REST API under `/api` and the `/auth` helpers: every endpoint's inputs,
    status codes and response bodies, including the ones that are easy to get
    wrong — `GET /api/build/{id}` answers `500` (not `404`) for an id that was
    never used, `POST /api/build/{id}/flush` answers `404` for a build that is
    merely queued, and `POST /api/settings` applies its fields one at a time and
    returns on the first *reported* failure, so a `500` does not mean nothing
    changed — while `concurrentBuilds` is the one field whose database error is
    never reported at all, so a `200` is not proof that it was saved.
*   Authentication: HTTP Basic and the `session` cookie with its exact
    attributes and the conditions under which `Secure` is set, plus the
    failed-authentication rate limiter (`429`, `Retry-After`, `X-RateLimit-*`).
    The limiter is consulted only on the Basic-auth branch, so a blocked client
    holding a valid session cookie is still served.
*   WebSocket API at `/ws`: the same-origin check on the upgrade, the message
    envelope — several messages may share one frame, separated by `\n` — the
    anchored subscription matching (only a subscription ending in `:` matches by
    prefix), the keep-alive, size and buffer limits, and which malformed
    messages end the connection versus which are ignored. A `data` of `null`,
    `{}` or `{"to": null}` is not an error: it subscribes to nothing, silently.
*   Storage under `/storage/build/`: the actual URL shapes for task logs and
    artifacts, directory listings, and the dedicated Content-Security-Policy.
    That policy binds the artifact's own document, not the origin — everything
    wakeci serves is one origin, `frame-ancestors 'self'` is set everywhere and
    there is no CSRF token — so the document says plainly that it is not a
    security boundary and that an artifact must be treated as untrusted code
    running with the viewer's session.
*   Conventions: exact path matching, `null` vs `[]` on empty collections, build
    vs task statuses, task kinds and their execution order, the units of
    `duration` and `eta`, when a final status can still carry `duration: 0`, and
    the one endpoint that ignores a multipart body (`POST /api/job/{name}/run`
    parses the request itself and reads `r.Form`, where every other input goes
    through `FormValue`).

Two notes on units and generated docs, from reading the code rather than the
comments: `eta` is expressed in nanoseconds like `duration` (the `// seconds`
comment on the struct field is stale, and the frontend divides by 10^9), and
`POST /api/build/{id}/start` is absent from the generated Swagger spec because
its handler carries the `/build/{id}/abort` route annotation.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread API.md
Comment on lines +42 to +49
2. **Session cookie** — obtain a `session` cookie from `POST /auth/login` and
send it back on subsequent requests. This is what the web frontend uses. The
cookie is `HttpOnly`, `SameSite=Strict`, `Path=/` and valid for 120 hours
(5 days). It additionally carries `Secure` whenever the request looks like
HTTPS — the server is configured with `port: 443`, the connection is TLS, or
the first value of `X-Forwarded-Proto` is `https` (case-insensitive). A
TLS-terminating reverse proxy must forward that header, otherwise the cookie
is issued without `Secure`.
Comment thread API.md
Comment on lines +139 to +145
- A final status is not a promise of a real duration. A build aborted through
`POST /api/build/{id}/abort` while it was still queued never started and ends
with the saturated `duration: 9223372036854775807` beside the zero
`startedAt`; a build left `pending` or `running` by a server restart keeps
`duration: 0` permanently once `GET /api/feed` rewrites its status to
`aborted`. Detect "never ran" from the zero `startedAt`, which both cases
share — not from the saturated value.
Comment thread API.md
Comment on lines +684 to +687
- `password` — `string`, optional. When non-empty, sets a new password (stored
bcrypt-hashed). Any non-empty value is accepted — there is no length or
strength check — and an empty value is ignored, so the password cannot be
cleared this way.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants