Skip to main content
Glama
andrei-vintila

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.