From 99506cb4eba990f499110cc98550e34f26c2b78f Mon Sep 17 00:00:00 2001 From: Billy Dunn Date: Fri, 7 Aug 2026 22:16:06 -0500 Subject: [PATCH] =?UTF-8?q?docs(reference):=20correct=20the=20X-API-Key=20?= =?UTF-8?q?auth=20claim=20=E2=80=94=20it=20is=20not=20general-purpose=20(#?= =?UTF-8?q?3247)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `reference/api.mdx` told readers an API key authenticates the REST API and gave `GET /api/v1/devices` as the example. That call returns 401: `authMiddleware` (middleware/auth.ts:448) reads the `Authorization` header and nothing else, and devices/core.ts:276 puts the whole devices surface behind it, so the key is never consulted. It was the first page anyone scripting Breeze reads, and it pointed at a wall of 401s. `X-API-Key` is honoured on three surfaces only, each of them dual-auth (key when the header is present, user JWT when it is absent): - `/api/v1/mcp/*` — the MCP server (index.ts:1114, mcpServer.ts:221) - `POST /api/v1/dev/push` — scope `devices:execute` (index.ts:1115, devPush.ts:79) - `GET`/`PATCH /api/v1/devices/:id/custom-fields` — scopes `devices:read` / `devices:write` (devices/customFieldValues.ts:87, mounted at devices/index.ts:39) Both reference pages now say so, with a table of the accepting routes and their required scopes, and the broken `/devices` example is replaced by one that works. Added the point that scopes gate what a key may DO on those routes but do not widen WHERE it is accepted — a `devices:read` key still cannot call `GET /api/v1/devices`. Also documented the other thing that silently breaks a scripted integration: the `requireMfa()` gate on `POST /orgs/partners` (orgs.ts:394), `POST /orgs/organizations` (:1279) and `POST /orgs/sites` (:1855), so an account without MFA enrolled cannot create tenancy unattended. Verified: `astro build` clean, 150 pages, both pages render, the new `#where-api-keys-work` anchor is generated and the link resolves to it, and the `/features/mcp-server/` target exists. `pnpm test:docs-automation` passes 5/5. --- .../src/content/docs/reference/api-keys.mdx | 27 ++++++++++++++-- apps/docs/src/content/docs/reference/api.mdx | 31 ++++++++++++++++++- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/apps/docs/src/content/docs/reference/api-keys.mdx b/apps/docs/src/content/docs/reference/api-keys.mdx index a71c308d9a..bbd35d3de9 100644 --- a/apps/docs/src/content/docs/reference/api-keys.mdx +++ b/apps/docs/src/content/docs/reference/api-keys.mdx @@ -87,11 +87,13 @@ Scopes can be updated after creation via the `PATCH` endpoint without rotating t ### Authentication Flow -To authenticate with an API key, include it in the `X-API-Key` header: +To authenticate with an API key, include it in the `X-API-Key` header — on one of +the routes that accept it (see [Where API keys work](#where-api-keys-work) below; +the general REST API is not one of them): ```bash curl -H "X-API-Key: brz_aBcDeFgHiJkLmNoPqRsTuVwXyZ012" \ - https://breeze.yourdomain.com/api/v1/devices + https://breeze.yourdomain.com/api/v1/devices/$DEVICE_ID/custom-fields ``` The middleware performs these steps in order: @@ -116,9 +118,28 @@ The middleware performs these steps in order: | `X-RateLimit-Reset` | Unix timestamp when the window resets. | | `Retry-After` | Seconds until the rate limit resets (only on 429 responses). | +### Where API keys work + +An API key authenticates **three surfaces**, not the API as a whole: + +| Endpoint | Required scope | +|---|---| +| `/api/v1/mcp/*` — the [MCP server](/features/mcp-server/) | `ai:*` | +| `POST /api/v1/dev/push` | `devices:execute` | +| `GET /api/v1/devices/:id/custom-fields` | `devices:read` | +| `PATCH /api/v1/devices/:id/custom-fields` | `devices:write` | + +Every other route authenticates from the `Authorization` header alone and never +looks at `X-API-Key`, so a key sent anywhere else returns +`401 Missing or invalid authorization header`. A key with `devices:read` will not +authenticate `GET /api/v1/devices` — that route needs a user JWT. Scopes gate what +a key may do on the routes above; they do not widen where it is accepted. + ### Dual Authentication -Some endpoints accept either a JWT bearer token or an API key. When both headers are present, the API key takes precedence: the route handler checks for `X-API-Key` first and authenticates via `apiKeyAuthMiddleware`, falling back to `authMiddleware` only when that header is absent. +The routes listed above accept either a JWT bearer token or an API key. When both headers are present, the API key takes precedence: the route handler checks for `X-API-Key` first and authenticates via `apiKeyAuthMiddleware`, falling back to `authMiddleware` only when that header is absent. + +On the JWT path some of them are stricter than the key path: `PATCH /devices/:id/custom-fields` and `POST /dev/push` additionally require MFA, matching the permissions they mirror. ### Key Rotation diff --git a/apps/docs/src/content/docs/reference/api.mdx b/apps/docs/src/content/docs/reference/api.mdx index 4919b4c0cc..c35e42e214 100644 --- a/apps/docs/src/content/docs/reference/api.mdx +++ b/apps/docs/src/content/docs/reference/api.mdx @@ -23,13 +23,42 @@ curl -H "Authorization: Bearer $TOKEN" \ https://breeze.yourdomain.com/api/v1/devices ``` -API keys can also be used via the `X-API-Key` header: +### API keys are not general-purpose auth + + + +The surfaces that accept `X-API-Key`: + +| Endpoint | Required key scope | +|---|---| +| `/api/v1/mcp/*` — the [MCP server](/features/mcp-server/) | `ai:*` | +| `POST /api/v1/dev/push` | `devices:execute` | +| `GET /api/v1/devices/:id/custom-fields` | `devices:read` | +| `PATCH /api/v1/devices/:id/custom-fields` | `devices:write` | + +Those four routes are dual-auth: they use the key when `X-API-Key` is present +and fall back to a user JWT when it is absent. Everything else is JWT-only. ```bash +# Works — a dual-auth route. +curl -H "X-API-Key: brz_..." \ + https://breeze.yourdomain.com/api/v1/devices/$DEVICE_ID/custom-fields + +# Returns 401 — the devices list is JWT-only. curl -H "X-API-Key: brz_..." \ https://breeze.yourdomain.com/api/v1/devices ``` +If you are scripting against Breeze, log in and use the resulting JWT. Note that +some write routes additionally require MFA on the JWT path — including +`POST /orgs/partners`, `POST /orgs/organizations` and `POST /orgs/sites` — so an +account without MFA enrolled cannot create tenancy unattended. + ### Login ```bash