avemguvern
README.md
# avemguvern.ro
A one-question site: **does Romania currently have a (full, non-interim) government?**
Current answer: **Nu! — Inca e interimar Bolojan!**
Designated PM: **Siegfried Muresan (PNL)**, waiting on the investiture vote.
Everything runs in a single Cloudflare Worker (free tier):
- **Static page** — minimalist verdict, served from `public/`, plus a nominee card
whenever someone is designated but not yet voted in.
- **Public API** — `GET /api/status` (read), `POST /api/status` (admin, token-protected).
- **Crowd-sourced jokes** — `POST /api/joke` (public, body `{"combo":["AUR","UDMR"],"joke":"..."}`)
lets visitors submit a joke for a party combination; stored but never shown live.
Review them with `GET /api/jokes` (admin token): `curl .../api/jokes -H "Authorization: Bearer $ADMIN_TOKEN"`.
- **MCP server** — read-only `get_government_status` tool at `/mcp` (Streamable HTTP).
State is one Cloudflare KV key (`current`). If KV is empty the Worker serves
`DEFAULT_STATUS` from `src/index.ts`, so it works before seeding.
## Project layout
```
public/index.html # the page (no build step)
src/index.ts # Worker: routes /api/status, /mcp, else static assets
wrangler.jsonc # Worker + assets + KV config
seed.json # initial KV value
```
## Local development
```sh
npm install
npx wrangler dev
```
Then:
```sh
# read
curl http://localhost:8787/api/status
# MCP: list tools
curl -X POST http://localhost:8787/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# MCP: call the tool
curl -X POST http://localhost:8787/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_government_status"}}'
```
For a local admin token during `wrangler dev`, create `.dev.vars`:
```
ADMIN_TOKEN=some-local-token
```
## Deploy
```sh
npm install
npx wrangler login
# 1. Create the KV namespace, then paste the returned id into wrangler.jsonc
npx wrangler kv namespace create GOV_STATUS
# 2. Create the D1 database for joke suggestions, paste its database_id into
# wrangler.jsonc, then apply the schema to the remote database
npx wrangler d1 create avemguvern-suggestions
npx wrangler d1 execute avemguvern-suggestions --remote --file=./schema.sql
# 3. Set the admin token (used to authorize POST /api/status and GET /api/jokes)
npx wrangler secret put ADMIN_TOKEN
# 4. Deploy
npx wrangler deploy
# 5. (optional) Seed the KV value — default already matches
npm run seed
```
For local development, apply the schema to the local D1 once:
```sh
npx wrangler d1 execute avemguvern-suggestions --local --file=./schema.sql
```
### Custom domain
After the first deploy, attach `avemguvern.ro` in the Cloudflare dashboard
(**Workers & Pages → avemguvern-ro → Settings → Domains & Routes**), or add to
`wrangler.jsonc`:
```jsonc
"routes": [{ "pattern": "avemguvern.ro", "custom_domain": true }]
```
(DNS for the domain must be on Cloudflare.)
## Updating the status
When the political situation changes, patch the status (only send fields that change):
```sh
curl -X POST https://avemguvern.ro/api/status \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'content-type: application/json' \
-d '{"hasGovernment": true, "answer": "Da!", "subtitle": "Avem guvern plin!", "interim": false, "primeMinister": "..."}'
```
`updatedAt` is stamped automatically.
### The nominee
`nominee` / `nomineeParty` describe the **designated** prime minister, separately from
`primeMinister` (whoever actually runs the government right now, interim or not). While
`nominee` is set, the page shows a card under the verdict, the MCP tool mentions the
nomination, and the coalition builder tells you whether the coalition you picked would
actually carry them.
```sh
# a new nomination
curl -X POST https://avemguvern.ro/api/status \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'content-type: application/json' \
-d '{"nominee": "Siegfried Muresan", "nomineeParty": "PNL"}'
# voted in — the nominee becomes the PM and the card disappears
curl -X POST https://avemguvern.ro/api/status \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'content-type: application/json' \
-d '{"hasGovernment": true, "interim": false, "answer": "Da!",
"subtitle": "Avem guvern plin!", "primeMinister": "Siegfried Muresan",
"nominee": "", "nomineeParty": ""}'
```
`nomineeParty` must be one of the party ids used by the page (`PSD`, `AUR`, `PNL`,
`USR`, `SOS`, `UDMR`, `POT`, `Minoritati`) or `""`; anything else is ignored. If the
nominee matches a party leader the page already ships a cutout for
(`public/leaders/`), that photo is reused in the card — otherwise it falls back to a
silhouette.
## Using the MCP server with Claude
Visit `https://avemguvern.ro/mcp` in a browser for setup instructions, or add it directly:
```sh
claude mcp add --transport http avemguvern https://avemguvern.ro/mcp
```
It exposes one read-only tool, `get_government_status`, that returns the current answer
plus the raw status JSON.
## Abuse protection
Built into the Worker:
- **Edge caching** — `GET /api/status` is cached at the Cloudflare edge via the
Cache API (`caches.default`) for `CACHE_TTL_SECONDS` (default 60), so a read flood
hits KV at most ~once/minute per data center. A successful `POST` purges the cache
so updates show immediately. The Cache API is per-colo, so a write purges only the
data center that served it; other regions refresh when their TTL expires (hence the
modest default — raise it in `wrangler.jsonc` vars for more offload if you can
tolerate longer cross-region staleness after a change).
- **Per-IP rate limiting** — native Workers rate-limit bindings: reads 120/min,
writes 10/min (the write limit also throttles token guessing). Over-limit → `429`.
- **Hardened writes** — `POST` only accepts the known fields
(`hasGovernment`, `interim`, `answer`, `subtitle`, `primeMinister`, `nominee`,
`nomineeParty`) with correct types, clamps strings to 200 chars, rejects bodies over
2 KB (`413`), and compares the admin token in constant time. `nomineeParty` is an
enum — only a known party id or `""` is stored. A leaked token can't store arbitrary
or huge data.
Recommended at the Cloudflare edge (dashboard):
- **WAF Rate Limiting rule** (e.g. per-IP threshold on `/api/*` and `/mcp`) — this is
the layer that drops abusive traffic *before* it counts against your quota.
- **Bot Fight Mode** under Security → Bots.
> Note: a zone-level **Cache Rule cannot bypass the Worker** here — for a Worker-owned
> route the Worker always executes before the cache is checked
> ([docs](https://developers.cloudflare.com/cache/interaction-cloudflare-products/workers-cache-rules/)).
> The in-Worker Cache API above is the effective edge-cache mechanism; it saves KV
> operations but each request still counts as one Worker invocation. To cut invocations
> themselves, use the WAF rate-limit rule.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues