docs/user-guide.md — magic-link sign-in, note basics, wikilink syntax, keyboard shortcuts, offline behaviour, and privacy notes (sessionStorage for tokens, tenant-scoped localStorage). docs/self-hosting.md — system requirements, Docker Compose quick-start, the full LIBRENOTES_* env-var matrix (which are required, which conditional), reverse proxy snippets for Caddy and nginx, volume layout, the in-binary healthcheck, and update/rollback procedure. docs/api.md — every public endpoint: auth (login/verify), notes CRUD, /api/whoami, /healthz. Status codes per endpoint, the optimistic-locking ?base=<unix> contract for PUT/DELETE, note-ID regex, and the rate-limit policy. CONTRIBUTING.md — dev setup (Nix flake .#dev, plain Go, Docker), package layout overview, coding standards (one-way dep flow, tenant FS gateway requirement), branch naming, commit format, and the PR process. Also points security reports at security@librete.ch rather than public issues. Closes #29. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
162 lines
4.1 KiB
Markdown
162 lines
4.1 KiB
Markdown
# API reference
|
|
|
|
This is the public HTTP API of librenotes. All endpoints return
|
|
JSON unless otherwise noted.
|
|
|
|
Base URL: whatever you configured as `LIBRENOTES_BASE_URL` (for
|
|
the cloud: `https://librenot.es`).
|
|
|
|
## Authentication
|
|
|
|
librenotes uses passwordless email magic links plus short-lived
|
|
JWTs. The flow is:
|
|
|
|
1. `POST /auth/login` with an email — server emails a one-time
|
|
token.
|
|
2. The recipient clicks the link, which is `GET /auth/verify?token=...`.
|
|
3. The verify endpoint returns a session JWT (24-hour lifetime).
|
|
4. Subsequent calls send `Authorization: Bearer <jwt>`.
|
|
|
|
### `POST /auth/login`
|
|
|
|
Request:
|
|
|
|
```http
|
|
POST /auth/login
|
|
Content-Type: application/json
|
|
|
|
{ "email": "you@example.com" }
|
|
```
|
|
|
|
Response:
|
|
|
|
| Status | Meaning |
|
|
| ------ | ------- |
|
|
| `202 Accepted` | Email accepted; magic link sent (or rate-limited silently). |
|
|
| `400 Bad Request` | Body is malformed. |
|
|
| `429 Too Many Requests` | Rate limit hit (5 per 15 min per email). |
|
|
|
|
The response body is `{"status":"sent"}` on 202. The endpoint
|
|
deliberately returns 202 even for unknown addresses to avoid
|
|
account enumeration.
|
|
|
|
### `GET /auth/verify?token=<token>` and `POST /auth/verify?token=<token>`
|
|
|
|
Consumes a magic-link token. Single-use. 15-minute expiry.
|
|
|
|
```json
|
|
{
|
|
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
|
|
"user_id": "f7b2e2c3-4a51-4d7c-bdc0-...",
|
|
"email": "you@example.com",
|
|
"expires_at": 1777411035
|
|
}
|
|
```
|
|
|
|
| Status | Meaning |
|
|
| ------ | ------- |
|
|
| `200 OK` | Verified; JWT issued. |
|
|
| `400 Bad Request` | `token` parameter missing. |
|
|
| `401 Unauthorized` | Token unknown, expired, or already used. |
|
|
|
|
## Notes
|
|
|
|
All `/api/*` endpoints require `Authorization: Bearer <jwt>`. A
|
|
401 means the token is missing/invalid; the client should
|
|
redirect to the login page.
|
|
|
|
### `GET /api/whoami`
|
|
|
|
Returns the verified tenant identity.
|
|
|
|
```json
|
|
{ "user_id": "f7b2e2c3-...", "email": "you@example.com" }
|
|
```
|
|
|
|
### `GET /api/notes`
|
|
|
|
List all notes for the current tenant.
|
|
|
|
```json
|
|
[
|
|
{ "id": "ideas", "title": "Ideas", "updated_at": 1777400000 },
|
|
{ "id": "shopping", "title": "Shopping", "updated_at": 1777300000 }
|
|
]
|
|
```
|
|
|
|
### `GET /api/notes/{id}`
|
|
|
|
Read a single note.
|
|
|
|
```json
|
|
{
|
|
"id": "ideas",
|
|
"title": "Ideas",
|
|
"content": "A few directions...\n",
|
|
"updated_at": 1777400000
|
|
}
|
|
```
|
|
|
|
| Status | Meaning |
|
|
| ------ | ------- |
|
|
| `200 OK` | Found. |
|
|
| `400 Bad Request` | Invalid ID (must match `^[a-z0-9][a-z0-9_-]{0,127}$`). |
|
|
| `404 Not Found` | No such note for this tenant. |
|
|
|
|
### `PUT /api/notes/{id}`
|
|
|
|
Create or update a note. Supports optimistic locking via
|
|
`?base=<unix-seconds>`: the server checks the current `updated_at`
|
|
against `base` and returns 409 if the note has been modified
|
|
since the client last saw it.
|
|
|
|
Request:
|
|
|
|
```http
|
|
PUT /api/notes/ideas?base=1777400000
|
|
Content-Type: application/json
|
|
|
|
{ "title": "Ideas", "content": "A few directions...\n" }
|
|
```
|
|
|
|
| Status | Meaning |
|
|
| ------ | ------- |
|
|
| `200 OK` | Saved; body returns the new `updated_at`. |
|
|
| `400 Bad Request` | Bad ID, missing fields, or invalid `base`. |
|
|
| `409 Conflict` | Note has been modified after `base`; body returns the current note so the client can resolve. |
|
|
|
|
### `DELETE /api/notes/{id}`
|
|
|
|
Delete a note. Same `?base=` semantics as PUT.
|
|
|
|
| Status | Meaning |
|
|
| ------ | ------- |
|
|
| `204 No Content` | Deleted (or already gone). |
|
|
| `409 Conflict` | Modified after `base`. |
|
|
|
|
## Health
|
|
|
|
### `GET /healthz`
|
|
|
|
Unauthenticated. Returns `{"status":"ok"}` when the binary is
|
|
serving requests. Used by load balancers and the Docker
|
|
HEALTHCHECK.
|
|
|
|
## Errors
|
|
|
|
Beyond the per-endpoint codes above, all endpoints may return
|
|
`500` for unhandled internal errors. Bodies are plain text
|
|
(`internal server error`), not JSON, to avoid leaking internals.
|
|
|
|
## Rate limits
|
|
|
|
`POST /auth/login` is the only currently rate-limited endpoint:
|
|
5 requests per 15-minute window per email address. The window
|
|
resets on the oldest attempt expiring.
|
|
|
|
## Versioning
|
|
|
|
The current public API surface is unversioned. Breaking changes
|
|
will be signalled via a major version tag in the project as a
|
|
whole, with a deprecation window announced in `CHANGELOG.md`.
|