Summary
#710 rewrites the public audience to the full https://www.w3.org/ns/activitystreams#Public IRI in to, cc, bto, bcc, and audience, because consumers that compare literally reject the compacted as:Public CURIE. The same hazard applies to object on a Follow, which is how Mastodon-style relay subscription is expressed — but that field isn't covered, so subscribing to a relay from Fedify fails.
Environment
- Fedify version: 2.3.4 (
@fedify/fedify and @fedify/vocab)
- Runtime: Node.js v26.7.0
- OS: Debian GNU/Linux 13 (trixie), linux/amd64
Steps to reproduce
import { Follow, PUBLIC_COLLECTION } from "@fedify/vocab";
const follow = new Follow({
id: new URL("https://example.com/users/alice/follows/1"),
actor: new URL("https://example.com/users/alice"),
object: PUBLIC_COLLECTION,
});
console.log((await follow.toJsonLd()).object);
Expected behavior
https://www.w3.org/ns/activitystreams#Public, matching what #710 already does for the addressing fields.
Actual behavior
Sending that Follow to a relay running YUKIMOCHI Activity-Relay gets a Reject. Its executeFollowing in api/resolver.go validates with exact string equality:
case contains(activity.Object, "https://www.w3.org/ns/activitystreams#Public"):
// contains(string) → entry == key
...
default:
err := errors.New("only https://www.w3.org/ns/activitystreams#Public is allowed to follow")
return err
"as:Public" doesn't match, so it falls through to default and Rejects.
I hit this against relay.toot.io and relay.intahnet.co.uk. Both run that software, both advertise open registration and auto-accept (toot.io lists ~1,300 connected instances), and both Rejected within about 3.5 minutes of receiving the Follow. Re-fetching the Follow object by URL returns the same as:Public, so a relay that dereferences to verify hits it too.
Why this belongs in Fedify
It's the same interop problem #710 already solved, one field short of the relay case, and every app that wants to subscribe to a relay has to reimplement the workaround. The workaround is also easy to get subtly wrong: it has to happen inside toJsonLd rather than after serialization, or the Object Integrity Proof no longer covers the bytes on the wire — the same ordering constraint #710 mentions.
For reference, what I'm using locally:
class RelayFollow extends Follow {
override async toJsonLd(options?: Parameters<Follow["toJsonLd"]>[0]): Promise<unknown> {
const json = await super.toJsonLd(options);
if (json && typeof json === "object") {
const doc = json as Record<string, unknown>;
if (doc.object === "as:Public" || doc.object === "Public") {
doc.object = "https://www.w3.org/ns/activitystreams#Public";
}
}
return json;
}
}
I appreciate that as:Public is correct JSON-LD and the relay is at fault for not expanding before comparing — but given #710 already accepted that tradeoff for Lemmy, extending it to object seems consistent.
Summary
#710 rewrites the public audience to the full
https://www.w3.org/ns/activitystreams#PublicIRI into,cc,bto,bcc, andaudience, because consumers that compare literally reject the compactedas:PublicCURIE. The same hazard applies toobjecton aFollow, which is how Mastodon-style relay subscription is expressed — but that field isn't covered, so subscribing to a relay from Fedify fails.Environment
@fedify/fedifyand@fedify/vocab)Steps to reproduce
Expected behavior
https://www.w3.org/ns/activitystreams#Public, matching what #710 already does for the addressing fields.Actual behavior
Sending that Follow to a relay running YUKIMOCHI Activity-Relay gets a
Reject. ItsexecuteFollowinginapi/resolver.govalidates with exact string equality:"as:Public"doesn't match, so it falls through todefaultand Rejects.I hit this against relay.toot.io and relay.intahnet.co.uk. Both run that software, both advertise open registration and auto-accept (toot.io lists ~1,300 connected instances), and both Rejected within about 3.5 minutes of receiving the Follow. Re-fetching the
Followobject by URL returns the sameas:Public, so a relay that dereferences to verify hits it too.Why this belongs in Fedify
It's the same interop problem #710 already solved, one field short of the relay case, and every app that wants to subscribe to a relay has to reimplement the workaround. The workaround is also easy to get subtly wrong: it has to happen inside
toJsonLdrather than after serialization, or the Object Integrity Proof no longer covers the bytes on the wire — the same ordering constraint #710 mentions.For reference, what I'm using locally:
I appreciate that
as:Publicis correct JSON-LD and the relay is at fault for not expanding before comparing — but given #710 already accepted that tradeoff for Lemmy, extending it toobjectseems consistent.