Skip to content

Latest commit

 

History

History
320 lines (248 loc) · 25 KB

api.md

File metadata and controls

320 lines (248 loc) · 25 KB

HTTP API

JSON in, JSON out. The app reaches these routes through the api Tauri command (share_api for /api/share/*); the headless bin serves them over HTTP, and the dev shim serves all but /api/share/*. Errors carry {status, detail} over IPC and {detail} with the HTTP status over HTTP.

Body and Response name the wire struct (see src/types/generated.ts) when the handler names one directly; blank means not statically named. Notes is the handler's doc comment.

annotations

Source: route/annotations.rs

Method Path Body Response Notes
GET /api/annotations?source_id=&project_id=&all_projects= Unknown paper → {"annotations": []} (not 404).
POST /api/annotations AnnotationCreateBody 404 if the paper is not in the library.
PATCH /api/annotations/{id} AnnotationUpdateBody OkReceipt edit the comment and optionally the anchor. 404 if no row matched.
DELETE /api/annotations/{id} OkReceipt 404 if no row matched.

authors

Source: route/authors.rs

Method Path Body Response Notes
GET /api/authors?exclude_single= AuthorsResponse
GET /api/authors/{id}
GET /api/authors/{id}/merge-candidates MergeCandidates likely-duplicate suggestions for the merge UI, split by evidence strength: candidates shares this author's ORCID (near-certain duplicate), name_candidates only its exact full name (NOCASE — weak evidence). An author matching both surfaces once, as ORCID.
PATCH /api/authors/{id} AuthorUpdateBody forwards the (all-optional) fields to update_fields, then returns the same detail shape as GET.
POST /api/authors/{id}/merge AuthorMergeBody AuthorMergeResponse fold duplicate_ids into author {id}, re-pointing their papers, then returns the canonical author's detail shape.
POST /api/authors/{id}/papers/{pid} OkReceipt attach one paper to an author without merging whole author records. Idempotent: relinking a pair is a no-op under the unique link index. 404 if either side is absent — the FK constraint would otherwise surface as a 500.
DELETE /api/authors/{id}/papers/{pid} OkReceipt detach one paper from an author. Unlinks every stored version of the paper's root: link rows are per-version and the author detail page shows latest-version ids, so an author linked only to v1 must still detach when addressed by v2's id. Idempotent on an absent link; unlinking a paper's last author is allowed (author-less papers are a legal state — delete requires it).
DELETE /api/authors/{id} OkReceipt 404 if absent, 409 if still linked to papers; both guards live in svc_author::delete.

editor

Source: route/editor.rs

Method Path Body Response Notes
GET /api/editor/projects?project_id= EditorProjectsResponse
POST /api/editor/projects CreateEditorProjectBody BadRequest → 400; returns core's created payload.
GET /api/editor/projects/{id}/doc
POST /api/editor/vault/{id}/fs FsOp parse the FsOp (422), 404 if the note is not an editor project, then run it. NotFound → 404, else 400.

feed

Source: route/feed.rs

Method Path Body Response Notes
GET /api/feed
POST /api/feed/dismiss FeedDismissBody OkReceipt hide an entry. permanent: true blocks the whole paper; otherwise dismisses just this version (the default).
GET /api/feed/rules FeedRulesResponse list auto-filter rules.
POST /api/feed/rules FeedRuleCreateBody CreatedFeedRule create an auto-filter rule.
DELETE /api/feed/rules/{id} OkReceipt remove an auto-filter rule. 404 if absent.

graph

Source: route/graph.rs

Method Path Body Response Notes
GET /api/graph?exclude_single_authors= the whole GraphView.

history

Source: route/history.rs

Method Path Body Response Notes
GET /api/history/actor DeviceActor this device's journal actor (None before init).
GET /api/history/library
GET /api/history/library/diff
POST /api/history/library/restore
GET /api/history/project/{fk}
GET /api/history/project/{fk}/diff
POST /api/history/project/{fk}/restore
GET /api/history/share/{id}
GET /api/history/share/{id}/diff

top-level

Source: route/mod.rs

Method Path Body Response Notes
GET /api/stats service::stats owns the envelope; all three surfaces emit it.
GET /api/categories wraps the list in {"categories": …} (the CLI emits a bare array; the envelope divergence is deliberate).

notes

Source: route/notes.rs

Method Path Body Response Notes
GET /api/notes?source_id=&project_id=&all_projects= Unknown paper → {"notes": []} (not 404).
GET /api/notes/{id} single note by NOTE_SK, for the dedicated note page. 404 if no row matched.
POST /api/notes NoteCreateBody 404 if the paper is not in the library.
PATCH /api/notes/{id} NoteUpdateBody 404 if no row matched.
DELETE /api/notes/{id} row + vault tree drop together.

orcid

Source: route/orcid.rs

Method Path Body Response Notes
POST /api/orcid/backfill OrcidBackfillBody OrcidBackfillResponse one pass over limit random ORCID-less, DOI-linked authors, via CrossRef then OpenAlex per DOI (paced, one write).

papers

Source: route/papers.rs

Method Path Body Response Notes
GET /api/papers?limit=&offset=&sort=&dir=&project= sort: published (default) / added / title; dir defaults per metric (newest first, titles A–Z). project narrows in SQL; an unknown id yields [].
GET /api/papers/sfk/{fk}/versions
GET /api/papers/sfk/{fk}/doi-candidates other paper roots sharing this one's DOI, i.e. merge candidates.
GET /api/papers/sfk/{fk}?version= the bare paper object, no envelope.
PUT /api/papers/sfk/{fk} rebuilds metadata from the existing paper's identity (source_id/version/source) + the PUT body.
POST /api/papers/sfk/{fk}/merge PaperMergeBody merge_papers. Merges the duplicate root named in the body INTO this paper (this paper's metadata is canonical; the duplicate's notes, annotations, memberships, tags, missing versions and PDFs move over, then the duplicate root is deleted). 404 on unknown roots, 409 on a self-merge, a trashed root, or a shared-project loser.
DELETE /api/papers/sfk/{fk}/projects remove the paper from every project.
GET /api/papers/search?q=&limit=
POST /api/papers/saved PaperSavedBody which of the given canonical stored ids (arxiv:2204.12985-style, the wire entry_id) the library holds active. Matches are echoed verbatim; trashed and unknown ids are simply absent. The search page's saved-indicator query is the caller.
GET /api/papers/full-text-pending how many fetchable arXiv papers still lack TeX, i.e. full_text_worker's remaining backlog.
POST /api/papers/{id}/full-text?force= download the paper's arXiv TeX tarball, extract, and store the text so papers_fts has something to match. Not automatic on save (arXiv pacing + tarball size); the opt-in background path is full_text_worker, which chews through the same backlog.
GET /api/papers/{id}/full-text-diff?from=&to= FullTextDiffResponse the splice hunks between two stored versions' TeX bodies, computed per request (no storage). 404 when either version has no stored text; 422 on bad/equal versions.
GET /api/papers/{id} the bare paper object, no envelope.
DELETE /api/papers/{id} soft-delete into the trash.

pdfs

Source: route/pdfs.rs

Method Path Body Response Notes
GET /api/papers/{id}/pdf-path?version=
GET /api/pdfs latest-version papers whose PDF is on disk, with file sizes, largest first, capped at 200.
DELETE /api/pdfs/{id} drops every version's local PDF (409 if a file is outside the managed dir), keeping the paper record.

projects

Source: route/projects.rs

Method Path Body Response Notes
GET /api/projects?status= ProjectsResponse default "active"; "all" => no filter.
POST /api/projects ProjectCreateBody CreatedProject
GET /api/projects/{id} not-found wording comes from CoreError::ProjectNotFound (the shared contract), mapped to 404 here.
PATCH /api/projects/{id} ProjectUpdateBody OkReceipt partial update; color cleared only when the color_hex key is present in the body.
DELETE /api/projects/{id} OkReceipt 404 if absent, then soft-delete.
POST /api/projects/{id}/papers ProjectAddPaperBody core's shared receipt; ? maps PaperNotFound/ProjectNotFound → 404, ProjectDeleted → 400.
POST /api/projects/{id}/papers/bulk ProjectAddPapersBulkBody BulkAddReceipt partial success.
DELETE /api/projects/{id}/papers/{sid} sid arrives already percent-decoded in ctx.segs.
POST /api/projects/{id}/export ProjectExportBody OkReceipt writes the .lxproj archive to dest_path, returns {ok}. No streaming branch: the Tauri frontend always sends a path.
GET /api/projects/{id}/export/bibtex
GET /api/projects/{id}/export/obsidian
GET /api/projects/{id}/export/zotero

reading-status

Source: route/reading_status.rs

Method Path Body Response Notes
GET /api/reading-status {"statuses": {source_id: "reading"|"read"}}. Sparse: unread papers are absent.
PUT /api/reading-status/{sid} ReadingStatusPutBody set the status in every reading list the paper is on; "unread" clears. applied = lists written (0 is a no-op, not an error).

search

Source: route/search.rs

Method Path Body Response Notes
GET /api/search/history?prefix=&limit= SearchHistoryResponse limit defaults to 10, range 1–50: out-of-range or non-integer is a 422.
GET /api/search/state SearchStateResponse {state: null|obj}.
POST /api/search/state SearchStateBody OkReceipt the history recording and its settings gate live in service::search_state::save.

settings

Source: route/settings.rs

Method Path Body Response Notes
GET /api/settings settings first, then each present env key overlaid; missing env keys are skipped. Secrets become <KEY>_SET booleans.
PATCH /api/settings Body OkReceipt loops set(k, v) over the body.
PATCH /api/env EnvPatchBody OkReceipt allowlist-gated (400 otherwise). Sets the live process env var (source clients + the GET overlay read via std::env::var) and persists to user settings. set_var mutates global process env: a concurrent GET can race it (values are short ASCII — old-or-new, never torn). The persisted copy is NOT reloaded into the env at startup.

share

Source: route/share.rs

Method Path Body Response Notes
POST /api/share/project/{id}/ticket TicketMinted publish the project if needed, then mint a pasteable ticket carrying the sender's address + share id. Access is gated by whether that id is currently published, not a per-recipient secret.
POST /api/share/join JoinedSummary dial the ticket's sender, fetch the CRDT doc, and materialize it as a read-only mirror under the receiver's share dir. Returns the resulting shared-project summary.
POST /api/share/project/{id}/publish PublishedReceipt snapshot a canonical project into the CRDT store (read-only over the canonical connection) and return its share_id.
POST /api/share/{id}/unpublish UnpublishedReceipt park the published doc as <id>.automerge.unpublished and delete its settings sidecar. An e2ee doc revokes every active member first, which stops the beelay node serving it.
POST /api/share/received/{id}/import
POST /api/share/received/{id}/leave LeftReceipt delete the mirror + ticket + settings, and drop the beelay registration behind an e2ee mirror so a later rejoin adopts from scratch. forgotten: false = node down, so a rejoin reuses the old doc.
POST /api/share/received/{id}/unlink UnlinkedReceipt detach the linked local project from a received share. Membership, mirror, and the project all stay; the interval sync keeps refreshing the mirror but stops importing (its import legs are gated on find_by_share_id). Re-importing creates a fresh link.
POST /api/share/{id}/sync
GET /api/share/member_code MemberCode this device's pasteable membership code.
POST /api/share/relay/reconnect rebind the p2p node against whatever is currently saved under Settings → Sharing, without an app restart. Save the relay settings first (PATCH /api/settings), then call this.
GET /api/share/received ReceivedListing every mirror's summary; pub for the headless bin.
POST /api/share/project/{id}/publish_secure PublishedReceipt snapshot a canonical project into an e2ee share (doc under share_dir/e2ee, beelay-registered), sharing PDF blobs for papers with a local file, and seed the members sidecar with this device as hoster.
POST /api/share/{id}/invite
GET /api/share/{id}/members MembersListing the roster (synced doc metadata ∪ the local invite sidecar), each entry truth-checked with a live query_role. Co-admin: management ops run on whichever admin-tier device issues them — keyhive delegation and PCS rotation happen locally and propagate through the session preamble — so this and the other member routes accept hosted AND received e2ee shares. A hosted store-only state (no node; tests) degrades to the sidecar-only legacy listing.
GET /api/share/{id}/presence PresenceListing every member's last heartbeat. Open to all members (unlike members, which is admin-tier).
POST /api/share/presence PresenceUpdate
POST /api/share/{id}/member/{mid}/role
POST /api/share/{id}/revoke
POST /api/share/{id}/transfer_admin
POST /api/share/{id}/member/{mid}/remove RemovedReceipt revoke, then drop the sidecar row and roster entry entirely, so a re-invite of the same device starts clean (a revoked row keeps its stale role and dead invite string). Revoking is what withdraws the capability; the rows are bookkeeping. Already-revoked members skip to the row delete. Same admin-tier matrix as revoke_member.
POST /api/share/{id}/rekey RekeyedReceipt re-encrypt a hosted e2ee share's history under the current epoch, then republish it (and re-key its PDF blobs) so every current member can read everything. Invites re-seal on their own; this repairs shares sealed before a member was invited, which leaves them fetching commits they hold no key for (keyhive #136) with no way out but this.
POST /api/share/{id}/pdf
GET /api/share/projects SharedProjectsListing summaries of every published shared project. pub so the headless bin's status aggregate reuses it.
GET /api/share/received/{id} ReceivedDetail the full subgraph of one received mirror (plain, falling back to the e2ee mirror of the same id).
GET /api/share/{id}/settings the per-share sidecar (defaults if unset).

sources

Source: route/sources.rs

Method Path Body Response Notes
POST /api/arxiv/search ArxivSearchBody ArxivSearchResponse 502 on any source error. save bulk-saves every result; saved_source_ids reports library membership either way.
POST /api/arxiv/fetch ArxivFetchBody ArxivFetchResponse 404 not-found / 502 other. The save is idempotent (INSERT OR IGNORE), so re-fetching a stored paper cannot conflict.
POST /api/openalex/search OpenAlexSearchBody OpenAlexSearchResponse 502 on any source error. Never saves; saved_source_ids reports what the library already holds.
POST /api/openalex/save OpenAlexSaveBody OpenAlexSaveResponse 404/400/502 on fetch. The save is idempotent (INSERT OR IGNORE). Returns the stripped stored source_id.
POST /api/crossref/search CrossrefSearchBody CrossrefSearchResponse a results-only envelope. The wire body carries no sort, so this arm pins relevance; a transport failure is a 502 rather than an empty result list.
POST /api/doi/resolve DoiResolveBody DoiResolveResponse a bad DOI is a 400 (CoreError::BadRequest via ?).
POST /api/doi/save DoiSaveBody DoiSaveResponse resolve then save; returns the resolved meta. With pdf_url, then best-effort fetches the PDF (SSRF/quota guarded) onto it.

storage

Source: route/storage.rs

Method Path Body Response Notes
POST /api/storage/backup StorageBackupBody {dest_path} → {path, bytes}. Vacuums to a temp file, then renames over dest. Temp path includes process ID for collision safety.
POST /api/storage/restore StorageRestoreBody OkReceipt {src_path} → {ok}. db_admin::restore_in_place owns the park/swap/reopen sequence (shared with the MCP tool); the state mutex is held throughout. The UI still tells the user to restart: in-flight frontend state is not rewound by the swap.
POST /api/storage/import StorageImportBody {src_path} → ImportReport. Merges a backup into the live DB insert-only; unlike restore, nothing is replaced and no restart is needed.
GET /api/storage/pre-migration-backups → PreMigrationBackup[], the copies init takes before a schema upgrade, newest first.

tags

Source: route/tags.rs

Method Path Body Response Notes
GET /api/tags each tag carries its active-paper count so the index can render a table sortable by name or by count.
POST /api/tags TagCreateBody CreatedTag svc_tag::upsert, same envelope as the CLI tag create. Upsert semantics: an existing label returns its id instead of erroring.
GET /api/tags/{label} canonical label via tag::get.
DELETE /api/tags/{id} DeletedTag svc_tag::delete by numeric id. Core's delete is a silent no-op on an unknown id, so existence is checked first for the 404.
POST /api/papers/{source_id}/tags PaperTags svc_paper::add_paper_tags; returns the paper's full tag list after the union. Unknown paper → PaperNotFound → 404.
DELETE /api/papers/{source_id}/tags PaperTags svc_paper::remove_paper_tags; returns the remaining tags. Unknown paper → PaperNotFound → 404.

trash

Source: route/trash.rs

Method Path Body Response Notes
GET /api/trash the canonical TrashListing envelope, shared with linxiv trash list and MCP list_trash.
POST /api/trash/projects/{id}/restore un-trash a project. 422 on a non-integer id, 404 if absent, 400 if the project is active or archived rather than trashed.
DELETE /api/trash/projects/{id} permanently delete a trashed project. 422 on a non-integer id, 404 if absent, 400 if it is not in the trash. ?force=1 skips the trash guard and is irreversible.
POST /api/trash/{rest..}/restore 404 unless the paper is actually in the trash.
DELETE /api/trash/{rest..} permanent, so it 404s unless the paper is in the trash; use DELETE /api/papers/{id} to trash one. ?force=1 skips that guard (only 404 on a missing paper) and is irreversible.

uploads

Source: route/uploads.rs

Method Path Body Response Notes
PUT /api/papers/{id}/pdf UploadPdfBody store PDF bytes the client already fetched for a saved paper. Check order: 413 size → 404 paper → 400 magic.
POST /api/papers/import/pdf ImportPdfBody resolve metadata (network) OUTSIDE the DB lock, then run the sync DB+FS import under it.
POST /api/papers/import/recognize RecognizeBody pure classify of a pasted string, no network.
POST /api/papers/import/pdf-url ImportPdfUrlBody SSRF/size-guarded fetch of a direct PDF URL, then the same two-phase import as /import/pdf.
POST /api/papers/import/bibtex ImportBibtexBody service::paper_import owns guard order, parse and link; ? maps ProjectNotFound → 404, the other refusals → 400.
POST /api/projects/import/preview ImportPreviewBody spill the decoded bytes to a temp .lxproj (preview takes a path); any error is a 400. Temp removed on every path.
POST /api/projects/import/commit ImportCommitBody on_conflict defaults to "merge"; a ProjectImport is a 422, any other failure a 400.

versions

Source: route/versions.rs

Method Path Body Response Notes
POST /api/versions/check VersionsCheckBody VersionCheckResponse one poll pass over the stalest limit papers. Candidates are read, the single batched arXiv call awaits with no DB lock held, then results are applied. 502 on an arXiv failure (nothing recorded, so the same papers stay at the front of the staleness queue).
GET /api/versions/new NewVersionsResponse papers with an un-acknowledged new version.
POST /api/versions/ack VersionsAckBody clear the flag for one paper. 404 when unset.

headless only

Source: bin/headless.rs

Method Path Body Response Notes
GET /api/admin/relay/members
GET /api/admin/relay/log EntriesResponse newest-last tail of log entries.
GET /api/admin/transfers EntriesResponse newest-last tail of log entries.
GET /api/admin/node-address NodeAddressResponse the locator members dial (endpoint id + relay URL; not a capability). 409 until the node is bound to a configured custom relay — n0's default relay set has no single URL to encode.
GET /api/admin/db/backup the snapshot as an attachment, so the browser saves it instead of rendering it. Failures answer as the usual JSON detail.
POST /api/admin/db/import DbImportBody DbImportResponse spill the upload to a temp dir, validate it, then merge (insert-only) or replace (destructive) per mode. validate_backup_source runs before any with_conn, so a junk upload is a 400 with the live library untouched.
GET /api/admin/actors ActorsList
POST /api/admin/relay/members MemberUpsertBody role/name/actors stay raw JSON so each bespoke 400 fires separately from the endpoint-id check; absent fields preserve the member's existing values.
DELETE /api/admin/relay/members/{id}
GET /admin
GET /api/status StatusResponse one-call health/config aggregate for a headless node.
POST /api/relay-access iroh-relay's access check. text/plain true/false, never the JSON envelope: the relay string-matches the body.