|
1 | | -# API Contracts -- Backend |
| 1 | +# API Contracts |
2 | 2 |
|
3 | | -> Auto-generated on 2026-04-17 (post-pivot rewrite) |
| 3 | +REST reference for the helPRs API. All endpoints under `/api/v1/*` require auth unless noted. Pydantic schemas are defined in `apps/api/src/helprs/modules/*/schemas.py`; see [data-models-api.md](data-models-api.md) for the field details. |
4 | 4 |
|
5 | | -## Overview |
| 5 | +**Conventions** |
6 | 6 |
|
7 | | -- Base prefix: `/api/v1` |
8 | | -- Auth mechanism: JWT Bearer tokens (HS256). Access tokens (15 min) issued after GitHub OAuth. Refresh tokens (7 days) stored as httpOnly cookies. |
9 | | -- Rate limiting: SlowAPI (per-IP via `get_remote_address`) |
10 | | -- Error format: `{"error": "<error_code>", "message": "<text>", "detail": <any>}` |
11 | | -- Webhook auth: HMAC SHA-256 signature verification via `X-Hub-Signature-256` header |
| 7 | +- Base path: `/api/v1` |
| 8 | +- Unauthenticated endpoints return `401 Unauthorized` when a bearer token is missing or invalid. Installation/session access failures return `403 Forbidden`. |
| 9 | +- Rate limiting is applied per route (via `slowapi`). Limits are shown inline below. |
| 10 | +- `{installation_id}` in paths is the **GitHub installation ID** (integer), not the internal UUID. |
| 11 | +- Session IDs are UUIDs. |
12 | 12 |
|
13 | | -## Route Groups |
| 13 | +## Health |
14 | 14 |
|
15 | | -### Health -- /health |
| 15 | +### `GET /health` |
16 | 16 |
|
17 | | -| Method | Path | Auth | Rate Limit | Description | |
18 | | -|--------|------|------|------------|-------------| |
19 | | -| GET | `/health` | None | None | Health check | |
| 17 | +Unauthenticated liveness probe that also checks DB reachability. |
20 | 18 |
|
21 | | -#### GET /health |
| 19 | +- **200** `{"status": "ok", "db": "ok"}` — API + DB healthy. |
| 20 | +- **503** `{"status": "degraded", "db": "unreachable"}` — API up, DB unreachable. |
22 | 21 |
|
23 | | -- Response: `{"status": "ok"}` |
24 | | -- Notes: Mounted directly on the app, not under `/api/v1` |
| 22 | +## Auth — `/api/v1/auth/*` |
25 | 23 |
|
26 | | ---- |
| 24 | +### `GET /auth/github` |
27 | 25 |
|
28 | | -### Auth -- /api/v1/auth |
| 26 | +Redirects to GitHub OAuth (`github.com/login/oauth/authorize`). Sets an httpOnly `oauth_state` cookie for CSRF (10 min TTL). Scopes requested: `read:user user:email read:org`. |
29 | 27 |
|
30 | | -| Method | Path | Auth | Rate Limit | Description | |
31 | | -|--------|------|------|------------|-------------| |
32 | | -| GET | `/api/v1/auth/github` | None | 10/min | Redirect to GitHub OAuth | |
33 | | -| GET | `/api/v1/auth/github/callback` | None | 10/min | Handle OAuth callback | |
34 | | -| POST | `/api/v1/auth/refresh` | Refresh cookie | 10/min | Refresh access token | |
35 | | -| GET | `/api/v1/auth/me` | Bearer JWT | 30/min | Get current user | |
36 | | -| POST | `/api/v1/auth/logout` | None | None | Clear refresh cookie | |
| 28 | +- **302** redirect to GitHub. |
| 29 | +- Rate limit: 10/min. |
37 | 30 |
|
38 | | -#### GET /api/v1/auth/github |
| 31 | +### `GET /auth/github/callback` |
39 | 32 |
|
40 | | -- Response: 302 redirect to `https://github.com/login/oauth/authorize` |
41 | | -- Sets `oauth_state` cookie (httpOnly, samesite=lax, 600s max-age) |
| 33 | +Handles two entry flows: OAuth login (with `state`) and GitHub App install (with `installation_id` + `setup_action`, no `state`). |
42 | 34 |
|
43 | | -#### GET /api/v1/auth/github/callback |
| 35 | +Query params: `code` (required), `state` (OAuth flow), `installation_id` + `setup_action` (install flow). |
44 | 36 |
|
45 | | -- Query params: `code` (required), `state` (required) |
46 | | -- Response: 302 redirect to `{frontend_url}/auth/callback?access_token={jwt}` |
47 | | -- Sets `refresh_token` cookie (httpOnly, 7-day max-age) |
| 37 | +On success: creates/updates the user, issues an access token, sets an httpOnly `refresh_token` cookie (7 days), and redirects to `{APP_BASE_URL}/auth/callback?access_token=...`. |
48 | 38 |
|
49 | | -#### POST /api/v1/auth/refresh |
| 39 | +- **302** redirect on success. |
| 40 | +- **401** on missing/invalid `state`. |
| 41 | +- Rate limit: 10/min. |
50 | 42 |
|
51 | | -- Auth: `refresh_token` httpOnly cookie |
52 | | -- Response: `TokenResponse { access_token, token_type }` |
| 43 | +### `POST /auth/refresh` |
53 | 44 |
|
54 | | -#### GET /api/v1/auth/me |
| 45 | +Rotates the access token using the `refresh_token` httpOnly cookie. Writes a new `refresh_token` cookie on the response. |
55 | 46 |
|
56 | | -- Auth: Bearer JWT |
57 | | -- Response: `UserResponse { id, github_id, github_login, email, avatar_url, created_at }` |
| 47 | +- **200** `TokenResponse { access_token, token_type: "bearer" }`. |
| 48 | +- **401** if the refresh cookie is missing or invalid. |
| 49 | +- Rate limit: 10/min. |
58 | 50 |
|
59 | | -#### POST /api/v1/auth/logout |
| 51 | +### `GET /auth/me` |
60 | 52 |
|
61 | | -- Response: `{"status": "ok"}` |
62 | | -- Deletes `refresh_token` cookie |
| 53 | +Requires auth. Returns the authenticated user. |
63 | 54 |
|
64 | | ---- |
| 55 | +- **200** `UserResponse`. |
| 56 | +- Rate limit: 30/min. |
65 | 57 |
|
66 | | -### Installations -- /api/v1/installations |
| 58 | +### `GET /auth/me/stats` |
67 | 59 |
|
68 | | -| Method | Path | Auth | Rate Limit | Description | |
69 | | -|--------|------|------|------------|-------------| |
70 | | -| GET | `/api/v1/installations` | Bearer JWT | 30/min | List accessible installations | |
71 | | -| GET | `/api/v1/installations/{installation_id}` | Bearer JWT | 30/min | Get installation detail | |
72 | | -| POST | `/api/v1/installations/{installation_id}/byok` | Bearer JWT | 10/min | Configure credentials | |
73 | | -| DELETE | `/api/v1/installations/{installation_id}/byok` | Bearer JWT | 10/min | Remove credentials | |
74 | | -| PUT | `/api/v1/installations/{installation_id}/suppression-labels` | Bearer JWT | 10/min | Update suppression labels | |
| 60 | +Requires auth. Returns aggregated session statistics for the user. |
75 | 61 |
|
76 | | -**Path params:** `installation_id` is the **GitHub installation ID** (integer), not the internal UUID. |
| 62 | +- **200** `UserStatsResponse { daily_counts: [...], totals: {...} }`. |
| 63 | +- Rate limit: 30/min. |
77 | 64 |
|
78 | | -#### GET /api/v1/installations |
| 65 | +### `POST /auth/logout` |
79 | 66 |
|
80 | | -- Response: `InstallationListResponse { items: InstallationResponse[], total: int }` |
| 67 | +Clears the `refresh_token` cookie. |
81 | 68 |
|
82 | | -#### POST /api/v1/installations/{installation_id}/byok |
| 69 | +- **200** `{"status": "ok"}`. |
83 | 70 |
|
84 | | -- Request: `BYOKConfigureRequest { api_key: str }` -- Claude credentials, min 20 chars |
85 | | -- Response: `BYOKConfigResponse { key_hint, key_status, validated_at }` |
86 | | -- Notes: Key is Fernet-encrypted at rest. |
| 71 | +## Installations — `/api/v1/installations/*` |
87 | 72 |
|
88 | | -#### DELETE /api/v1/installations/{installation_id}/byok |
| 73 | +All endpoints require auth. Admin endpoints additionally call `verify_admin_permission`. |
89 | 74 |
|
90 | | -- Response: 204 No Content |
| 75 | +### `GET /installations` |
91 | 76 |
|
92 | | -#### PUT /api/v1/installations/{installation_id}/suppression-labels |
| 77 | +List installations the user has access to (owns, or org member). |
93 | 78 |
|
94 | | -- Request: `SuppressionLabelsRequest { labels: list[str] }` -- max 20 items |
95 | | -- Response: `SuppressionLabelsResponse { labels: list[str] }` |
| 79 | +- **200** `InstallationListResponse { items: InstallationResponse[], total }`. Each item includes `session_count`, BYOK status, suppression labels, and `post_results_to_pr`. |
| 80 | +- Rate limit: 30/min. |
96 | 81 |
|
97 | | ---- |
| 82 | +### `GET /installations/{installation_id}` |
98 | 83 |
|
99 | | -### Webhooks -- /api/v1/webhooks |
| 84 | +Installation detail — requires admin permission on the installation. |
100 | 85 |
|
101 | | -| Method | Path | Auth | Rate Limit | Description | |
102 | | -|--------|------|------|------------|-------------| |
103 | | -| POST | `/api/v1/webhooks/github` | HMAC SHA-256 | 100/min | Receive GitHub webhooks | |
| 86 | +- **200** `InstallationDetailResponse`. |
| 87 | +- **404** if unknown. |
| 88 | +- Rate limit: 30/min. |
104 | 89 |
|
105 | | -#### POST /api/v1/webhooks/github |
| 90 | +### `POST /installations/{installation_id}/byok` |
106 | 91 |
|
107 | | -- Auth: `X-Hub-Signature-256` header |
108 | | -- Response: `{"status": "ok", "duplicate": false}` |
109 | | -- Handled events: `installation.*`, `pull_request.opened`, `pull_request.synchronize` |
| 92 | +Configure the BYOK Claude credential. Accepts API keys (`sk-ant-api03-...`) and OAuth tokens (`sk-ant-oat...`). OAuth tokens skip server-side validation. |
110 | 93 |
|
111 | | ---- |
| 94 | +- Request: `BYOKConfigureRequest { api_key }`. |
| 95 | +- **200** `BYOKConfigResponse`. |
| 96 | +- **404** if installation unknown. |
| 97 | +- Rate limit: 10/min. |
112 | 98 |
|
113 | | -### Container Sessions -- Coming in Phase 2 |
| 99 | +### `DELETE /installations/{installation_id}/byok` |
114 | 100 |
|
115 | | -Container session endpoints will be added when the container module is implemented. Expected endpoints: |
| 101 | +Remove the BYOK credential. |
116 | 102 |
|
117 | | -| Method | Path | Auth | Description | |
118 | | -|--------|------|------|-------------| |
119 | | -| POST | `/api/v1/sessions/{id}/run` | Bearer JWT | Trigger skill execution in container | |
120 | | -| GET | `/api/v1/sessions/{id}/stream` | Bearer JWT | SSE stream of container output | |
121 | | -| GET | `/api/v1/sessions/{id}` | Bearer JWT | Get session status + results | |
| 103 | +- **204** No Content. |
| 104 | +- **404** if installation unknown. |
| 105 | +- Rate limit: 10/min. |
122 | 106 |
|
123 | | -Exact schemas TBD. |
| 107 | +### `PUT /installations/{installation_id}/suppression-labels` |
124 | 108 |
|
125 | | ---- |
| 109 | +Update the list of PR labels that cause helPRs to skip session creation. |
126 | 110 |
|
127 | | -### Admin -- /admin |
| 111 | +- Request: `SuppressionLabelsRequest { labels: string[] }`. |
| 112 | +- **200** `SuppressionLabelsResponse { labels }`. |
| 113 | +- Rate limit: 10/min. |
128 | 114 |
|
129 | | -- SQLAdmin panel at `/admin` |
130 | | -- Auth: session-based. Development mode accepts any login. Production requires `ADMIN_PASSWORD` env var. |
131 | | -- Full CRUD for: GitHubUser, Installation, BYOKConfig |
132 | | -- Read-only views for: WebhookEvent |
133 | | -- Sensitive fields excluded: `github_access_token_enc`, `encrypted_api_key` |
| 115 | +### `GET /installations/{installation_id}/sessions` |
| 116 | + |
| 117 | +Paginated session history for an installation. |
| 118 | + |
| 119 | +- Query params: `page` (default 1), `per_page` (default 20, max 100), `status` (optional filter). |
| 120 | +- **200** `PaginatedSessionsResponse { items, total, page, per_page, total_pages }`. |
| 121 | +- Rate limit: 30/min. |
| 122 | + |
| 123 | +### `PUT /installations/{installation_id}/post-results` |
| 124 | + |
| 125 | +Enable or disable automatic posting of challenge-me score cards to PRs. |
| 126 | + |
| 127 | +- Request: `PostResultsSettingRequest { post_results_to_pr: boolean }`. |
| 128 | +- **200** `PostResultsSettingResponse { post_results_to_pr }`. |
| 129 | +- Rate limit: 10/min. |
| 130 | + |
| 131 | +## Webhooks — `/api/v1/webhooks/*` |
| 132 | + |
| 133 | +### `POST /webhooks/github` |
| 134 | + |
| 135 | +Unauthenticated but HMAC-verified via the `X-Hub-Signature-256` header (shared secret `GITHUB_WEBHOOK_SECRET`). Required header: `X-GitHub-Delivery`. |
| 136 | + |
| 137 | +Flow: verify HMAC → persist raw event → return 200 → dispatch processing as a background task. Duplicate deliveries (same `X-GitHub-Delivery`) are idempotent. |
| 138 | + |
| 139 | +- **200** `{"status": "ok", "duplicate": false|true}`. |
| 140 | +- **400** on missing `X-GitHub-Delivery`, invalid JSON, or HMAC failure. |
| 141 | +- Handled events: `installation.*`, `pull_request.opened`, `pull_request.synchronize`. Other events are logged and ignored (still stored for audit). |
| 142 | +- Rate limit: 100/min. |
| 143 | + |
| 144 | +## Container sessions — `/api/v1/containers/*` |
| 145 | + |
| 146 | +All endpoints require auth. Installation access is checked at session creation; session-level access (`verify_session_access`) is checked on all other routes. |
| 147 | + |
| 148 | +### `POST /containers/sessions` |
| 149 | + |
| 150 | +Create a session record and start a Claude runner container for a given PR + skill. |
| 151 | + |
| 152 | +- Request: `CreateSessionRequest { installation_id, pr_number, repo_full_name: "owner/repo", skill_name }`. |
| 153 | +- **201** `ContainerSessionResponse`. |
| 154 | +- **404** if the installation is unknown or has no BYOK configured. |
| 155 | +- Rate limit: 10/min. |
| 156 | + |
| 157 | +### `GET /containers/sessions/{session_id}` |
| 158 | + |
| 159 | +Current session status and metadata. |
| 160 | + |
| 161 | +- **200** `ContainerSessionResponse`. |
| 162 | +- Rate limit: 30/min. |
| 163 | + |
| 164 | +### `GET /containers/sessions/{session_id}/stream` |
| 165 | + |
| 166 | +SSE stream of live container output (while `status = RUNNING`). |
| 167 | + |
| 168 | +- Query param: `offset` (default 0) — number of events to skip, for client-side resume. Also read from `Last-Event-ID` header (set automatically by `EventSource` on reconnect). |
| 169 | +- **200** `text/event-stream`. Headers: `Cache-Control: no-cache`, `X-Accel-Buffering: no`. |
| 170 | +- **404** if the container is not running. |
| 171 | +- Each event is a line-delimited SSE message carrying a stream-json object. On stream end, a final `event: done` carries `{"message": "...", "status": "completed" | "failed"}`. |
| 172 | +- Rate limit: 60/min. |
| 173 | + |
| 174 | +### `GET /containers/sessions/{session_id}/events` |
| 175 | + |
| 176 | +Persisted stream-json events (JSONB) for replay without SSE. Suitable for rendering completed sessions. |
| 177 | + |
| 178 | +- **200** `SessionEventsListResponse { session_id, events: SessionEventResponse[], total }`, ordered by `event_id`. |
| 179 | +- Rate limit: 30/min. |
| 180 | + |
| 181 | +### `GET /containers/sessions/{session_id}/scorecard` |
| 182 | + |
| 183 | +Parsed score card for a completed session (currently only emitted by the `challenge-me` skill). |
| 184 | + |
| 185 | +- **200** `ScorecardResponse { session_id, scorecard, xp_earned }`. `scorecard` is `null` if none was extracted. |
| 186 | +- Rate limit: 30/min. |
| 187 | + |
| 188 | +### `POST /containers/sessions/{session_id}/message` |
| 189 | + |
| 190 | +Send a follow-up message to a running container. Delivered to the container's Claude CLI stdin via a FIFO (`docker exec`). |
| 191 | + |
| 192 | +- Request: `SendMessageRequest { content }`. |
| 193 | +- **200** `SendMessageResponse { session_id, status: "sent", message }`. |
| 194 | +- Rate limit: 30/min. |
| 195 | + |
| 196 | +### `POST /containers/sessions/{session_id}/stop` |
| 197 | + |
| 198 | +Stop a running container (graceful SIGTERM then kill). Marks the session as `COMPLETED` or `FAILED`. |
| 199 | + |
| 200 | +- **200** `StopSessionResponse { id, status, message }`. |
| 201 | +- Rate limit: 10/min. |
| 202 | + |
| 203 | +### `DELETE /containers/sessions/{session_id}` |
| 204 | + |
| 205 | +Delete a session and its persisted events. |
| 206 | + |
| 207 | +- **200** `{"status": "deleted", "id": "..."}`. |
| 208 | +- Rate limit: 10/min. |
| 209 | + |
| 210 | +## Admin — `/admin` |
| 211 | + |
| 212 | +SQLAdmin panel mounted at `/admin`. Not part of the REST contract; see `admin/views.py` for the exposed models. Credentials: |
| 213 | + |
| 214 | +- **Development**: any password (auto-auth). |
| 215 | +- **Production**: the `ADMIN_PASSWORD` env var. |
0 commit comments