tickets
All repositories: gitoria
3.6 KB
// api.hl — the JSON API's plumbing: query strings, bodies, answers. Statics only;// the routes themselves are in project.hl (a function route is `(route, req) => …`).import { Response } from 'hl:http1'import { jsonErrorAt } from './jsoncheck.hl'static jsonHeaders = { 'Content-Type' = 'application/json; charset=utf-8' }static reply = (status, value) => {return new Response(JSON.stringify(value), { status = status headers = jsonHeaders })}static fail = (status, message) => { return reply(status, { error = message }) }// `?a=1&b=x` of a request path → { a = '1', b = 'x' } ('+' is a space; hl:http1// has already percent-decoded the path)static queryOf = (path) => {let out = {}let q = path.indexOf('?')if (q < 0) { return out }for (pair of path.slice(q + 1).split('&')) {if (pair != '') {let eq = pair.indexOf('=')if (eq < 0) { out[pair] = '' } else { out[pair.slice(0, eq)] = pair.slice(eq + 1).replaceAll('+', ' ') }}}return out}// the request body as JSON, or null when it is notstatic bodyOf = (req) => {if (req.body == null || req.body == '') { return null }return JSON.parse(req.body)}// THE API IS STRICT (ticket #10): a malformed report must not be filed half-read. A body// is a JSON object whose keys are all in `required` or `optional`, every value is a// String, and every required one is non-empty after trimming. Answers null (fine) or// { error, field } — the first offence, naming the field.// (a list has no concat() in this build — NotCallable, hybriel #6's family)static allowedOf = (required, optional) => {let all = []for (k of required) { all.push(k) }for (k of optional) { all.push(k) }return all.join(', ')}static bodyError = (b, required, optional) => {if (b == null || hlTypeName(b) != 'Hybrid' || b.length != null) {return { error = 'the body must be a JSON object' field = '' }}for (k of b.keys()) {// ticket #7: the author is the token's user — a body that still names one is refused// (not silently ignored), so a client learns that its field does nothingif (k == 'author') {return { error = "no field 'author' any more: the author is the user of your API token" field = 'author' }}if (!required.includes(k) && !optional.includes(k)) {return { error = "unknown field '" + k + "' (allowed: " + allowedOf(required, optional) + ')' field = k }}if (hlTypeName(b[k]) != 'String') {return { error = "field '" + k + "' must be a string, not " + hlTypeName(b[k]) field = k }}}for (k of required) {if (b[k] == null) { return { error = "field '" + k + "' is required" field = k } }if (b[k].trim() == '') { return { error = "field '" + k + "' must not be empty" field = k } }}return null}static refuse = (e) => { return reply(400, e) }// ticket #7: an API write without a valid token (missing, malformed, unknown, revoked) → 401static unauthorized = () => {return new Response(JSON.stringify({ error = 'an API write needs Authorization: Bearer <token> — log in with ident and create a token on /you' }), { status = 401 headers = { 'Content-Type' = 'application/json; charset=utf-8' 'WWW-Authenticate' = 'Bearer' } })}// a POST body, checked: { body } or { bad } (bad = the 400 answer's value).// Invalid JSON is refused by jsoncheck.hl BEFORE JSON.parse ever sees it (ticket #15,// workaround for hybriel #12: an uncaught JSON.parse would answer 500 with source paths).static readBody = (req, required, optional) => {if (req.body != null && req.body != '') {let at = jsonErrorAt(req.body)if (at >= 0) { return { bad = { error = 'invalid JSON at character ' + at } } }}let b = bodyOf(req)return { body = b bad = bodyError(b, required, optional) }}
Branches
- mainmain branch
Latest commits
- bce182e3tickets: Hybriel master 7eea0d32 (#126 memory, #48 lambda copies its argument); migrate.hl lambdas take &logmre
- 4137be0fantcolony#40: mission references point to the moved missionsmre
- 9bfba36aantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- c7bd2645tickets: Hybriel master 73267707 (#122 fixed); compactNow workaround removed (#110 covered)mre
- 2ab91ee9tickets: gate checks rows appear once (session sync); re-vendor to ff51cf46 stopped on hybriel#122, stays 837fe120mre
- e01c2b1dtickets#24: installable app (manifest, service worker, offline list), own icon; gate waits for the hello's pongmre
- 752fbb7fdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- 38bdd5e4deploy.sh: never send .git or .gitignore to Byrodinmre
- f12fa1bcState of 2026-09-27, before the move to gitoriamre