Skip to content

fix: send relay Follow object as the full Public IRI - #140

Merged
icco merged 1 commit into
mainfrom
fix/relay-follow-public-iri
Aug 17, 2026
Merged

fix: send relay Follow object as the full Public IRI#140
icco merged 1 commit into
mainfrom
fix/relay-follow-public-iri

Conversation

@icco

@icco icco commented Aug 17, 2026

Copy link
Copy Markdown
Owner

relay.toot.io and relay.intahnet.co.uk weren't dead and weren't refusing us on policy — we were sending a Follow they can't parse.

Cause

Both run YUKIMOCHI Activity-Relay, both auto-accept (toot.io lists 1,324 connected instances), and both Rejected within 3.5 minutes of a fresh, correctly-formatted Follow after #139 deployed. Its executeFollowing validates with a literal string comparison:

case contains(activity.Object, "https://www.w3.org/ns/activitystreams#Public"):
    // contains(string) → entry == key
...
default:
    return errors.New("only https://www.w3.org/ns/activitystreams#Public is allowed to follow")

What we put on the wire:

type  : Follow
actor : https://robot.villas/users/nyt_homepage
object: "as:Public"

"as:Public" != "https://www.w3.org/ns/activitystreams#Public" → falls to default → Reject. Semantically we're right — the CURIE expands to that IRI — but the relay string-matches, so we lose.

Fedify hit this class of bug against Lemmy and shipped the same rewrite in 2.2.0 (#710), but only for to/cc/bto/bcc/audience, not object. We're on 2.3.4, so there's nothing to upgrade to.

Fix

RelayFollow overrides toJsonLd to emit the full IRI. Doing it inside toJsonLd rather than post-serialization keeps the Object Integrity Proof over the same bytes that go on the wire — the same ordering constraint #710 calls out.

Applied in both places a relay Follow is produced: the sent activity, and the Follow object dispatcher (the relay re-fetches that URL to verify the subscription, and applies the same check).

Testing

  • Four new cases: full IRI emitted, clone() preserves the subclass (Fedify clones internally), non-Public objects untouched, and one asserting plain Follow still compacts to as:Public so the upstream behaviour this works around is pinned.
  • 174 tests pass against real Postgres.

Real verification is the relays themselves: after deploy, reset the two rows to pending and watch for Accept.

relay.toot.io and relay.intahnet.co.uk both run YUKIMOCHI Activity-Relay,
both auto-accept, and both Rejected us within 3.5 minutes of a fresh Follow.
Its executeFollowing validates with a literal string comparison:

  case contains(activity.Object, "https://www.w3.org/ns/activitystreams#Public")

We serialize PUBLIC_COLLECTION as the compacted CURIE "as:Public", so the
comparison fails and it falls through to "only ...#Public is allowed to
follow" and Rejects. Semantically as:Public is correct — the relay is
string-matching a compacted term — but we're the ones being refused.

Fedify applies this same rewrite to to/cc/bto/bcc/audience as of 2.2.0
(fedify-dev/fedify#710) but not to object. RelayFollow overrides toJsonLd so
the rewrite lands before signing and the signed bytes match the wire bytes.

Used on both the sent activity and the Follow object dispatcher, since the
relay re-fetches the Follow URL to verify it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 17, 2026 02:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes relay subscription failures against YUKIMOCHI Activity-Relay instances by ensuring outgoing Follow activities serialize object as the full ActivityStreams Public IRI (https://www.w3.org/ns/activitystreams#Public) rather than the compacted JSON-LD CURIE (as:Public), which some relays reject via literal string comparison.

Changes:

  • Introduces RelayFollow, a Follow subclass that rewrites object during toJsonLd() serialization to emit the full Public IRI.
  • Switches relay-follow creation paths to use RelayFollow (both the sent activity and the Follow object dispatcher used for relay verification re-fetch).
  • Adds tests covering serialization behavior, clone() preserving the subclass, and non-Public objects remaining untouched.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/lib/subscriptions.ts Adds RelayFollow and the AS_PUBLIC constant to enforce full-IRI serialization for relay follows.
src/lib/federation.ts Uses RelayFollow when generating relay subscription Follow activities and when serving Follow objects for relay verification.
src/lib/tests/subscriptions.test.ts Adds unit tests validating RelayFollow serialization, clone behavior, and pinning upstream Follow compaction behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@icco
icco merged commit 938ce75 into main Aug 17, 2026
8 checks passed
@icco
icco deleted the fix/relay-follow-public-iri branch August 17, 2026 03:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants