[READ-ONLY] Mirror of https://github.com/olamaelcu/bibliograph. An AppView for interfacing with bibliographic information on ATProto biblio.livtet.olamaelcu.net
atproto atproto-api atproto-browser atprotocol books library library-app library-catalogue library-management

README.md

Bibliograph #

A collection of tools for bibliographic work in one project:

The end goal of this project is to provide the appview that Livtet will leverage to interface with the ATProto ecosystem - it's published in the open to allow for simpler interoperability with other products and projects but designed for the needs of Livtet.

Discovery #

Discovery for things like reviews happen in a few ways:

Network Discovery #

Bibliograph leverages https://constellation.microcosm.blue/ to find book reviews specific to Bibliograph written into the Atmosphere as well as records ingested from TAP of the expected NSID net.olamaelcu.livtet.biblio.bookReview. This allows for (near-)real time analysis of book reviews on protocol from Livtet. For compatibility with other applications like Bookhive, Bibliograph normalizes those reviews into Bibliograph reviews so they can appear to Bibliograph-powered applications.

Material Discovery #

Information is sourced from a few places:

  • Book information is sourced from OpenLibrary (works + editions APIs) with optional Google Books enrichment for descriptions and covers (requires GOOGLE_BOOKS_API_KEY)
  • Author information is gleamed from Wikipedia and OpenLibrary

Catalog curation #

The catalog records (community.lexicon.book.*) are served by Bibliograph, but corrections come from people. Anyone who signs in with an AT Protocol account can vote on which work an edition belongs to.

  • The vote is a record in the voter's own repository: net.olamaelcu.livtet.biblio.editionWorkLink, keyed by the edition's rkey, so an account holds at most one vote per edition and changing your mind updates that record rather than stacking another one.
  • Bibliograph ingests votes through TAP into edition_work_votes and only rewrites editions.work_uri once votes agree: link votes − unlink votes >= EDITION_WORK_CONSENSUS_THRESHOLD (default 2). With one vote per account, a single dissenter cannot overwrite an established link. Set the threshold to 1 to let one account curate a small or private catalog.
  • A decision is stamped editions.work_link_source = 'consensus', and the automatic reconciler (pnpm backfill:works) skips those rows — otherwise a nightly run would silently undo a voted unlink.

OAuth is least-privilege: the client asks only for repo:net.olamaelcu.livtet.biblio.editionWorkLink, never for general repo or profile access.

Variable Dev default Purpose
OAUTH_PUBLIC_ORIGIN http://127.0.0.1:5176 Origin used to build the redirect URI. Loopback redirects must be 127.0.0.1/[::1], not localhost.
OAUTH_CLIENT_ID (unset) Unset → an ATProto loopback client id carrying its metadata inline. Set to an HTTPS URL serving /oauth-client-metadata.json in production.
EDITION_WORK_CONSENSUS_THRESHOLD 2 Net votes required to apply a link or unlink.

Sessions are server-side (oauth_sessions holds DPoP key material; the browser cookie only carries an opaque id), so a tampered cookie cannot claim another account's session.

Librarian gate #

Voting on catalog edits is limited to accounts that hold the librarian label. Signing in is not enough: authentication proves who you are, the label decides whether you may curate.

Bibliograph is itself the labeler. Its labeler identity is the same did:web as the publisher, so /.well-known/did.json advertises both #atproto_label (the label signing key) and #atproto_labeler (the AtprotoLabeler service, an origin with no path).

Endpoint Where
com.atproto.label.queryLabels XRPC query in the SvelteKit router
com.atproto.label.subscribeLabels WebSocket on the Node entrypoint

Labels are signed over the DRISL/dag-CBOR encoding of the schema fields (ver included, sig and $type excluded), SHA-256 hashed, with the raw hash bytes signed by the #atproto_label key. Revocation is a negation label (neg: true), per spec; the gate treats a negated label as absent.

Entrypoint note #

com.atproto.label.subscribeLabels is a WebSocket stream. @sveltejs/adapter-node generates a build/index.js that constructs its own http.Server and never exposes the upgrade event, so a SvelteKit route cannot serve it. Bibliograph therefore runs server.ts, which imports the emitted build/handler.js, hands HTTP to SvelteKit, and keeps upgrades for the label stream. pnpm start, the Procfile, and the Dockerfile ENTRYPOINT all use that entrypoint.

vite dev does not run server.ts, so the stream only exists in a built server: run pnpm build && pnpm start to exercise it locally.

Label vocabulary #

All values live in src/lib/server/labeler/vocabulary.ts, shaped like com.atproto.label.defs#labelValueDefinition so they can be published in a labeler service record without translation. Every write is validated against it: an undeclared value, or a value applied to the wrong subject kind, throws before anything is signed.

Label Subject Behavior Issued by
librarian account none/inform, default ignore admin approval of an application
senior-librarian account none/inform, default ignore pnpm labels:seniority, automatically
administrator account none/inform, default ignore an existing administrator
controversial record (edition/work) none/inform, default warn administrators, from the record page

Credential labels deliberately never blur content: they say nothing about what the subject contains. controversial defaults to warn so the banner is visible without every subscriber configuring it — the point is that a reader sees the context.

senior-librarian requires both tenure and contribution, since applying and then going quiet for a year should not earn it: SENIOR_LIBRARIAN_MIN_DAYS (default 365) since the first librarian label, and SENIOR_LIBRARIAN_MIN_VOTES (default 10) votes cast. Run pnpm --filter bibliograph-service labels:seniority --dry-run to see the evaluation without writing anything; the job is idempotent, so schedule it daily.

Administrators #

The admin view is gated by the administrator label, so anyone holding it — including accounts granted long after deployment — can review the queue without a redeploy. ADMIN_DIDS remains as a bootstrap only: it is the way to grant the first administrator on a fresh deployment, and to recover if a label is ever lost. An account in that list stays an admin even if the label is withdrawn.

Requesting access #

A signed-in account without the label sees a request control on any edition page. The request lands in /admin/applications, readable only by DIDs listed in ADMIN_DIDS (empty = nobody). Approving emits the label; revoking emits a negation and marks the application revoked. The application table records the decision trail, but the label is the authority.

Variable Dev default Purpose
ATP_LABEL_SIGNING_KEY (unset → labeler disabled) K-256 private key (64 hex) used to sign labels. pnpm --filter bibliograph-service label-keygen.
ATP_LABEL_KEY_MULTIBASE (unset → placeholder published) Public half, published as #atproto_label in the DID document. Must match the signing key or every label fails verification.
ADMIN_DIDS (unset → nobody) Comma-separated DIDs allowed to review the queue.

Observability #

Prometheus is available locally at http://localhost:9090 (no auth, dev only).

Scrape targets:

  • bibliograph-service — host.docker.internal:5000/metrics. HTTP request histograms labelled by method, status, and a normalized path (DIDs / opaque IDs collapsed to /{did} and /{id} to bound cardinality; /xrpc/* and /.well-known/* are kept verbatim).
  • tap — tap:2481/metrics. Firehose ingestion, outbox delivery, resync latency emitted by indigo/tap.

Pre-computed recording rules live in prometheus/rules/bibliograph.yml and refresh every 30s:

Rule What it tells you
job:http_requests:rate5m Request rate per route + status class
job:http_latency:p50_5m / :p95_5m / :p99_5m Latency percentiles per route
job:http_errors:ratio5m Share of 5xx responses per route
job:nodejs_eventloop_lag:p95_1m Event-loop lag p95 (Node-side)
job:process_memory_rss:bytes RSS memory in bytes
job:tap_firehose:received_rate1m Events/sec received from the relay
job:tap_firehose:processed_rate1m Events/sec successfully processed
job:tap_outbox:delivered_rate1m Events/sec delivered to consumers
job:tap_resync:p95_5m Resync latency p95
job:tap_event_cache:size Tap in-memory buffer size
job:tap_firehose:last_seq Last cursor seq committed upstream

TSDB lives in the promdata named volume with 7-day retention.

The /metrics endpoint on bibliograph-service is currently unauthenticated — bind Prometheus to localhost before exposing the port.