style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
+101
-77
@@ -1,4 +1,5 @@
|
||||
<!-- generated-by: gsd-doc-writer -->
|
||||
|
||||
# API Reference
|
||||
|
||||
FamilySync exposes a Hono HTTP API served on port 3000. All `/api/*` routes require an authenticated session (OIDC via Authelia, or `DEV_AUTH_BYPASS=true` for local development). Unauthenticated requests to protected routes receive a `302` redirect to Authelia's authorize endpoint, not a `401`, except where noted.
|
||||
@@ -18,31 +19,31 @@ No API key or `Authorization` header is used. Credentials are never included in
|
||||
|
||||
## Endpoints Overview
|
||||
|
||||
| Method | Path | Auth | Description |
|
||||
|--------|------|------|-------------|
|
||||
| GET | `/health` | None | DB liveness check |
|
||||
| GET | `/callback` | None | OIDC authorization-code exchange |
|
||||
| GET | `/api/login` | OIDC | Login entry point, redirects to `/` |
|
||||
| GET | `/api/me` | OIDC | Current user identity and color |
|
||||
| GET | `/api/events` | OIDC | Windowed calendar occurrences |
|
||||
| POST | `/api/events/create` | OIDC | Enqueue a new event write |
|
||||
| PATCH | `/api/events/:uid/edit` | OIDC | Enqueue an event update |
|
||||
| DELETE | `/api/events/:uid` | OIDC | Enqueue an event delete |
|
||||
| GET | `/api/events/sync-status` | OIDC | Outbox status for a UID |
|
||||
| GET | `/api/events/writable-calendars` | OIDC | Calendars the member can write to |
|
||||
| GET | `/api/lists` | OIDC | All lists accessible to the member |
|
||||
| POST | `/api/lists` | OIDC | Create a list |
|
||||
| PATCH | `/api/lists/:id` | OIDC | Update list name or sharing |
|
||||
| DELETE | `/api/lists/:id` | OIDC | Delete a list (owner only) |
|
||||
| GET | `/api/lists/:id/items` | OIDC | All items in a list |
|
||||
| POST | `/api/lists/:id/items` | OIDC | Add an item to a list |
|
||||
| PATCH | `/api/list-items/:itemId` | OIDC | Update a single list item field |
|
||||
| DELETE | `/api/list-items/:itemId` | OIDC | Delete a list item |
|
||||
| GET | `/api/sse/heartbeat` | OIDC | SSE heartbeat stream |
|
||||
| GET | `/api/sse/lists` | OIDC | Scoped live-list SSE stream |
|
||||
| GET | `/api/push/vapid-public-key` | OIDC | VAPID public key for push subscribe |
|
||||
| POST | `/api/push/subscription` | OIDC | Register a push subscription |
|
||||
| DELETE | `/api/push/subscription` | OIDC | Remove push subscriptions for caller |
|
||||
| Method | Path | Auth | Description |
|
||||
| ------ | -------------------------------- | ---- | ------------------------------------ |
|
||||
| GET | `/health` | None | DB liveness check |
|
||||
| GET | `/callback` | None | OIDC authorization-code exchange |
|
||||
| GET | `/api/login` | OIDC | Login entry point, redirects to `/` |
|
||||
| GET | `/api/me` | OIDC | Current user identity and color |
|
||||
| GET | `/api/events` | OIDC | Windowed calendar occurrences |
|
||||
| POST | `/api/events/create` | OIDC | Enqueue a new event write |
|
||||
| PATCH | `/api/events/:uid/edit` | OIDC | Enqueue an event update |
|
||||
| DELETE | `/api/events/:uid` | OIDC | Enqueue an event delete |
|
||||
| GET | `/api/events/sync-status` | OIDC | Outbox status for a UID |
|
||||
| GET | `/api/events/writable-calendars` | OIDC | Calendars the member can write to |
|
||||
| GET | `/api/lists` | OIDC | All lists accessible to the member |
|
||||
| POST | `/api/lists` | OIDC | Create a list |
|
||||
| PATCH | `/api/lists/:id` | OIDC | Update list name or sharing |
|
||||
| DELETE | `/api/lists/:id` | OIDC | Delete a list (owner only) |
|
||||
| GET | `/api/lists/:id/items` | OIDC | All items in a list |
|
||||
| POST | `/api/lists/:id/items` | OIDC | Add an item to a list |
|
||||
| PATCH | `/api/list-items/:itemId` | OIDC | Update a single list item field |
|
||||
| DELETE | `/api/list-items/:itemId` | OIDC | Delete a list item |
|
||||
| GET | `/api/sse/heartbeat` | OIDC | SSE heartbeat stream |
|
||||
| GET | `/api/sse/lists` | OIDC | Scoped live-list SSE stream |
|
||||
| GET | `/api/push/vapid-public-key` | OIDC | VAPID public key for push subscribe |
|
||||
| POST | `/api/push/subscription` | OIDC | Register a push subscription |
|
||||
| DELETE | `/api/push/subscription` | OIDC | Remove push subscriptions for caller |
|
||||
|
||||
---
|
||||
|
||||
@@ -53,11 +54,13 @@ No API key or `Authorization` header is used. Credentials are never included in
|
||||
Unauthenticated. Performs a `SELECT 1` against MariaDB to prove connectivity.
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{ "ok": true, "db": "up" }
|
||||
```
|
||||
|
||||
**Response 503** (DB unreachable)
|
||||
|
||||
```json
|
||||
{ "ok": false, "db": "down" }
|
||||
```
|
||||
@@ -73,6 +76,7 @@ Returns the authenticated member's identity and their assigned color.
|
||||
Display name is derived from OIDC claims in priority order: `name` → `preferred_username` → `email` → `sub`. The user row is upserted on first visit (keyed on `oidc_iss` + `oidc_sub`).
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{
|
||||
"user": {
|
||||
@@ -97,12 +101,13 @@ Returns a flat array of concrete event occurrences for the given date window. Re
|
||||
|
||||
**Query parameters**
|
||||
|
||||
| Parameter | Required | Format | Notes |
|
||||
|-----------|----------|--------|-------|
|
||||
| `start` | Yes | `YYYY-MM-DD` | Window start (inclusive) |
|
||||
| `end` | Yes | `YYYY-MM-DD` | Window end (exclusive); max 90 days from start |
|
||||
| Parameter | Required | Format | Notes |
|
||||
| --------- | -------- | ------------ | ---------------------------------------------- |
|
||||
| `start` | Yes | `YYYY-MM-DD` | Window start (inclusive) |
|
||||
| `end` | Yes | `YYYY-MM-DD` | Window end (exclusive); max 90 days from start |
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{
|
||||
"occurrences": [
|
||||
@@ -148,22 +153,23 @@ Enqueues a new event for creation on Fastmail CalDAV. Returns `202` immediately
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
|-------|------|----------|-------------|
|
||||
| `title` | string | Yes | 1–255 characters |
|
||||
| `allDay` | boolean | Yes | |
|
||||
| `start` | string | Yes | ISO datetime or `YYYY-MM-DD` for all-day; max 64 chars |
|
||||
| `end` | string | Yes | ISO datetime or `YYYY-MM-DD` for all-day; max 64 chars |
|
||||
| `location` | string | No | Max 2000 characters |
|
||||
| `description` | string | No | Max 2000 characters |
|
||||
| `recurrence` | string | No | `none` \| `daily` \| `weekly` \| `monthly` \| `yearly` |
|
||||
| `recurrenceUntil` | string | No | `YYYY-MM-DD` — maps to RRULE `UNTIL` |
|
||||
| `recurrenceCount` | integer | No | ≥ 1 — maps to RRULE `COUNT` |
|
||||
| `calendarUrl` | string | No | CalDAV collection URL (max 1024); defaults to member's first personal calendar |
|
||||
| Field | Type | Required | Constraints |
|
||||
| ----------------- | ------- | -------- | ------------------------------------------------------------------------------ |
|
||||
| `title` | string | Yes | 1–255 characters |
|
||||
| `allDay` | boolean | Yes | |
|
||||
| `start` | string | Yes | ISO datetime or `YYYY-MM-DD` for all-day; max 64 chars |
|
||||
| `end` | string | Yes | ISO datetime or `YYYY-MM-DD` for all-day; max 64 chars |
|
||||
| `location` | string | No | Max 2000 characters |
|
||||
| `description` | string | No | Max 2000 characters |
|
||||
| `recurrence` | string | No | `none` \| `daily` \| `weekly` \| `monthly` \| `yearly` |
|
||||
| `recurrenceUntil` | string | No | `YYYY-MM-DD` — maps to RRULE `UNTIL` |
|
||||
| `recurrenceCount` | integer | No | ≥ 1 — maps to RRULE `COUNT` |
|
||||
| `calendarUrl` | string | No | CalDAV collection URL (max 1024); defaults to member's first personal calendar |
|
||||
|
||||
`calendarUrl` must belong to the authenticated member or be the shared family calendar. Requests for another member's personal calendar return `403`.
|
||||
|
||||
**Response 202**
|
||||
|
||||
```json
|
||||
{ "uid": "f47ac10b-58cc-4372-a567-0e02b2c3d479@familysync" }
|
||||
```
|
||||
@@ -183,6 +189,7 @@ If `calendarUrl` in the body differs from the event's current calendar, the oper
|
||||
**Request body:** Same schema as `POST /api/events/create`.
|
||||
|
||||
**Response 202**
|
||||
|
||||
```json
|
||||
{ "uid": "f47ac10b-58cc-4372-a567-0e02b2c3d479@familysync" }
|
||||
```
|
||||
@@ -198,6 +205,7 @@ Enqueues a delete for an existing event. Returns `202` immediately.
|
||||
**Path parameter:** `uid` — the event UID.
|
||||
|
||||
**Response 202**
|
||||
|
||||
```json
|
||||
{ "uid": "f47ac10b-58cc-4372-a567-0e02b2c3d479@familysync" }
|
||||
```
|
||||
@@ -214,11 +222,12 @@ If no outbox row exists, the write has settled (or never existed).
|
||||
|
||||
**Query parameters**
|
||||
|
||||
| Parameter | Required | Notes |
|
||||
|-----------|----------|-------|
|
||||
| `uid` | Yes | 1–512 characters |
|
||||
| Parameter | Required | Notes |
|
||||
| --------- | -------- | ---------------- |
|
||||
| `uid` | Yes | 1–512 characters |
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{ "uid": "f47ac10b-...", "status": "done" }
|
||||
```
|
||||
@@ -226,6 +235,7 @@ If no outbox row exists, the write has settled (or never existed).
|
||||
Possible `status` values: `pending` \| `failed` \| `dead` \| `done`.
|
||||
|
||||
When `status` is `failed` or `dead`, an `error` field is included:
|
||||
|
||||
```json
|
||||
{ "uid": "f47ac10b-...", "status": "dead", "error": "CalDAV 409 conflict" }
|
||||
```
|
||||
@@ -239,6 +249,7 @@ When `status` is `failed` or `dead`, an `error` field is included:
|
||||
Returns the authoritative writable calendar set for the authenticated member: their own personal calendar(s) and the shared family calendar. Another member's personal calendar is never included.
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{
|
||||
"calendars": [
|
||||
@@ -271,6 +282,7 @@ Lists are stored in MariaDB. A list can be private (owner-only) or shared (all h
|
||||
Returns all lists the authenticated member owns or has been shared. Includes item counts per list.
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{
|
||||
"lists": [
|
||||
@@ -302,12 +314,13 @@ Creates a new list. If `isShared: true` (the default), share rows are auto-inser
|
||||
{ "name": "Gift Ideas", "isShared": true }
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
|-------|------|----------|-------------|
|
||||
| `name` | string | Yes | 1–255 characters |
|
||||
| `isShared` | boolean | No | Default: `true` |
|
||||
| Field | Type | Required | Constraints |
|
||||
| ---------- | ------- | -------- | ---------------- |
|
||||
| `name` | string | Yes | 1–255 characters |
|
||||
| `isShared` | boolean | No | Default: `true` |
|
||||
|
||||
**Response 201**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2,
|
||||
@@ -330,6 +343,7 @@ Creates a new list. If `isShared: true` (the default), share rows are auto-inser
|
||||
Updates a list's `name` and/or `isShared`. At least one field must be present. Only the list owner can change `isShared`; a sharee may rename.
|
||||
|
||||
Visibility changes reconcile share rows automatically:
|
||||
|
||||
- `false → true`: inserts share rows for all other members.
|
||||
- `true → false`: deletes all non-owner share rows.
|
||||
|
||||
@@ -342,6 +356,7 @@ Visibility changes reconcile share rows automatically:
|
||||
```
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
@@ -364,6 +379,7 @@ Deletes a list. Owner-only. Cascades to all items and share rows.
|
||||
**Path parameter:** `id` — integer list ID.
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{ "id": 1 }
|
||||
```
|
||||
@@ -379,6 +395,7 @@ Returns all items in a list, ordered by fractional rank ascending. Accessible to
|
||||
**Path parameter:** `id` — integer list ID.
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
@@ -411,11 +428,12 @@ Adds an item to a list at the bottom of the active (unchecked) section. Accessib
|
||||
{ "text": "Eggs" }
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
|-------|------|----------|-------------|
|
||||
| `text` | string | Yes | 1–500 characters (plain text only) |
|
||||
| Field | Type | Required | Constraints |
|
||||
| ------ | ------ | -------- | ---------------------------------- |
|
||||
| `text` | string | Yes | 1–500 characters (plain text only) |
|
||||
|
||||
**Response 201**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 11,
|
||||
@@ -458,11 +476,11 @@ or
|
||||
{ "position": "a0V" }
|
||||
```
|
||||
|
||||
| Field | Type | Constraints |
|
||||
|-------|------|-------------|
|
||||
| `checked` | boolean | |
|
||||
| `text` | string | 1–500 characters |
|
||||
| `position` | string | Fractional rank string, 1–255 characters |
|
||||
| Field | Type | Constraints |
|
||||
| ---------- | ------- | ---------------------------------------- |
|
||||
| `checked` | boolean | |
|
||||
| `text` | string | 1–500 characters |
|
||||
| `position` | string | Fractional rank string, 1–255 characters |
|
||||
|
||||
**Response 200** — updated item shape (same as `POST /api/lists/:id/items` response).
|
||||
|
||||
@@ -477,6 +495,7 @@ Deletes a list item permanently. Accessible to list owner and sharees. No soft-d
|
||||
**Path parameter:** `itemId` — integer item ID.
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{ "id": 11 }
|
||||
```
|
||||
@@ -494,6 +513,7 @@ SSE streams use `Content-Type: text/event-stream`. The client should reconnect o
|
||||
Streams `heartbeat` events every 10 seconds indefinitely. Used as a Pangolin tunnel smoke test.
|
||||
|
||||
**Event format**
|
||||
|
||||
```
|
||||
event: heartbeat
|
||||
id: 0
|
||||
@@ -510,16 +530,17 @@ A `heartbeat` event is sent every 30 seconds to keep the Pangolin connection ali
|
||||
|
||||
**Event types**
|
||||
|
||||
| Event type | Payload |
|
||||
|------------|---------|
|
||||
| `list:updated` | `{ type, listId, payload: { id, name } }` |
|
||||
| `list:deleted` | `{ type, listId, payload: { id } }` |
|
||||
| `item:added` | `{ type, listId, payload: { id, listId, text } }` |
|
||||
| `item:updated` | `{ type, listId, payload: { id, listId } }` |
|
||||
| `item:deleted` | `{ type, listId, payload: { id } }` |
|
||||
| `heartbeat` | `{ ts }` |
|
||||
| Event type | Payload |
|
||||
| -------------- | ------------------------------------------------- |
|
||||
| `list:updated` | `{ type, listId, payload: { id, name } }` |
|
||||
| `list:deleted` | `{ type, listId, payload: { id } }` |
|
||||
| `item:added` | `{ type, listId, payload: { id, listId, text } }` |
|
||||
| `item:updated` | `{ type, listId, payload: { id, listId } }` |
|
||||
| `item:deleted` | `{ type, listId, payload: { id } }` |
|
||||
| `heartbeat` | `{ ts }` |
|
||||
|
||||
**Event format example**
|
||||
|
||||
```
|
||||
event: item:added
|
||||
id: 1-1718020800000
|
||||
@@ -541,6 +562,7 @@ Push uses VAPID direct push (no FCM broker). The PWA must be installed to the ho
|
||||
Returns the server's VAPID public key. The public key is non-secret and required by the browser to call `pushManager.subscribe()`.
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{ "publicKey": "BNF8dFt..." }
|
||||
```
|
||||
@@ -563,13 +585,14 @@ Registers a Web Push subscription for the authenticated member. Upserts on `endp
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
|-------|------|----------|-------------|
|
||||
| `endpoint` | string (URL) | Yes | Max 2048 characters |
|
||||
| `keys.p256dh` | string | Yes | 1–512 characters |
|
||||
| `keys.auth` | string | Yes | 1–256 characters |
|
||||
| Field | Type | Required | Constraints |
|
||||
| ------------- | ------------ | -------- | ------------------- |
|
||||
| `endpoint` | string (URL) | Yes | Max 2048 characters |
|
||||
| `keys.p256dh` | string | Yes | 1–512 characters |
|
||||
| `keys.auth` | string | Yes | 1–256 characters |
|
||||
|
||||
**Response 201**
|
||||
|
||||
```json
|
||||
{ "ok": true }
|
||||
```
|
||||
@@ -583,6 +606,7 @@ Registers a Web Push subscription for the authenticated member. Upserts on `endp
|
||||
Removes all push subscription rows for the authenticated member. Cannot affect another member's subscriptions.
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{ "ok": true }
|
||||
```
|
||||
@@ -599,14 +623,14 @@ All error responses use a consistent JSON envelope.
|
||||
{ "error": "Human-readable message" }
|
||||
```
|
||||
|
||||
| HTTP Status | Meaning |
|
||||
|-------------|---------|
|
||||
| `400` | Invalid request parameters (e.g., malformed date window) |
|
||||
| `401` | Session missing or invalid |
|
||||
| `403` | Authenticated but not authorized (wrong owner, sharee attempted owner-only op) |
|
||||
| `404` | Resource not found |
|
||||
| `422` | Valid request but cannot be fulfilled (e.g., user has no calendar configured) |
|
||||
| `503` | DB or downstream service unavailable |
|
||||
| HTTP Status | Meaning |
|
||||
| ----------- | ------------------------------------------------------------------------------ |
|
||||
| `400` | Invalid request parameters (e.g., malformed date window) |
|
||||
| `401` | Session missing or invalid |
|
||||
| `403` | Authenticated but not authorized (wrong owner, sharee attempted owner-only op) |
|
||||
| `404` | Resource not found |
|
||||
| `422` | Valid request but cannot be fulfilled (e.g., user has no calendar configured) |
|
||||
| `503` | DB or downstream service unavailable |
|
||||
|
||||
Zod validation failures return `400` with a structured body from `@hono/zod-validator` rather than the `{ "error": "..." }` envelope.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user