The aud of an rpc: scope is a service reference — a DID plus the fragment
naming one entry in its document, as in did:web:api.bsky.app#bsky_appview.
The proxy path checked the grant against ProxyTarget::did instead: the bare
DID, which is the audience of the service-auth JWT it was about to mint. The
two are different values for the same call by design, and the reference
implementation keeps them apart — the scope check sees did#serviceId, the
outbound JWT keeps the DID alone.
Comparing the grant against the wrong half refuses every conformant client. A
Colibri sign-in against a PDS built from this workspace sent
Atproto-Proxy: did:web:api.colibri.social#colibri_appview holding
rpc:social.colibri.actor.listCommunities?aud=did:web:api.colibri.social#colibri_appview,
and got back InsufficientScope: this token does not grant rpc:social.colibri.actor.listCommunities?aud=did:web:api.colibri.social —
the scope it plainly held, with the fragment shaved off. The check passes only
for a grant that names the DID alone, which is what this workspace's own client
and its own tests wrote, so client and server agreed with each other and every
test passed.
ProxyTarget now carries the service_id it resolved by rather than
discarding it, and the assertion is ScopesSet::assert_rpc_service, which asks
about did#service_id and names that reference when it refuses. A grant naming
a bare DID still answers for every service under it: nothing conformant emits
one, but tokens this server issued did, and a DID with no fragment has no other
reading. The service-auth JWT's audience is unchanged.
com.atproto.moderation.createReport is the same kind of proxied call and had
the same defect; it is checked against <report did>#atproto_labeler. The
default AppView pin has no header to read a fragment from, so it assumes
bsky_appview and honours one written on PDS_BSKY_APP_VIEW_DID when the
operator's AppView names its service differently.