Routes
The dashboard’s server only relays. The dashboard’s logic lives in the browser (public/); all the
server does is provide a way to the ledger and the daemon, and serve the dashboard’s files. The
moment logic lived on the server, there would be a copy of what the ledger knows, so it does
not.
| Route | Answered by |
|---|---|
GET /healthz |
the server itself (liveness, and the settings the dashboard needs) |
POST /api/crawls |
the ledger (the one and only route that writes) |
/api/* (the rest) |
passed through to the ledger (GET only) |
/wacz/:id/… |
signed by the ledger, then answered by the daemon |
| anything else | the files in public/ |
The routing is in src/server.mjs; parsing and rewriting the /wacz/ routes is in
src/wacz.mjs.
Every route refuses with a 403 unless the request’s Host is 127.0.0.1:<port> or
localhost:<port>. That stops a page from another site from reading the dashboard as if it were
its own origin, by pointing its domain name at 127.0.0.1 (DNS rebinding).
/healthz
Section titled “/healthz”Returns liveness and the settings the dashboard needs.
{ "ok": true, "subject": "alice", "organizations": ["acme"], "replayOrigin": "http://127.0.0.1:8899", "windmillOrigin": "http://127.0.0.1:8000", "windmillWorkspace": "crawler", "writeLimit": 1048576}The dashboard reads it when it opens, to learn who it is looking as and where the deep links go.
writeLimit is the largest body, in bytes, the route that writes accepts; the new crawl view stops
before sending a bigger one.
It does not live under /api/: that is where the ledger is passed through, and mixing this
server’s answers in would make it unclear who answered.
/api/* — passed through to the ledger
Section titled “/api/* — passed through to the ledger”The ledger does not return CORS (Cross-Origin Resource Sharing, which lets pages from another
origin call it) headers (OPTIONS /api/archives is a 404). Since the browser
cannot call it directly, the server relays it to make it same-origin.
- The path and query go through unchanged, as a GET. Any other method is a 405 (the only route
that writes is
POST /api/crawlsbelow). - The access token is attached as
Authorization: Bearer. If the server does not have one yet, it first gets one from the development issuer. - On a 401 it gets a new token and retries once. It does not track the token’s lifetime: restarting the issuer changes its keys, so a clock that says “still valid” cannot be trusted. A 401 says both “expired” and “the keys changed” in the same way.
- The ledger’s status,
content-typeand body are returned as they are.
Which ledger routes are read is decided by the dashboard (public/app.js); the server does not
choose. It keeps no list of allowed routes. So the ledger’s GET /api/crawl-rules/check (the new
crawl view checking its rules as you write them) and GET /api/crawls/:id/decisions (what an
opened crawl’s rules decided) need nothing here: they are reads, and pass through like the lists.
POST /api/crawls — the route that starts a crawl
Section titled “POST /api/crawls — the route that starts a crawl”This is the one route of the dashboard’s server that writes. The form on the crawls view
(Screens) uses it. The server does not check the contents: it hands the body that
passed the checks below to the ledger’s POST /api/crawls as it is, and returns the answer as
it is. The ledger is the one that checks and decides the seed URLs, the policy (depth, page
limit, which scripts run) and the per-crawl variables (variables: the spelling and number of the
names, the length of the values, and whether a ticked script declared each name). Whether a
declared secret exists in Windmill is also the ledger’s check; the dashboard never touches a
secret’s value (it lives in a Windmill variable, and is neither in the form nor in the body).
- Only JSON from the dashboard’s own page gets through. If the
Originis not the dashboard it is a 403, if thecontent-typeis not JSON it is a 415, and a body over 1 MiB (seed URLs and any script written in the form, together; the same limit as the ledger’sPOST /api/crawls) is a 413. This stops a page from another site from making your browser send the POST (CSRF). It does not rely on Chrome’s Private Network Access. - The access token is attached, and a 401 retried, the same way as for
/api/*above.
| The ledger’s answer | When |
|---|---|
202 {"crawlId": …} |
accepted (it does not wait for the crawl to finish) |
400 {"error": …} |
a seed URL that cannot be read, no registered scripts (with a hint), an unknown script id or the same id twice (with ids), a required variable missing or a variable no script declared (with names), a declared secret that is missing from Windmill or is not a secret there (with secrets: name and path), and so on |
| 404 | the identity may not start crawls ({"error":"not found"}), or the ledger has nowhere to send crawls configured and does not serve the route at all |
409 {"error": …, "crawlId": …} |
a crawl is already running (one at a time, overall); crawlId is the one in the way |
503 {"error": "windmill unreachable"} |
the ledger could not ask Windmill whether a declared secret exists (the flow would not run either, so it does not start the crawl) |
The checks are writeRefusal in src/lib.mjs, which the tests call directly.
/wacz/:id/… — routes into a WACZ
Section titled “/wacz/:id/… — routes into a WACZ”These routes validate a WACZ (the archive format that packs captured pages into one ZIP) and read inside it. The WARC files in it (which record the captured requests and responses in order) and its indexes are read one window at a time.
| The dashboard calls | Sent to the daemon | Returns |
|---|---|---|
POST /wacz/:id/validate |
POST /validate |
the validation report |
GET /wacz/:id/lines?path=&from=&count= |
POST /lines |
a window of lines |
GET /wacz/:id/line?path=&n= |
POST /line |
one whole line, and its fields |
GET /wacz/:id/records?path=&from=&count= |
POST /records |
the records, walking the WARC from the start |
GET /wacz/:id/record?path=&offset=&length= |
POST /record |
one record (WARC headers, HTTP, body) |
GET /wacz/:id/record/body?path=&offset=&length= |
POST /record/body |
the image itself; raster only, with nosniff and sandbox, 415 otherwise |
This table is checked against OPS in src/wacz.mjs (npm run site:check). To add a route,
change OPS, paramsFor and this table.
- Every route takes the same two steps. Get a signature from the ledger’s
POST /api/archives/:id/url, then hand that URL to the daemon (why in that order is in Inspecting a WACZ). - The dashboard calls GET; the daemon gets POST.
<img src>is a GET too, so the image itself can be fetched with a GET. Only validation is a POST from the dashboard as well (it runs only when you press it). - The body sent to the daemon is
{ "source": { "kind": "uri", "uri": "<signed URL>" }, …arguments }. Validation’s only argument islocale.
Headers that pass
Section titled “Headers that pass”Which headers of the upstream answer (the ledger’s or the daemon’s) pass through is an
allowlist, PASS_HEADERS in src/server.mjs. The daemon’s date and connection are not mixed
into the dashboard’s response. The image itself passes as bytes, together with the marks the daemon
put on it (nosniff, sandbox, no-store).
What failures look like
Section titled “What failures look like”A failure says what is missing. “You may not see it” and “it is not running” are never mixed.
| Status | When | Body |
|---|---|---|
| 400 | an argument of a /wacz/ route is missing or not a number; the ledger is not asked |
{"error":"from is required"} |
| 403 | the Host is not the dashboard’s (any route); on the route that writes, also when the Origin is not the dashboard |
{"error":"dashboard の Host ではない"} and so on |
| 405 | wrong method: on a /wacz/ route (validation is POST, the rest GET), or on /api/* (GET only, except the route that writes) |
POST だけ, GET だけ and so on, with an Allow header |
| 413 | the body sent to the route that writes is over 1 MiB | {"error":"本文が 1048576 バイトを超えた"} |
| 415 | the content-type sent to the route that writes is not JSON |
{"error":"JSON だけを受ける"} |
| as the ledger said | the ledger refused (such as a 404 for an archive you may not view) | the ledger’s body |
| 502 | the issuer, the ledger or the daemon cannot be reached, or the issuer refused the identity | {"error":"… に届かない (…)。…"}, with one sentence on how to fix it |
| 404 | a file that is not in public/, or an extension that is not served |
not found |
If the upstream breaks off while the answer is being streamed, the headers have already been written, so the server closes the connection.