From 924b66bc64ae96949eeffcd42fb513f3e0d515a9 Mon Sep 17 00:00:00 2001 From: Joakim Date: Sun, 15 Feb 2026 14:58:01 +0100 Subject: [PATCH] docs: add opal-task REST API reference Co-Authored-By: Claude Opus 4.6 --- opal-task/docs/api.md | 921 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 921 insertions(+) create mode 100644 opal-task/docs/api.md diff --git a/opal-task/docs/api.md b/opal-task/docs/api.md new file mode 100644 index 0000000..f9eadf4 --- /dev/null +++ b/opal-task/docs/api.md @@ -0,0 +1,921 @@ +# Opal-Task API Reference + +REST API for the opal task manager. Built with Go and [chi](https://github.com/go-chi/chi) router, backed by SQLite. + +**Base URL:** `http://localhost:8080` (default) or behind a reverse proxy at `/api` + +## Table of Contents + +- [Authentication](#authentication) +- [Response Format](#response-format) +- [Endpoints](#endpoints) + - [Health](#health) + - [OAuth](#oauth) + - [Tasks](#tasks) + - [Tags](#tags) + - [Projects](#projects) + - [Sync](#sync) + - [API Keys](#api-keys) +- [Data Models](#data-models) +- [Reports](#reports) + +--- + +## Authentication + +The API supports two authentication methods: + +### API Key + +Generate a key with the CLI, then pass it as a Bearer token: + +```bash +opal server keygen --name "My Phone" +# Output: oak_aBcDeFgH... (shown once, save it) +``` + +``` +Authorization: Bearer oak_aBcDeFgH... +``` + +Keys are bcrypt-hashed at rest. The `oak_` prefix identifies opal API keys. + +### OAuth / JWT + +When OAuth is enabled, authenticate through the [login flow](#get-authlogin) to receive a JWT: + +``` +Authorization: Bearer eyJhbGciOiJIUzI1NiIs... +``` + +JWTs are HS256-signed, issued by `opal-task`, and expire after 1 hour by default (configurable via `JWT_EXPIRY`). + +### Public Endpoints + +These endpoints require no authentication: + +| Endpoint | Description | +|---|---| +| `GET /health` | Health check | +| `GET /auth/login` | Get OAuth login URL | +| `POST /auth/callback` | OAuth code exchange | +| `POST /auth/refresh` | Refresh access token | +| `POST /auth/logout` | Revoke refresh token | + +All other endpoints require a valid `Authorization: Bearer ` header. + +--- + +## Response Format + +Every response follows this envelope: + +### Success + +```json +{ + "success": true, + "data": { ... } +} +``` + +### Error + +```json +{ + "success": false, + "error": "description of what went wrong" +} +``` + +### Conventions + +- **JSON keys** are `snake_case` throughout. +- **Timestamps** are Unix seconds (integers), not ISO 8601 strings. Nullable timestamps are `null`. +- **Durations** (e.g., `recurrence_duration`) are in seconds. A 1-week recurrence is `604800`. +- **Status** is a single-character string: `"P"`, `"C"`, `"D"`, or `"R"`. + +### Status Codes + +| Code | Meaning | +|---|---| +| `200` | Success | +| `201` | Resource created | +| `400` | Invalid input (bad JSON, missing required fields, invalid UUID) | +| `401` | Missing or invalid authentication | +| `404` | Resource not found | +| `500` | Server error | +| `501` | Feature disabled (e.g., OAuth not configured) | + +--- + +## Endpoints + +### Health + +#### `GET /health` + +Returns server status. No authentication required. + +```bash +curl http://localhost:8080/health +``` + +```json +{ + "success": true, + "data": { + "status": "ok" + } +} +``` + +--- + +### OAuth + +#### `GET /auth/login` + +Returns the OAuth authorization URL for redirecting the user to the identity provider. + +```bash +curl http://localhost:8080/auth/login +``` + +```json +{ + "success": true, + "data": { + "url": "https://auth.example.com/application/o/authorize/?client_id=...&state=abc123", + "state": "abc123" + } +} +``` + +Returns `501` if OAuth is not enabled. + +--- + +#### `POST /auth/callback` + +Exchanges an OAuth authorization code for access and refresh tokens. The `code` parameter comes from the OAuth provider's redirect. + +**Query parameters:** + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `code` | string | yes | Authorization code from OAuth provider | + +```bash +curl -X POST "http://localhost:8080/auth/callback?code=AUTH_CODE_HERE" +``` + +```json +{ + "success": true, + "data": { + "access_token": "eyJhbGciOiJIUzI1NiIs...", + "refresh_token": "dGhpcyBpcyBhIHJlZnJl...", + "expires_at": 1739700000, + "token_type": "Bearer", + "user": { + "id": 1, + "username": "alice", + "email": "alice@example.com" + } + } +} +``` + +--- + +#### `POST /auth/refresh` + +Exchanges a valid refresh token for a new access token. + +**Request body:** + +```json +{ + "refresh_token": "dGhpcyBpcyBhIHJlZnJl..." +} +``` + +```json +{ + "success": true, + "data": { + "access_token": "eyJhbGciOiJIUzI1NiIs...", + "expires_at": 1739703600, + "token_type": "Bearer" + } +} +``` + +Returns `401` if the refresh token is invalid or expired. + +--- + +#### `POST /auth/logout` + +Revokes a refresh token, preventing further use. + +**Request body:** + +```json +{ + "refresh_token": "dGhpcyBpcyBhIHJlZnJl..." +} +``` + +```json +{ + "success": true, + "data": { + "message": "logged out" + } +} +``` + +--- + +### Tasks + +#### `GET /tasks` + +Lists tasks, either by named report or by filter parameters. + +**Query parameters:** + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `report` | string | no | Named report (see [Reports](#reports)). Overrides filter params. | +| `status` | string | no | Filter by status: `pending`, `completed`, `deleted`, `recurring` | +| `project` | string | no | Filter by project name | +| `priority` | string | no | Filter by priority: `L`, `D`, `M`, `H` | +| `tag` | string[] | no | Filter by tags (repeat for multiple: `?tag=home&tag=urgent`) | + +**With report:** + +```bash +curl -H "Authorization: Bearer $TOKEN" \ + "http://localhost:8080/tasks?report=overdue" +``` + +```json +{ + "success": true, + "data": { + "report": "overdue", + "tasks": [ ... ], + "count": 3 + } +} +``` + +**With filters:** + +```bash +curl -H "Authorization: Bearer $TOKEN" \ + "http://localhost:8080/tasks?status=pending&tag=home&priority=H" +``` + +```json +{ + "success": true, + "data": [ + { + "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "id": 42, + "status": "P", + "description": "Fix leaking faucet", + "project": "house", + "priority": 3, + "created": 1739174400, + "modified": 1739545800, + "start": null, + "end": null, + "due": 1740009600, + "scheduled": null, + "wait": null, + "until": null, + "recurrence_duration": null, + "parent_uuid": null, + "tags": ["home", "urgent"], + "urgency": 12.4 + } + ] +} +``` + +--- + +#### `POST /tasks` + +Creates a new task using structured JSON fields. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `description` | string | yes | Task description | +| `tags` | string[] | no | Tags to attach | +| `project` | string | no | Project name | +| `priority` | string | no | `L` (low), `D` (default), `M` (medium), `H` (high) | +| `due` | int64 | no | Due date as Unix timestamp (seconds) | +| `scheduled` | int64 | no | Scheduled date as Unix timestamp | +| `wait` | int64 | no | Wait-until date as Unix timestamp | +| `until` | int64 | no | Expiration date as Unix timestamp | +| `recurrence` | string | no | Recurrence interval (e.g., `1d`, `2w`, `3m`, `1y`) | + +```bash +curl -X POST http://localhost:8080/tasks \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "description": "Buy groceries", + "tags": ["personal", "errands"], + "project": "household", + "priority": "M", + "due": 1739836800 + }' +``` + +**Response** (`201 Created`): + +```json +{ + "success": true, + "data": { + "uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "id": 43, + "status": "P", + "description": "Buy groceries", + "project": "household", + "priority": 2, + "created": 1739750400, + "modified": 1739750400, + "due": 1739836800, + "tags": ["personal", "errands"], + "urgency": 6.1 + } +} +``` + +--- + +#### `POST /tasks/parse` + +Creates a task from a CLI-style input string. Supports the same syntax as the `opal add` command: words without special prefixes become the description, `+tag` adds tags, `-tag` removes tags, and `key:value` pairs set attributes. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `input` | string | yes | CLI-style task input | + +**Modifier syntax:** + +| Syntax | Meaning | Example | +|---|---|---| +| `+tag` | Add tag | `+home` | +| `-tag` | Remove tag | `-garden` | +| `project:name` | Set project | `project:household` | +| `priority:X` | Set priority | `priority:H` | +| `due:value` | Set due date | `due:tomorrow`, `due:monday`, `due:2026-03-01` | +| `scheduled:value` | Set scheduled date | `scheduled:nextweek` | +| `wait:value` | Set wait date | `wait:friday` | +| `until:value` | Set expiration | `until:eom` | +| `recur:interval` | Set recurrence | `recur:1w`, `recur:2d` | + +```bash +curl -X POST http://localhost:8080/tasks/parse \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"input": "Change bed sheets +home project:household due:sunday recur:1w"}' +``` + +**Response** (`201 Created`): + +```json +{ + "success": true, + "data": { + "task": { + "uuid": "c9bf9e57-1685-4c89-bafb-ff5af830be8a", + "id": 44, + "status": "P", + "description": "Change bed sheets", + "project": "household", + "priority": 1, + "created": 1739750400, + "modified": 1739750400, + "due": 1739836800, + "recurrence_duration": 604800, + "parent_uuid": "d4e5f6a7-b8c9-0123-4567-890abcdef012", + "tags": ["home"], + "urgency": 5.3 + } + } +} +``` + +For recurring tasks (those with `recur:`), the API creates a template task (status `"R"`) and returns the first instance. + +--- + +#### `GET /tasks/{uuid}` + +Returns a single task by its UUID. + +```bash +curl -H "Authorization: Bearer $TOKEN" \ + http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890 +``` + +```json +{ + "success": true, + "data": { + "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "id": 42, + "status": "P", + "description": "Fix leaking faucet", + "project": "house", + "priority": 3, + "created": 1739174400, + "modified": 1739545800, + "due": 1740009600, + "tags": ["home", "urgent"], + "urgency": 12.4 + } +} +``` + +Returns `404` if the UUID does not match any task. + +--- + +#### `PUT /tasks/{uuid}` + +Updates one or more fields on an existing task. Only include the fields you want to change. + +**Request body:** + +| Field | Type | Description | +|---|---|---| +| `description` | string | New description | +| `status` | string | New status: `pending`, `completed`, `deleted` | +| `priority` | string | `L`, `D`, `M`, `H` | +| `project` | string | Project name | +| `due` | int64 | Unix timestamp (seconds) | +| `scheduled` | int64 | Unix timestamp | +| `wait` | int64 | Unix timestamp | +| `until` | int64 | Unix timestamp | +| `start` | int64 | Unix timestamp | +| `recurrence` | string | Recurrence interval | + +All fields are optional. + +```bash +curl -X PUT http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"priority": "H", "due": 1739923200}' +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "id": 42, + "status": "P", + "description": "Fix leaking faucet", + "project": "house", + "priority": 3, + "created": 1739174400, + "modified": 1739750400, + "due": 1739923200, + "tags": ["home", "urgent"], + "urgency": 14.7 + } +} +``` + +--- + +#### `DELETE /tasks/{uuid}` + +Deletes a task. By default, sets the task status to `"D"` (soft delete). Pass `permanent=true` to remove it from the database entirely. + +**Query parameters:** + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `permanent` | string | `false` | Set to `true` for permanent deletion | + +```bash +# Soft delete +curl -X DELETE -H "Authorization: Bearer $TOKEN" \ + http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890 + +# Permanent delete +curl -X DELETE -H "Authorization: Bearer $TOKEN" \ + "http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890?permanent=true" +``` + +```json +{ + "success": true, + "data": { + "message": "task deleted" + } +} +``` + +--- + +#### `POST /tasks/{uuid}/complete` + +Marks a task as completed. Sets the status to `"C"` and records the completion time. For recurring task instances, this may trigger creation of the next instance. + +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" \ + http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/complete +``` + +**Response:** The updated task object with `"status": "C"`. + +--- + +#### `POST /tasks/{uuid}/start` + +Marks a task as actively being worked on by setting its `start` timestamp to now. + +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" \ + http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/start +``` + +**Response:** The updated task object with `start` set to the current unix timestamp. + +--- + +#### `POST /tasks/{uuid}/stop` + +Clears the `start` timestamp, marking the task as no longer actively being worked on. + +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" \ + http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/stop +``` + +**Response:** The updated task object with `start` cleared to `null`. + +--- + +### Task Tags + +#### `GET /tasks/{uuid}/tags` + +Returns the tag list for a specific task. + +```bash +curl -H "Authorization: Bearer $TOKEN" \ + http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/tags +``` + +```json +{ + "success": true, + "data": ["home", "urgent"] +} +``` + +--- + +#### `POST /tasks/{uuid}/tags` + +Adds a tag to a task. + +**Request body:** + +```json +{ + "tag": "important" +} +``` + +**Response:** The updated task object. + +--- + +#### `DELETE /tasks/{uuid}/tags/{tag}` + +Removes a tag from a task. + +```bash +curl -X DELETE -H "Authorization: Bearer $TOKEN" \ + http://localhost:8080/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/tags/urgent +``` + +**Response:** The updated task object. + +--- + +### Tags + +#### `GET /tags` + +Returns all tags used across all tasks. + +```bash +curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/tags +``` + +```json +{ + "success": true, + "data": ["errands", "home", "important", "personal", "urgent", "work"] +} +``` + +--- + +### Projects + +#### `GET /projects` + +Returns all project names used across all tasks. + +```bash +curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/projects +``` + +```json +{ + "success": true, + "data": ["household", "work", "garden"] +} +``` + +--- + +### Sync + +These endpoints power the multi-device sync protocol. The CLI client uses them via `opal sync` commands. + +#### `POST /sync/changes` + +Returns all changes recorded since a given timestamp. Used by clients to pull updates from the server. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `since` | int64 | yes | Unix timestamp (seconds). Return changes after this time. Use `0` for initial sync. | +| `client_id` | string | yes | Unique identifier for the syncing device | + +```bash +curl -X POST http://localhost:8080/sync/changes \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"since": 1739600000, "client_id": "phone-abc123"}' +``` + +```json +{ + "success": true, + "data": [ + { + "id": 101, + "task_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "change_type": "create", + "changed_at": 1739650000, + "data": "description:Buy groceries\nstatus:80\npriority:2" + }, + { + "id": 102, + "task_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "change_type": "update", + "changed_at": 1739660000, + "data": "status:67" + } + ] +} +``` + +The `change_type` is one of `create`, `update`, or `delete`. The `data` field uses a `key:value` format with newline separators, recorded by database triggers on every task mutation. Note that change log data uses raw database values (integer status codes), not the API's serialized format. + +--- + +#### `POST /sync/push` + +Pushes local task changes to the server. Conflicts are resolved with **last-write-wins** based on the `modified` timestamp. + +**Request body:** + +| Field | Type | Required | Description | +|---|---|---|---| +| `tasks` | Task[] | yes | Array of full task objects to push | +| `client_id` | string | yes | Unique identifier for the syncing device | + +```bash +curl -X POST http://localhost:8080/sync/push \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "client_id": "phone-abc123", + "tasks": [ + { + "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "status": "P", + "description": "Buy groceries", + "priority": 2, + "created": 1739600000, + "modified": 1739650000, + "tags": ["personal"] + } + ] + }' +``` + +```json +{ + "success": true, + "data": { + "processed": 1, + "conflicts": 0 + } +} +``` + +- **processed** — number of tasks successfully applied +- **conflicts** — number of tasks where the server had a newer version (still applied via last-write-wins) + +--- + +### API Keys + +#### `GET /auth/keys` + +Lists all API keys for the current user. The key value itself is not returned (it is only shown once at creation time). + +```bash +curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/auth/keys +``` + +```json +{ + "success": true, + "data": [ + { + "id": 1, + "name": "My Phone", + "user_id": 1, + "created_at": 1736935200, + "last_used": 1739558400, + "revoked": false + } + ] +} +``` + +--- + +#### `DELETE /auth/keys/{id}` + +Revokes an API key. The key becomes immediately unusable. + +**URL parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `id` | int | API key ID | + +```bash +curl -X DELETE -H "Authorization: Bearer $TOKEN" \ + http://localhost:8080/auth/keys/1 +``` + +```json +{ + "success": true, + "data": { + "message": "API key revoked" + } +} +``` + +--- + +## Data Models + +### Task + +| Field | Type | Description | +|---|---|---| +| `uuid` | string | Unique identifier (UUID v4) | +| `id` | int | Auto-increment database ID | +| `status` | string | `"P"` (pending), `"C"` (completed), `"D"` (deleted), `"R"` (recurring) | +| `description` | string | Task description | +| `project` | string \| null | Project name | +| `priority` | int | `0` = Low, `1` = Default, `2` = Medium, `3` = High | +| `created` | int | Unix timestamp (seconds) | +| `modified` | int | Unix timestamp (seconds) | +| `start` | int \| null | When the task was started (actively being worked on) | +| `end` | int \| null | When the task was completed or deleted | +| `due` | int \| null | Deadline | +| `scheduled` | int \| null | Earliest date the task is actionable | +| `wait` | int \| null | Task is hidden until this date | +| `until` | int \| null | Task auto-deletes after this date | +| `recurrence_duration` | int \| null | Recurrence interval in seconds (e.g., `604800` = 1 week) | +| `parent_uuid` | string \| null | UUID of the recurring template task | +| `tags` | string[] | Attached tags | +| `urgency` | float | Computed urgency score (higher = more urgent) | + +#### Status Values + +| Value | Meaning | +|---|---| +| `"P"` | Pending — active, not yet completed | +| `"C"` | Completed | +| `"D"` | Deleted (soft delete) | +| `"R"` | Recurring template | + +#### Priority Values + +| API Input | Numeric Value | Meaning | +|---|---|---| +| `L` | `0` | Low | +| `D` | `1` | Default | +| `M` | `2` | Medium | +| `H` | `3` | High | + +Use the letter codes (`L`, `D`, `M`, `H`) when creating or updating tasks. Responses return the numeric value. + +--- + +## Reports + +Named reports return pre-filtered, pre-sorted task lists. Pass the report name as `?report=` on `GET /tasks`. + +| Report | Description | +|---|---| +| `active` | Tasks that have been started (pending with a `start` time) | +| `all` | All tasks including recurring templates | +| `completed` | Completed tasks | +| `list` | Pending tasks (default view) | +| `minimal` | Minimal output view | +| `newest` | Pending tasks sorted newest first | +| `next` | Next task due | +| `oldest` | Pending tasks sorted oldest first | +| `overdue` | Tasks past their due date | +| `ready` | Tasks ready to work on (past scheduled date, not waiting) | +| `recurring` | Recurring template tasks | +| `template` | Alias for `recurring` | +| `waiting` | Tasks with a future `wait` date | + +--- + +## CORS + +The API allows cross-origin requests: + +- **Origins:** `*` +- **Methods:** `GET`, `POST`, `PUT`, `DELETE`, `OPTIONS` +- **Headers:** `Content-Type`, `Authorization` + +--- + +## Running the Server + +```bash +# Build +go build -o opal main.go + +# Generate an API key +./opal server keygen --name "My Device" --db /path/to/opal.db + +# Start the server +./opal server start --addr :8080 --db /path/to/opal.db +``` + +### Environment Variables + +| Variable | Default | Description | +|---|---|---| +| `OAUTH_ENABLED` | `false` | Enable OAuth authentication | +| `OAUTH_CLIENT_ID` | — | OAuth client ID | +| `OAUTH_CLIENT_SECRET` | — | OAuth client secret | +| `OAUTH_ISSUER` | — | OAuth issuer URL | +| `OAUTH_REDIRECT_URI` | — | OAuth redirect URI | +| `JWT_SECRET` | — | Secret for signing JWTs | +| `JWT_EXPIRY` | `3600` | JWT lifetime in seconds | +| `REFRESH_TOKEN_EXPIRY` | `604800` | Refresh token lifetime in seconds (default 7 days) | +| `OPAL_DB_PATH` | XDG data dir | Override database file path | +| `OPAL_CONFIG_DIR` | `~/.config/opal` | Config directory | +| `OPAL_DATA_DIR` | `~/.local/share/opal` | Data directory |