grain-serve serves its DID document, naming its appview service
ClosedThis 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.
What changes
grain-serve serves
/.well-known/did.json. The document is built from thedid:webof its public URL (GRAIN_PUBLIC_URL, the DID service tokens already name inaud), and it names three services at the address the document is read from:#grain_appview, typeGrainAppView: the appview, for a PDS to proxy an app's calls to withatproto-proxy: did:web:grain.social#grain_appview, as#bsky_appview/BskyAppViewis Bluesky's and#knotless_appview/KnotlessAppViewis knotless's.#atproto_labeler, typeAtprotoLabeler: 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, typeMentionSearchService, 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=3600as before. A server with no public URL answers 404. The hardcodedweb/src/routes/.well-known/did.json/+server.tsis removed. The dev proxy (vite.config.ts) andservedByGrainServesend 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.rsdid_document, routed with CORS inlib.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-proxytoday:https://grain.socialdirectly.audis the bare DID (GRAIN_APPVIEW_DID).A PDS proxying to
#grain_appviewsigns its service token with the bare DID asaudtoo: the reference PDS'spipethroughkeeps a bare-DIDaudand usesdid#serviceonly for OAuthrpc:scopes. Soviewer::viewer's check takes proxied calls unchanged. An app that asks for anrpc:<nsid>?aud=did:web:grain.social%23grain_appviewscope names this id.Mirrors knotless's
crates/knotless-serve/src/gateway.rs(did_document) and the/.well-known/did.jsonroute incrates/knotless-serve/src/lib.rs. Kept grain-specific:#mention_search.GRAIN_DID_WEB_SCHEMEnames (https unless set).Apphas no public URL field yet, and adding one would touch everyAppliteral the parallel pulls also edit. When the flags pull lands, the endpoint can come from--public-urlas knotless's does.Access-Control-Allow-Origin: *as a header, since grain-serve has notower-http.How it was checked
tests/health.rschecks three things:did:web:grain.test%3A8443, with each service athttps://grain.test:8443.test/web/grainServe.test.tschecks that the path is grain-serve's. Everything else is left to CI.Shortcomings
parts.page.mention.searchyet, so#mention_searchnames a call it refuses as not implemented. The old server on main still answers it./.well-known/did.jsonto grain-serve, as it sends/xrpcand/oauth, or grain-serve must be in front of the web.#atproto_labeleris new. A client that finds grain's labels through it also needs anapp.bsky.labeler.servicerecord, which adid:webhas no repo for, so Bluesky's app will not list it as a labeler.🤖 Generated with Claude Code