gitoriaLog in with ident

tickets

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit9bfba36a9bfba36aantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre9bfba36a/api.hl

3.6 KB

  1. // api.hl — the JSON API's plumbing: query strings, bodies, answers. Statics only;
  2. // the routes themselves are in project.hl (a function route is `(route, req) => …`).
  3. import { Response } from 'hl:http1'
  4. import { jsonErrorAt } from './jsoncheck.hl'
  5. static jsonHeaders = { 'Content-Type' = 'application/json; charset=utf-8' }
  6. static reply = (status, value) => {
  7. return new Response(JSON.stringify(value), { status = status headers = jsonHeaders })
  8. }
  9. static fail = (status, message) => { return reply(status, { error = message }) }
  10. // `?a=1&b=x` of a request path → { a = '1', b = 'x' } ('+' is a space; hl:http1
  11. // has already percent-decoded the path)
  12. static queryOf = (path) => {
  13. let out = {}
  14. let q = path.indexOf('?')
  15. if (q < 0) { return out }
  16. for (pair of path.slice(q + 1).split('&')) {
  17. if (pair != '') {
  18. let eq = pair.indexOf('=')
  19. if (eq < 0) { out[pair] = '' } else { out[pair.slice(0, eq)] = pair.slice(eq + 1).replaceAll('+', ' ') }
  20. }
  21. }
  22. return out
  23. }
  24. // the request body as JSON, or null when it is not
  25. static bodyOf = (req) => {
  26. if (req.body == null || req.body == '') { return null }
  27. return JSON.parse(req.body)
  28. }
  29. // THE API IS STRICT (ticket #10): a malformed report must not be filed half-read. A body
  30. // is a JSON object whose keys are all in `required` or `optional`, every value is a
  31. // String, and every required one is non-empty after trimming. Answers null (fine) or
  32. // { error, field } — the first offence, naming the field.
  33. // (a list has no concat() in this build — NotCallable, hybriel #6's family)
  34. static allowedOf = (required, optional) => {
  35. let all = []
  36. for (k of required) { all.push(k) }
  37. for (k of optional) { all.push(k) }
  38. return all.join(', ')
  39. }
  40. static bodyError = (b, required, optional) => {
  41. if (b == null || hlTypeName(b) != 'Hybrid' || b.length != null) {
  42. return { error = 'the body must be a JSON object' field = '' }
  43. }
  44. for (k of b.keys()) {
  45. // ticket #7: the author is the token's user — a body that still names one is refused
  46. // (not silently ignored), so a client learns that its field does nothing
  47. if (k == 'author') {
  48. return { error = "no field 'author' any more: the author is the user of your API token" field = 'author' }
  49. }
  50. if (!required.includes(k) && !optional.includes(k)) {
  51. return { error = "unknown field '" + k + "' (allowed: " + allowedOf(required, optional) + ')' field = k }
  52. }
  53. if (hlTypeName(b[k]) != 'String') {
  54. return { error = "field '" + k + "' must be a string, not " + hlTypeName(b[k]) field = k }
  55. }
  56. }
  57. for (k of required) {
  58. if (b[k] == null) { return { error = "field '" + k + "' is required" field = k } }
  59. if (b[k].trim() == '') { return { error = "field '" + k + "' must not be empty" field = k } }
  60. }
  61. return null
  62. }
  63. static refuse = (e) => { return reply(400, e) }
  64. // ticket #7: an API write without a valid token (missing, malformed, unknown, revoked) → 401
  65. static unauthorized = () => {
  66. 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' } })
  67. }
  68. // a POST body, checked: { body } or { bad } (bad = the 400 answer's value).
  69. // Invalid JSON is refused by jsoncheck.hl BEFORE JSON.parse ever sees it (ticket #15,
  70. // workaround for hybriel #12: an uncaught JSON.parse would answer 500 with source paths).
  71. static readBody = (req, required, optional) => {
  72. if (req.body != null && req.body != '') {
  73. let at = jsonErrorAt(req.body)
  74. if (at >= 0) { return { bad = { error = 'invalid JSON at character ' + at } } }
  75. }
  76. let b = bodyOf(req)
  77. return { body = b bad = bodyError(b, required, optional) }
  78. }

Branches

Latest commits

  • 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