~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2

grain-serve serves its DID document, naming its appview service

Closed
Closed chadtmiller.com asked to merge into feature/rust from grain.social/grain:infra/did-json

What changes

grain-serve serves /.well-known/did.json. The document is built from the did:web of its public URL (GRAIN_PUBLIC_URL, the DID service tokens already name in aud), and it names three services at the address the document is read from:

  • #grain_appview, type GrainAppView: the appview, for a PDS to proxy an app's calls to with atproto-proxy: did:web:grain.social#grain_appview, as #bsky_appview / BskyAppView is Bluesky's and #knotless_appview / KnotlessAppView is knotless's.
  • #atproto_labeler, type AtprotoLabeler: grain-serve gives out labels as its DID (com.atproto.label.queryLabels), and this is how atproto finds a labeler's endpoint, as knotless's document names it.
  • #mention_search, type MentionSearchService, unchanged from the web's document: Leaflet's mention search, parts.page.mention.search.

Any origin may read the document, with Cache-Control: public, max-age=3600 as before. A server with no public URL answers 404. The hardcoded web/src/routes/.well-known/did.json/+server.ts is removed. The dev proxy (vite.config.ts) and servedByGrainServe send the path to grain-serve.

Why

The rewrite copies knotless where knotless already does the thing. knotless-serve serves its own document from --public-url, naming its appview service (crates/knotless-serve/src/gateway.rs did_document, routed with CORS in lib.rs; docs/reference/api.md "Who asks"). grain's was a constant in the web, which named no appview at all, so no PDS could proxy to grain-serve.

Which id and type are used, and by whom. Nothing in grain sends atproto-proxy today:

  • The iOS app (grain.social/grain-ios) calls https://grain.social directly.
  • The CLI and grain-mcp call grain-serve directly, with service tokens whose aud is the bare DID (GRAIN_APPVIEW_DID).
  • The web goes through its own server.

A PDS proxying to #grain_appview signs its service token with the bare DID as aud too: the reference PDS's pipethrough keeps a bare-DID aud and uses did#service only for OAuth rpc: scopes. So viewer::viewer's check takes proxied calls unchanged. An app that asks for an rpc:<nsid>?aud=did:web:grain.social%23grain_appview scope names this id.

Mirrors knotless's crates/knotless-serve/src/gateway.rs (did_document) and the /.well-known/did.json route in crates/knotless-serve/src/lib.rs. Kept grain-specific:

  • #mention_search.
  • The endpoint comes from the DID, with the scheme GRAIN_DID_WEB_SCHEME names (https unless set). App has no public URL field yet, and adding one would touch every App literal the parallel pulls also edit. When the flags pull lands, the endpoint can come from --public-url as knotless's does.
  • Access-Control-Allow-Origin: * as a header, since grain-serve has no tower-http.

How it was checked

tests/health.rs checks three things:

  • The document in full, for did:web:grain.test%3A8443, with each service at https://grain.test:8443.
  • The CORS header.
  • The 404 from a server with no DID.

test/web/grainServe.test.ts checks that the path is grain-serve's. Everything else is left to CI.

Shortcomings

  • grain-serve does not answer parts.page.mention.search yet, so #mention_search names a call it refuses as not implemented. The old server on main still answers it.
  • At cutover, the proxy in front (Caddy in hetzner-infra) must send /.well-known/did.json to grain-serve, as it sends /xrpc and /oauth, or grain-serve must be in front of the web.
  • #atproto_labeler is new. A client that finds grain's labels through it also needs an app.bsky.labeler.service record, which a did:web has no repo for, so Bluesky's app will not list it as a labeler.

🤖 Generated with Claude Code

Approved

This moves /.well-known/did.json from the hardcoded web route into grain-serve. The document is built from the server's did:web. It names #grain_appview, #atproto_labeler and #mention_search at the endpoint the DID resolves to, using GRAIN_DID_WEB_SCHEME. It answers 404 when the server has no public URL. I checked it against the code on the branch. App.did, env.did_web_scheme and the served_app test helper all exist. The router, the dev proxy and servedByGrainServe all send the path to grain-serve. The test covers the full document, the CORS header and the 404 case. I did not read the checks and did not run anything.

The PR body already lists the open items: grain-serve doesn't answer parts.page.mention.search yet, the Caddy route has to change at cutover, and Bluesky's app won't list the labeler. I found nothing that has to change.

Landed on feature/rust at fc5f686 through the batch pull Seven reviewed pulls land on feature/rust together, checked once, checked there as a whole. This pull's head is in feature/rust's history.