# MSChanges Production API

A small, read-only HTTP API over the Microsoft documentation change feed that powers
[MSChanges](https://mc.msnugget.com/). It exposes the same data used by the production
website as JSON for scripts, dashboards, and automation.

> The underlying dataset is already public (the site ships it as a static file).
> The API adds a clean, queryable, rate-limited interface on top — it is not a
> secrecy boundary.

## Production

```
https://mc.msnugget.com/api/v1
```

- Website: `https://mc.msnugget.com/`
- Consumer documentation: `https://mc.msnugget.com/api`
- Worker: `ms-changes`
- Source branch: `main`
- Runtime: Cloudflare Workers with static assets and a Durable Object rate limiter
- API version: `v1`

All examples below target production.

## Authentication

Every request needs an API key, sent **either** way:

```bash
curl -H "Authorization: Bearer $MSCHANGES_API_KEY" https://mc.msnugget.com/api/v1/health
curl -H "x-api-key: $MSCHANGES_API_KEY" https://mc.msnugget.com/api/v1/health
```

Missing key → `401 missing_api_key`. Unknown key → `401 invalid_api_key`.
Disabled key → `403 disabled_api_key`.

### Getting a key

API keys are approved manually. Request one from either maintainer on LinkedIn and
briefly describe what you want to build:

- [Jannik Reinhard](https://www.linkedin.com/in/jannik-r/)
- [Florian Salzmann](https://www.linkedin.com/in/fsalzmann/)

The same request process is linked from the [MSChanges homepage](https://mc.msnugget.com/#api).

### Issuing and revoking keys

Keys are minted from the repo with a small CLI. The Worker reads only the SHA-256
hashes in `api-keys.json`. For hand-out, raw keys are also written to
`api-keys.distribution.json`; that file is excluded from the deployed static assets.
It is confidential and may only live in a **private** repository. Every collaborator
with repository read access can see those keys, including old keys in Git history.

```bash
npm run key:new -- --name "Florian CLI"   # stores raw key in the confidential distribution file
npm run key:list                          # list keys (never shows the secret)
npm run key:revoke -- k_ab12cd34          # disable a key

# Optional: do not write the raw key to the distribution file
npm run key:new -- --name "One-time key" --no-store
```

## Rate limit

Two exact limits are enforced by Durable Objects using fixed 60-second windows:

- **60 requests/minute per client IP**, applied before authentication (including
  preflight and unsupported methods). This prevents missing and random keys from
  bypassing throttling.
- **20 requests/minute per valid API key**, applied after authentication.

Over either limit → `429 rate_limited` with a `Retry-After` header. Responses include
`RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`, and `RateLimit-Policy`.
Cloudflare's network-level DDoS protection remains the outer layer; these application
limits protect Worker and dataset work.

## Endpoints

### `GET /changes?product=&area=&status=&since=&q=&limit=&offset=`
List documentation changes (newest first) with optional filters.
- `product` — e.g. `Intune`, `Entra`, `Defender`
- `area` — sub-area within a product
- `status` — change status (e.g. `added`, `modified`, `removed`)
- `since` — ISO date (`2026-06-01`), inclusive lower bound on the change date
- `q` — substring across title/product/area/message/author/path
- `limit` (default 50, max 200), `offset`

```bash
curl -H "x-api-key: $MSCHANGES_API_KEY" "https://mc.msnugget.com/api/v1/changes?product=Intune&since=2026-06-01&limit=5"
```
```jsonc
{ "meta": { "total": 240, "count": 5, "limit": 5, "offset": 0, "next": 5 },
  "data": [ { "id": "...", "product": "Intune", "area": "...", "title": "...", "status": "modified",
              "date": "2026-06-12", "commitUrl": "https://github.com/...", "fileUrl": "https://..." } ] }
```

### `GET /changes/<id>`
A single change by id, or `404 not_found`.

### `GET /products`
The product catalog (labels, colors, tags) plus dataset meta.

### `GET /health`
```jsonc
{ "status": "ok", "changes": 12000, "products": 20 }
```

## Errors

All errors share one shape:
```jsonc
{ "error": { "code": "rate_limited", "message": "Rate limit of 20 requests per minute exceeded." } }
```
| Status | code | meaning |
|-------|------|---------|
| 401 | `missing_api_key` / `invalid_api_key` | no / unknown key |
| 403 | `disabled_api_key` | key revoked |
| 404 | `not_found` | unknown route or id |
| 405 | `method_not_allowed` | only `GET` is supported |
| 429 | `rate_limited` | per-IP or per-key limit exceeded |
| 503 | `data_unavailable` | dataset not built (run `npm run build:api`) |

## CORS

`Access-Control-Allow-Origin: *` on all API responses; `OPTIONS` preflight supported.

## Production release procedure

Requirements: Node.js 22+, repository write access, and an authenticated Wrangler
session with access to the `ms-changes` Cloudflare Worker.

```bash
git switch main
git pull --ff-only
npm ci
npm test
npm run deploy
```

`npm run deploy` rebuilds `data/api/changes.json` and then publishes the Worker,
static assets, key hashes, and Durable Object binding from `wrangler.jsonc`.

Adding or revoking an API key is a production code change: update the registry with
the key CLI, commit both key files to the private repository, and deploy again before
telling the recipient that the key is active.

### Post-deploy smoke test

```bash
export MSCHANGES_API_KEY='msc_live_…'

curl --fail-with-body \
  -H "x-api-key: $MSCHANGES_API_KEY" \
  https://mc.msnugget.com/api/v1/health

curl --fail-with-body \
  -H "x-api-key: $MSCHANGES_API_KEY" \
  "https://mc.msnugget.com/api/v1/changes?limit=1"
```

Confirm that both return `200`, the change response contains one record, and the
response headers include `RateLimit-Limit: 20` and `RateLimit-Remaining`.

Also verify that the confidential distribution file is not public:

```bash
curl -o /dev/null -sS -w '%{http_code}\n' \
  https://mc.msnugget.com/api-keys.distribution.json
# expected: 404
```

### Rollback

Revert the faulty production commit with `git revert`, push `main`, and run
`npm run deploy` again. Do not remove or rewrite Git history: raw keys that were ever
committed must be treated as exposed to repository collaborators and revoked normally.
