Skip to content

Commit 5dd314b

Browse files
docs: complete API v2.0.0 public read spec and sync README
HKW pass after v2.6.2 — full API.md for Phase 1 routes, limits, and examples; README public chart curl; rebuild dist; changelog [Unreleased]. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a237280 commit 5dd314b

5 files changed

Lines changed: 231 additions & 54 deletions

File tree

API.md

Lines changed: 200 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,63 @@ https://<host>:<port>/api
2020

2121
The server listens on `API_PORT` (default `3000`).
2222

23+
Production chart API hosting is maintainer-operated (not served from GitHub Pages). The static app lives at `https://earlution.github.io/6-axis-compass/`; run `npm run api` locally or deploy `api/server.js` behind HTTPS.
24+
25+
### API vs app versioning
26+
27+
| Semver | What it tracks | Current |
28+
|--------|----------------|---------|
29+
| **API** (`apiVersion` in `/api/health`) | REST contract (auth zones, routes, limits) | **2.0.0** |
30+
| **App** (`version` in `/api/health`) | Compass package / Pages bundle | See `package.json` (e.g. **2.6.2**) |
31+
32+
---
33+
34+
## Trust zones (v2.0.0)
35+
36+
| Zone | Auth | Routes |
37+
|------|------|--------|
38+
| **Public read** | None (default) | `GET /api/health`, `GET /api/actors`, `GET /api/actors/:slug`, `POST /api/chart`, `GET /api/axes`, `GET /api/openapi.json` |
39+
| **Private write** | `ADMIN_SECRET` (Phase 2) | `POST /api/admin/*` — not implemented yet |
40+
41+
Specification: [`docs/feature-request-public-private-api-v0.1.0.md`](docs/feature-request-public-private-api-v0.1.0.md).
42+
43+
---
44+
45+
## Configuration
46+
47+
| Variable | Default | Description |
48+
|----------|---------|-------------|
49+
| `API_PORT` | `3000` | HTTP listen port |
50+
| `API_PUBLIC_READ` | `true` | When true, read routes need no `Authorization` header |
51+
| `API_SECRET` | *(unset)* | Legacy Bearer for read when `API_PUBLIC_READ=false`; optional when public read is on |
52+
| `ADMIN_SECRET` | *(unset)* | Future private write API (Phase 2) |
53+
| `API_CHART_RATE_LIMIT` | `60` | Max `POST /api/chart` requests per client IP per minute |
54+
55+
Copy `.env.example` to `.env.local` for local development.
56+
57+
### Secret naming (integrations)
58+
59+
| Name | Use | Required for public chart fetch? |
60+
|------|-----|--------------------------------|
61+
| `API_SECRET` | Legacy read Bearer | **No** (v2.0.0+ with default `API_PUBLIC_READ`) |
62+
| `ADMIN_SECRET` | Private actor writes (Phase 2) | No |
63+
| `COMPASS_REPO_PAT` / `DISPATCH_TOKEN` | GitHub `repository_dispatch` only | No |
64+
| `OSF_PAT` | OSF uploads only | No |
65+
66+
---
67+
68+
## CORS
69+
70+
Public read routes send:
71+
72+
```http
73+
Access-Control-Allow-Origin: *
74+
Access-Control-Allow-Methods: GET, POST, OPTIONS
75+
Access-Control-Allow-Headers: Content-Type, Authorization
76+
```
77+
78+
`OPTIONS` preflight on `/api/*` returns **204**. Admin routes (Phase 2) will not use permissive CORS.
79+
2380
---
2481

2582
## Authentication
@@ -68,7 +125,9 @@ GET /api/health
68125
```json
69126
{
70127
"status": "ok",
71-
"version": "2.2.3"
128+
"version": "2.6.2",
129+
"apiVersion": "2.0.0",
130+
"publicRead": true
72131
}
73132
```
74133

@@ -80,9 +139,10 @@ Returns every political actor in the dataset with basic metadata.
80139

81140
```http
82141
GET /api/actors
83-
Authorization: Bearer <API_SECRET>
84142
```
85143

144+
Public read (default): no `Authorization` header. Optional Bearer is ignored.
145+
86146
**Response — 200 OK**
87147

88148
```json
@@ -91,18 +151,29 @@ Authorization: Bearer <API_SECRET>
91151
{
92152
"name": "Green Party",
93153
"slug": "Green-Party",
94-
"color": "#4a9c5d"
154+
"color": "#4a9c5d",
155+
"category": "UK Political Party",
156+
"lastUpdated": "2026-05-19"
95157
},
96158
{
97159
"name": "Conservative Party",
98160
"slug": "Conservative-Party",
99-
"color": "#1a6bc4"
161+
"color": "#1a6bc4",
162+
"category": "UK Political Party",
163+
"lastUpdated": "2026-05-19"
100164
}
101-
]
165+
],
166+
"meta": {
167+
"count": 32,
168+
"schemaVersion": "1.1.0",
169+
"axesOrder": ["Cultural", "Economic", "Military", "Sovereignty", "Governance", "Class"]
170+
}
102171
}
103172
```
104173

105-
**Error — 401 Unauthorized**
174+
**Response headers:** `Cache-Control: public, max-age=300`
175+
176+
**Error — 401 Unauthorized** (only when `API_PUBLIC_READ=false` and Bearer missing/invalid)
106177

107178
```json
108179
{ "error": "Unauthorized" }
@@ -116,9 +187,10 @@ Returns the full record for a single actor, including metadata, scores, per-ques
116187

117188
```http
118189
GET /api/actors/:slug
119-
Authorization: Bearer <API_SECRET>
120190
```
121191

192+
Public read (default): no `Authorization` header.
193+
122194
The `:slug` parameter is the URL-safe identifier from `data/actors/{slug}.json` (e.g. `conservative-party`, `restore-britain-2025-2026`).
123195

124196
**Response — 200 OK**
@@ -160,13 +232,15 @@ The `:slug` parameter is the URL-safe identifier from `data/actors/{slug}.json`
160232
}
161233
```
162234

235+
**Response headers:** `Cache-Control: public, max-age=300`, `ETag`, `Last-Modified`. Send `If-None-Match` with the prior `ETag` to receive **304 Not Modified** when the file is unchanged.
236+
163237
**Error — 404 Not Found**
164238

165239
```json
166240
{ "error": "Actor not found" }
167241
```
168242

169-
**Error — 401 Unauthorized**
243+
**Error — 401 Unauthorized** (only when `API_PUBLIC_READ=false`)
170244

171245
```json
172246
{ "error": "Unauthorized" }
@@ -176,29 +250,34 @@ The `:slug` parameter is the URL-safe identifier from `data/actors/{slug}.json`
176250

177251
### 4. Render Chart
178252

179-
Generates a radar-chart image from your scores, optionally overlaying known actors.
253+
Generates a radar-chart image from your scores, optionally overlaying known actors. **Rate-limited** on public deployments (see below).
180254

181255
```http
182256
POST /api/chart
183-
Authorization: Bearer <API_SECRET>
184257
Content-Type: application/json
185258
```
186259

260+
Public read (default): no `Authorization` header.
261+
187262
**Request Body**
188263

189264
| Field | Type | Required | Default | Description |
190265
|-------|------|----------|---------|-------------|
191266
| `scores` | `object` | No | All axes `0` | Six-axis scores. Each key must be one of: `Cultural`, `Economic`, `Military`, `Sovereignty`, `Governance`, `Class`. Each value must be a number `0–10`. |
192-
| `actors` | `string[]` | No | `[]` | Names of actors to overlay on the chart. Must match the `name` field from `/api/actors`. |
193-
| `format` | `string` | No | `"svg"` | Output format: `"svg"` or `"png"`. |
194-
| `orientation` | `string` | No | `"flat"` | Chart orientation: `"flat"` (edge-up hexagon; **Cultural** at the top flat edge) or `"pointy"`. |
195-
| `axes` | `string[]` | No | Canonical order (below) | Clockwise spoke order. Must be a permutation of the six axis names. |
196-
| `register` | `string` | No | `"primary"` | Actor overlay score register: `"primary"`, `"declared"`, or `"structural"` (uses `dualRegister` when present). |
197-
| `showUser` | `boolean` | No | `true` | Whether to draw the user’s own score polygon. |
267+
| `actors` | `string[]` | No | `[]` | Actor overlays by `name` from `/api/actors`. **Max 8.** |
268+
| `format` | `string` | No | `"svg"` | `"svg"` or `"png"`. |
269+
| `orientation` | `string` | No | `"flat"` | `"flat"` (**Cultural** at top flat edge) or `"pointy"`. |
270+
| `axes` | `string[]` | No | Canonical order (below) | Clockwise spoke permutation of the six axis names. |
271+
| `register` | `string` | No | `"primary"` | `"primary"`, `"declared"`, or `"structural"` for overlays when `dualRegister` exists. |
272+
| `showUser` | `boolean` | No | `true` | Draw the user score polygon (`false` for actor-only charts). |
198273
| `colors.user` | `string` | No | `"#c8a84b"` | Hex colour for the user polygon. |
199-
| `title` | `string` | No | `"Chart"` | Chart title (embedded in SVG/PNG metadata). |
200-
| `width` | `number` | No | `600` | PNG width in pixels (PNG only). |
201-
| `height` | `number` | No | `600` | PNG height in pixels (PNG only). |
274+
| `title` | `string` | No | `"Chart"` | Chart title. **Max 200 characters.** |
275+
| `width` | `number` | No | `600` | PNG width (PNG only). Max **4096**; `width × height`**16_777_216**. |
276+
| `height` | `number` | No | `600` | PNG height (PNG only). Same limits as `width`. |
277+
278+
**Rate limiting (public read):** Default **60** requests per client IP per minute (`API_CHART_RATE_LIMIT`). Excess requests return **429** with `Retry-After` (seconds).
279+
280+
**Permutation coverage:** Any valid combination of actor set (0–8), register, custom `scores`, `axes` order, `orientation`, and `format` supported by the table above.
202281

203282
**Example Request — SVG**
204283

@@ -262,23 +341,80 @@ Returns raw PNG bytes with `Content-Type: image/png`.
262341
{ "error": "Invalid format. Use svg or png." }
263342
```
264343

265-
**Error — 401 Unauthorized**
344+
```json
345+
{ "error": "At most 8 actor overlays allowed" }
346+
```
347+
348+
```json
349+
{ "error": "Too many requests", "retryAfter": 42 }
350+
```
351+
352+
**Error — 401 Unauthorized** (only when `API_PUBLIC_READ=false`)
266353

267354
```json
268355
{ "error": "Unauthorized" }
269356
```
270357

358+
**Error — 429 Too Many Requests**
359+
360+
```json
361+
{ "error": "Too many requests", "retryAfter": 42 }
362+
```
363+
364+
Response header: `Retry-After: 42`
365+
366+
---
367+
368+
### 5. Axis catalog
369+
370+
Canonical OQ2 axis order and pole labels (for clients building manifests without scraping this document).
371+
372+
```http
373+
GET /api/axes
374+
```
375+
376+
**Response — 200 OK**
377+
378+
```json
379+
{
380+
"axesOrder": ["Cultural", "Economic", "Military", "Sovereignty", "Governance", "Class"],
381+
"scale": {
382+
"min": 0,
383+
"max": 10,
384+
"description": "0 = less-critical pole; 10 = more-critical pole (methodology §II)"
385+
},
386+
"axes": [
387+
{ "name": "Cultural", "low": "Cultural internationalism", "high": "Cultural nationalism" }
388+
]
389+
}
390+
```
391+
392+
---
393+
394+
### 6. OpenAPI document
395+
396+
Machine-readable OpenAPI 3.1 summary of public routes.
397+
398+
```http
399+
GET /api/openapi.json
400+
```
401+
402+
**Response — 200 OK**`Content-Type: application/json` (see `api/openapi-v2.0.0.json` in the repository).
403+
271404
---
272405

273406
## Error Reference
274407

275408
| Status | Meaning |
276409
|--------|---------|
277410
| `200` | Success |
278-
| `400` | Malformed request (bad axis, bad score, unknown actor, invalid format) |
279-
| `401` | Missing or incorrect `Authorization` header |
411+
| `304` | Actor detail unchanged (`If-None-Match` matched) |
412+
| `400` | Malformed request (bad axis, score, actor, format, title length, PNG size, overlay count) |
413+
| `401` | Missing or incorrect `Authorization` when `API_PUBLIC_READ=false` |
280414
| `404` | Unknown endpoint or actor slug not found |
281-
| `500` | Server error (e.g. `API_SECRET` not configured) |
415+
| `429` | Chart rate limit exceeded |
416+
| `500` | Server error |
417+
| `503` | Admin API not configured (`ADMIN_SECRET` missing on future write routes) |
282418

283419
---
284420

@@ -475,38 +611,62 @@ Per-axis object holding separate confidence values for the declared and structur
475611

476612
## Quick cURL Examples
477613

614+
Set `API_BASE` (default `http://localhost:3000`). With default `API_PUBLIC_READ=true`, read routes need **no** Bearer token.
615+
478616
### Health
479617

480618
```bash
481-
curl https://localhost:3000/api/health
619+
curl -sS "$API_BASE/api/health"
482620
```
483621

484-
### Actors (authenticated)
622+
### Actors (public read)
485623

486624
```bash
487-
curl -H "Authorization: Bearer <API_SECRET>" \
488-
https://localhost:3000/api/actors
625+
curl -sS "$API_BASE/api/actors"
489626
```
490627

491-
### Chart SVG
628+
### Actor detail (public read)
492629

493630
```bash
494-
curl -X POST \
495-
-H "Authorization: Bearer <API_SECRET>" \
496-
-H "Content-Type: application/json" \
497-
-d '{"scores":{"Cultural":5,"Economic":5,"Military":5,"Sovereignty":5,"Governance":5,"Class":5},"format":"svg"}' \
498-
https://localhost:3000/api/chart
631+
curl -sS "$API_BASE/api/actors/restore-britain-2025-2026"
499632
```
500633

501-
### Chart PNG with actor overlay
634+
### Axis catalog
635+
636+
```bash
637+
curl -sS "$API_BASE/api/axes"
638+
```
639+
640+
### Chart SVG (public read)
641+
642+
```bash
643+
curl -sS -X POST "$API_BASE/api/chart" \
644+
-H "Content-Type: application/json" \
645+
-d '{"scores":{"Cultural":5,"Economic":5,"Military":5,"Sovereignty":5,"Governance":5,"Class":5},"format":"svg"}'
646+
```
647+
648+
### Chart PNG — paper figure (structural register, no user polygon)
649+
650+
```bash
651+
curl -sS -X POST "$API_BASE/api/chart" \
652+
-H "Content-Type: application/json" \
653+
-d '{
654+
"actors": ["Reform UK", "Green Party"],
655+
"register": "structural",
656+
"showUser": false,
657+
"format": "png",
658+
"width": 1400,
659+
"height": 1400,
660+
"orientation": "flat"
661+
}' -o figure.png
662+
```
663+
664+
See also: [`docs/examples/api/chart-public-read.sh`](docs/examples/api/chart-public-read.sh).
665+
666+
### Legacy mode (`API_PUBLIC_READ=false`)
502667

503668
```bash
504-
curl -X POST \
505-
-H "Authorization: Bearer <API_SECRET>" \
506-
-H "Content-Type: application/json" \
507-
-d '{"scores":{"Cultural":3,"Economic":7,"Military":2,"Sovereignty":8,"Governance":6,"Class":4},"actors":["Conservative Party"],"format":"png","width":800,"height":800}' \
508-
https://localhost:3000/api/chart \
509-
-o chart.png
669+
curl -sS -H "Authorization: Bearer $API_SECRET" "$API_BASE/api/actors"
510670
```
511671

512672
---

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4242

4343
## [Unreleased]
4444

45+
### Changed
46+
47+
- **`API.md` v2.0.0 (complete)** — trust zones, configuration, CORS, public read on all endpoints, `meta` on actors list, chart rate limits and caps, `GET /api/axes` / `GET /api/openapi.json`, error `304`/`429`, public cURL examples.
48+
- **`README.md`** — API section aligned with public read (default `npm run api` without secret; env table; public chart example).
49+
4550
## [2.6.2] - 2026-05-20
4651

4752
### Added

0 commit comments

Comments
 (0)