## API Key Management

### Overview

The service uses **Bearer token authentication** with a granular, permission-scoped API key model. There are two kinds of keys:

1. A single, bootstrap **Master Key**, injected at deploy time, used only to create operational keys.
2. **Operational API keys**, created via the API, scoped to specific permissions and (optionally) time-limited via TTL.

The intent of this model is to avoid using the Master Key for day-to-day operations and to give each client only the permissions it actually needs. In our current deployment two operational clients hold keys:

- **Mobile app** — `hotupdater:read analytics:write`
- **Jenkins / CI build pipeline** — `hotupdater:write admin:write`

### Authentication

Every protected endpoint requires an `Authorization` header in the form:

```
Authorization: Bearer <api_key_value>
```

#### Authentication outcomes

| Condition | Status | Response |
|-----------|--------|----------|
| Missing `Authorization` header | `401` | `{"error": "Unauthorized..."}` |
| Token is not a known API key | `401` | `{"error": "Unauthorized..."}` |
| Token is a known API key but expired | `401` | `{"error": "Unauthorized: API key has expired"}` |
| Token is valid but lacks required permission | `403` | `{"error": "Forbidden: Insufficient permissions..."}` |
| Token is valid and has the required permission | endpoint runs | — |

Expired keys are rejected at the authentication layer for all protected endpoints, including hot-updater, admin, analytics, and api-key endpoints themselves.

#### Public endpoints

The CDN file-download endpoint (`GET /<key>` on the CDN host) is intentionally public and does not require authentication — bundle artifacts must be reachable by mobile clients without a credential. Bundles uploaded to the CDN should therefore be treated as world-readable; secrets must never be placed in bundle files.

### The Master Key

The Master Key is a single high-privilege credential injected into the service via the `API_MASTER_KEY` environment variable at build/deploy time.

#### Properties

- Set once, at deploy time, via environment configuration.
- Implicitly grants **all** permissions — equivalent to holding every permission listed below.
- Not stored as a database row; it is validated directly against the environment variable.
- Cannot be created, listed, rotated, or deleted through the API.
- Never expires (no TTL).

#### Intended use

The Master Key exists **only** to bootstrap the system. The expected operator workflow is:

1. Deploy the service with `API_MASTER_KEY` set to a strong, randomly generated value.
2. Use the Master Key once (via `POST /api-keys`) to create the operational keys needed by each client (mobile app, Jenkins) with the minimum permissions required.
3. Store the Master Key in a secrets manager and **stop using it** for routine traffic.

The Master Key should be treated as a break-glass credential. Routine operations must use scoped, TTL-bound operational keys.

#### Rotating the Master Key

Because the Master Key lives in environment configuration, rotation is a redeploy:

1. Generate a new value.
2. Update the environment variable in your secrets manager / CI pipeline.
3. Redeploy the service. The previous Master Key value is invalidated as soon as the new container starts.

Operational keys created using the old Master Key remain valid; rotating the Master Key does not invalidate them.

### Permissions

Permissions follow a `domain:action` naming scheme. The full set is closed — only the values below are accepted.

| Permission | Grants |
|-----------|--------|
| `hotupdater:read` | `GET` on hot-updater endpoints (e.g. version, bundle listings, channels, single bundle lookup) |
| `hotupdater:write` | All `hotupdater:read` operations plus mutating endpoints (`POST`, `PATCH`, `DELETE` on bundles) |
| `analytics:write` | `POST /analytics/track` — emit download/install/rollback events |
| `admin:read` | `GET` on admin endpoints (e.g. admin bundle queries) |
| `admin:write` | All `admin:read` operations plus mutations: file uploads (`POST /admin/files`), bundle promotion (`POST /admin/bundles/promote`), enable/disable (`PATCH /admin/bundles/:id/enabled`) |
| `apikey:read` | List and read API keys (preview only — full key values are never returned after creation) |
| `apikey:write` | All `apikey:read` operations plus creating and deleting API keys |

#### Permission rules

- **No implicit cross-domain access.** Holding `admin:write` does not grant `hotupdater:read`; holding `hotupdater:write` does not grant `admin:read`. Each domain is checked independently. If a client needs to read hot-updater data and write to admin endpoints, it needs **both** permissions on its key.
- **`write` implies `read` within the same domain.** A `hotupdater:write` key can call `GET /hot-updater/...` endpoints. The same applies to `admin:write` and `apikey:write`.
- `analytics` has only a `write` action — there is no `analytics:read`.

#### Combining permissions on a single key

A key can hold multiple permissions, separated by **single spaces**:

```json
{
  "title": "Jenkins CI",
  "permission": "hotupdater:write admin:write",
  "ttl": "1y"
}
```

Validation rules:

- Only values from the table above are accepted. Unknown values (e.g. `invalid:permission`) return `400 Invalid permission`.
- Duplicates are rejected (`hotupdater:read hotupdater:read` → `400 Duplicate permissions`).
- Permissions must be separated by exactly one space. Multiple spaces between values (`hotupdater:read  hotupdater:write`) return `400 must be separated by single spaces`.

#### Permission scopes for our clients

| Client | Permissions | Rationale |
|--------|-------------|-----------|
| Mobile app | `hotupdater:read analytics:write` | Reads bundle metadata to decide whether to update; reports download/install/rollback events. Never mutates server-side state. |
| Jenkins / CI build pipeline | `hotupdater:write admin:write` | Publishes new bundles (creates, updates, deletes hot-updater records) and performs admin actions (uploads bundle files to storage, promotes bundles between channels, enables/disables bundles). |

Any future client should receive the narrowest set of permissions that lets it do its job. New permission combinations should be reviewed before issuance.

### TTL and Expiration

Every key is created with an explicit `ttl` parameter. There is no default — the request is rejected if `ttl` is missing.

#### Accepted TTL formats

| Format | Meaning |
|--------|---------|
| `never` | Key never expires (`expires_at` is `null`) |
| `Nd` | `N` days, where `N > 0` (e.g. `1d`, `7d`, `30d`) |
| `Nw` | `N` weeks (`1w` = 7 days) |
| `Nm` | `N` months (`1m` = 30 days) |
| `Ny` | `N` years (`1y` = 365 days) |

Invalid TTL values are rejected at creation:

- Missing unit (`"30"`) → `400 Invalid TTL format`
- Unknown unit (`"5x"`) → `400 Invalid TTL format`
- Non-numeric (`"invalid"`) → `400 Invalid TTL format`
- Zero (`"0d"`) → `400 Value must be greater than 0`
- Negative (`"-5d"`) → `400 Invalid TTL format`

#### How `expires_at` is computed

`expires_at` is computed once at key creation and stored on the record. It is set to the **beginning of day in UTC** (`00:00:00.000Z`) on the calculated expiration date.

For example, a key created with `ttl: "7d"` will have `expires_at` set to the UTC date 7 days from now at exactly midnight UTC. This means the actual lifetime of a key is between `ttl` and `ttl + 24h` depending on what time of day it was issued. This is intentional — it makes expiration dates predictable and rounded, and avoids leaking second-precision creation timestamps.

#### Behavior of expired keys

- Expired keys return `401 Unauthorized: API key has expired` on every protected endpoint (this is distinct from `401 Unauthorized` for unknown tokens).
- Expired keys **remain visible** in `GET /api-keys` so that operators can audit and clean them up.
- Expired keys **can still be deleted** via `DELETE /api-keys/:id` (assuming the caller has `apikey:write`).
- Expiration is enforced at request time by checking `expires_at` against the current time. There is no background sweep that deletes expired keys.

#### Recommended TTL policy

- Mobile app key: rotated on the app's release cadence; TTL chosen to comfortably outlast the release window.
- Jenkins / CI key: rotated on a regular cadence (e.g. quarterly or annually); typically `90d`–`1y`.
- `ttl: "never"` should be avoided and re-justified during periodic security audits when it does appear.

### Key Storage and Disclosure

#### Full key value is shown exactly once

When a key is created via `POST /api-keys`, the response includes the field `apiKey.key_value` — the raw bearer token to be configured into the client. **This is the only time the full value is returned.** It is the caller's responsibility to capture and store it securely (typically in a secrets manager) at creation time.

#### Subsequent reads return a preview only

All subsequent `GET /api-keys` and `GET /api-keys/:id` calls return `key_value_preview` instead of `key_value`. The preview format is the first 12 characters of the key followed by `...`, allowing operators to visually identify a key without exposing it.

```json
{
  "id": "...",
  "title": "Jenkins CI",
  "permission": "hotupdater:write admin:write",
  "key_value_preview": "abcd12345678...",
  "expires_at": "2026-11-13T00:00:00.000Z",
  "created_at": "2025-11-13T14:22:31.123Z"
}
```

If a key value is lost, it cannot be recovered. The correct remediation is to delete the old key and create a new one.

#### Storage at rest

API keys are stored as records in the application database (`api_keys` table, accessed via Prisma). Operators are responsible for protecting the database itself with the usual controls (encryption at rest, restricted network access, principle-of-least-privilege on the DB user).

### API Key Endpoints

All `/api-keys` endpoints require Bearer authentication. The Master Key authorizes all of them implicitly. Operational keys must hold the relevant `apikey:*` permission.

#### `GET /api-keys` — list keys

- **Permission required:** `apikey:read` (or `apikey:write`)
- **Response:** `{ "apiKeys": [ ... ] }` — array of key records with `key_value_preview` (not full values).
- Expired keys are included in results.

#### `GET /api-keys/:id` — fetch a single key

- **Permission required:** `apikey:read` (or `apikey:write`)
- `404` if the key does not exist.
- Returns `key_value_preview` only.

#### `POST /api-keys` — create a key

- **Permission required:** `apikey:write` (or Master Key)
- **Required body fields:** `title`, `permission`, `ttl`. Each missing field returns `400 Missing required parameter: <name>`.
- On success, returns `201` with the full `apiKey` object **including `key_value`** (the only time this is returned).

Example:

```http
POST /api-keys
Authorization: Bearer <master_key_or_apikey:write_key>
Content-Type: application/json

{
  "title": "Mobile app prod 2026",
  "permission": "hotupdater:read analytics:write",
  "ttl": "1y"
}
```

#### `DELETE /api-keys/:id` — revoke a key

- **Permission required:** `apikey:write`
- Deletion is immediate: the next request using that key value returns `401`.
- `404` if the key has already been deleted or never existed.
- Works on expired keys (useful for cleanup).

### Operational Workflow

A typical deployment lifecycle:

1. **Deploy.** Service is deployed with `API_MASTER_KEY` set in the environment.
2. **Bootstrap keys.** Operator calls `POST /api-keys` twice using the Master Key — once for the mobile app (`hotupdater:read analytics:write`) and once for Jenkins (`hotupdater:write admin:write`) — each with an appropriate TTL.
3. **Distribute.** Each generated `key_value` is stored in the appropriate secrets manager (Jenkins credentials store for the CI key, mobile build configuration for the app key).
4. **Master key parked.** The Master Key is moved to a sealed/break-glass location and no longer used for routine traffic.
5. **Rotation.** Before each operational key expires, a replacement is issued and the old one is deleted. The mobile-app key is rotated on the app's release cadence.
6. **Audit.** `GET /api-keys` is reviewed periodically. Unused or expired keys are deleted. Any `ttl: "never"` keys are re-justified.
7. **Incident response.** If a key is suspected compromised, `DELETE /api-keys/:id` revokes it instantly; a new key is issued with a new value.

### Security Properties Summary

| Property | Behavior |
|----------|----------|
| Credential type | Bearer token (opaque string) |
| Privileged bootstrap credential | Single Master Key, env-injected, not in DB, not API-manageable |
| Authorization model | Allow-list of explicit `domain:action` permissions per key |
| Cross-domain access | None implicit — each domain checked independently |
| Time-bound | Mandatory TTL on creation; expiration enforced at every request |
| Secret disclosure | Full key value returned exactly once at creation; preview-only thereafter |
| Revocation | Immediate via `DELETE /api-keys/:id`; takes effect on next request |
| Rotation of Master Key | Redeploy with new env value; existing operational keys unaffected |
| Public surface | Only CDN file-download is unauthenticated; all data-plane and control-plane endpoints require a valid, permissioned, non-expired key |

### Quick Reference: HTTP Status Codes

| Code | Meaning |
|------|---------|
| `200` / `201` | Request succeeded |
| `400` | Validation error (bad `ttl`, unknown permission, missing field, duplicate permission, etc.) |
| `401` | Authentication failed: missing token, unknown token, or expired key (response message distinguishes the cases) |
| `403` | Authentication succeeded but the key lacks the required permission for the endpoint |
| `404` | Resource not found (e.g. unknown api-key id, unknown bundle id) |
