From d429534b12f34bda21169edc86e28af29854da5c Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 22 Aug 2026 13:16:54 -0400 Subject: [PATCH] docs(api): deleted-annotation history + KOReader deletion propagation New media-items/deleted_annotations.md for the list/restore/purge endpoints; endpoint index updated. The KOReader protocol page documents deleted_highlights/deleted_bookmarks on the progress push and the deletion-propagation contract: keys learned only from server pulls, explicit arrays only (never absence), tombstone convergence via the metadata fetch, no resurrection from stale replays, and the web history as the restore path. --- docs/developer/api/api-reference.md | 4 + .../api/media-items/deleted_annotations.md | 84 +++++++++++++++++++ docs/developer/api/sync/koreader-protocol.md | 28 +++++++ 3 files changed, 116 insertions(+) create mode 100644 docs/developer/api/media-items/deleted_annotations.md diff --git a/docs/developer/api/api-reference.md b/docs/developer/api/api-reference.md index 031f54b..e9ec59e 100644 --- a/docs/developer/api/api-reference.md +++ b/docs/developer/api/api-reference.md @@ -107,6 +107,10 @@ See [Media Item Operations](media-items/) - GET /api/media-items/:id/highlights/:highlightId - Get highlight - PUT /api/media-items/:id/highlights/:highlightId - Update highlight - DELETE /api/media-items/:id/highlights/:highlightId - Delete highlight +- GET /api/media-items/:id/bookmarks - Get bookmarks +- GET /api/media-items/:id/annotations/deleted - List deleted annotations (history) +- POST /api/media-items/:id/annotations/:annotationId/restore - Restore a deleted annotation +- DELETE /api/media-items/:id/annotations/:annotationId?annotation_type=highlight|note|bookmark - Permanently delete a deleted annotation - POST /api/media-items - Create media item (admin) - PUT /api/media-items/:id - Update media item (admin) - DELETE /api/media-items/:id - Delete media item (admin) diff --git a/docs/developer/api/media-items/deleted_annotations.md b/docs/developer/api/media-items/deleted_annotations.md new file mode 100644 index 0000000..3812fec --- /dev/null +++ b/docs/developer/api/media-items/deleted_annotations.md @@ -0,0 +1,84 @@ +# Deleted Annotations History + +List, restore, or permanently delete tombstoned annotations (highlights, +notes, bookmarks) for a book. Deletions — from the web or propagated from a +synced device — are soft-deleted and retained for the sync retention window +(default 30 days), powering the book page's "Recently deleted" list. A +restore returns the row to the active set on every synced device; a purge +removes it immediately and irreversibly. + +All endpoints require user JWT authentication and operate only on the +caller's own annotations. + +## List Deleted Annotations + +**Endpoint**: `GET /api/media-items/:id/annotations/deleted` + +Returns tombstoned annotations for the book, newest deletion first. + +### Response (200 OK) + +```json +{ + "deleted_annotations": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "annotation_type": "highlight", + "display_text": "the chosen text", + "secondary_text": "user note", + "color": "#ffd54f", + "deleted_at": "2026-08-22T15:04:05Z", + "created_at": "2026-08-01T10:00:00Z" + } + ], + "total": 1 +} +``` + +| Field | Description | +| --------------- | ------------------------------------------------------ | +| annotation_type | `highlight`, `note`, or `bookmark` | +| display_text | Highlighted text / note content / bookmark title | +| secondary_text | Note text (highlights) or notes field (bookmarks) | + +## Restore Deleted Annotation + +**Endpoint**: `POST /api/media-items/:id/annotations/:annotationId/restore` + +Body (or query param) `annotation_type` must be `highlight`, `note`, or +`bookmark`. Clears the tombstone; the annotation reappears in the active +set and re-syncs to devices on their next pull. + +```json +{ "annotation_type": "highlight" } +``` + +### Response (200 OK) + +```json +{ "restored": true } +``` + +404 when no matching *deleted* annotation exists for this user and book. + +## Permanently Delete Annotation + +**Endpoint**: `DELETE /api/media-items/:id/annotations/:annotationId?annotation_type=highlight|note|bookmark` + +Removes the tombstoned row from the history immediately. Irreversible — +unlike the tombstone itself, which is restorable until the retention window +lapses and the daily maintenance sweep purges it. + +### Response (200 OK) + +```json +{ "purged": true } +``` + +## Error Responses + +| Code | Description | +| ---- | -------------------------------------------------- | +| 400 | Invalid IDs or missing/unknown `annotation_type` | +| 401 | Not authenticated | +| 404 | No matching deleted annotation | diff --git a/docs/developer/api/sync/koreader-protocol.md b/docs/developer/api/sync/koreader-protocol.md index aad91ab..8a15d77 100644 --- a/docs/developer/api/sync/koreader-protocol.md +++ b/docs/developer/api/sync/koreader-protocol.md @@ -33,6 +33,8 @@ KOReader uses a custom JSON-based sync protocol. | books[].epubcfi | string | No | EPUB CFI location | | books[].character | integer | No | Character offset | | books[].bookmarks | array | No | Array of bookmarks/highlights (shape, color mapping, and echo/dedup rules: see [Sync Bookmarks](../koreader/sync_bookmarks.md)) | +| books[].deleted_highlights | array | No | Highlights deleted on the device: `[{ "dedup_key": "..." }]` — keys previously served to this device (see [Deletion propagation](#deletion-propagation)) | +| books[].deleted_bookmarks | array | No | Bookmarks deleted on the device: `[{ "dedup_key": "..." }]` | \* At least one of `uuid` or `sha256` should be present. The server resolves the book through the shared `BookResolver` with this priority: `uuid` → `sha256` → @@ -170,6 +172,32 @@ returned so clients can cache it regardless of how the book was originally obtained. The library list endpoint (`GET /api/sync/koreader/library`) includes the same `sha256` field on each book. +## Deletion propagation + +The progress push is upsert-only: absence of an annotation from +`highlights`/`notes`/`bookmarks` is **never** interpreted as a delete (a +client with a category disabled must not wipe the server). Deletions are +reported explicitly: + +- Devices remember the `dedup_key` of every annotation the server served + them (persisted locally, e.g. KOReader's sidecar `bookhoard_known_keys`). +- When one of those annotations no longer exists locally, the next push + lists its key in `deleted_highlights` / `deleted_bookmarks`. +- The server tombstones the matching rows (`deleted = TRUE`, kept for the + retention window). Tombstones are served back to *other* devices via the + metadata fetch's `deleted_highlights` / `deleted_bookmarks` arrays so the + deletion converges everywhere. +- A stale replay pushing the annotation's content cannot resurrect the + tombstone: device pushes carry no modification timestamp, so the save is + treated as older than the delete. +- Restoring is possible from the web book page's deleted-annotation + history (`GET /api/media-items/:id/annotations/deleted`, restore/purge + endpoints) until the retention window lapses. + +Because keys are only learned from server pulls, a device-native annotation +deleted locally is simply never pushed again — it can never be mis-flagged +as a server annotation deletion. + ## Book identification Every client/sync interface (KOReader, Kobo, OPDS, the device-link UI, and any