If the object isn’t embedded then it needs to be fetched, yes. But this fetching doesn’t need to happen by the server. It more often happens at the client.
Say you send an activity to an actor whose specified behavior is to Announce anything it receives. The first step here is that the relay actor’s server needs to verify that the original delivery was okay. For example, the relay actor’s server might have an allowlist of which domains are allowed to deliver activities in the first place, enforced by HTTP signatures. Separately, the relay actor might want to verify (at the client/app level) that the sending actor is allowed to relay things via that relay.
So one way that this might look like is if the sending actor sends this:
POST /the-relay-actor-inbox HTTP/1.1
Host: domain.example
Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"
Signature: <a signature from the delivering server/domain>
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://domain.example/an-activity" // protected by access control
}
At this point the relay actor’s server can verify that the HTTP signature came from an allowed entity. It passes the check, so it gets added to the relay actor’s inbox.
The relay actor’s client/app then sees this activity in its inbox:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://domain.example/an-activity" // protected by access control
}
And, according to its specified behavior, the relay actor can:
- Generate an Announce where the activity is embedded as-is
- Generate an Announce where the activity is referenced by id
- Generate an Announce where Create/Announce are unwrapped to find a “post” object, which is referenced by id
Generate an Announce where Create/Announce are unwrapped to find a “post” object, which is embedded (this one is probably a bad idea because it can leak information if you’re not careful)
- Forward the activity as-is
- Forward the activity by reference
- etc
Each of these would be its own “relay protocol”. So the FEP may end up defining multiple types instead of just one, although certain “protocols” may be more recommended than others.
Protocol exploration
Announce with embed
Simple and straightforward. Just take the object as-is and wrap it in an Announce.
Pros:
Cons:
- You can Announce an Announce
- Maximum depth is unbounded
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://domain.example/announce-with-embed",
"actor": "https://domain.example/the-relay-actor",
"type": "Announce",
"object": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://domain.example/an-activity" // protected by access control
}
}
Announce with reference
Also simple and straightforward. Just take the id of the object and refer to it in an Announce.
Pros:
Cons:
- You can Announce an Announce
- Maximum depth is unbounded
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://domain.example/announce-with-reference",
"actor": "https://domain.example/the-relay-actor",
"type": "Announce",
"object": "https://domain.example/an-activity" // protected by access control
}
Announce with reference to “post” object
Less simple. Potentially requires fetching references until a “post” object is found, but comes with the guarantee that whatever is being Announced is a “post”. This depends on having a formal definition of what a “post” is, which would be the subject of a separate FEP, but for the purposes of this example let’s say that a “post” is any object that has content
.
Pros:
- Maximum depth is limited
- The
object
being Announced is guaranteed to be a “post” (meaning that it is immediately useful and can be rendered as-is for the most part)
Cons:
- Less simple
- Conceptual coupling to other protocols, like “what is a post” and “how do you do access control”
The relay actor starts with this activity in its inbox, which currently does not look like a “post”:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://domain.example/an-activity" // protected by access control
}
So the relay actor can follow up by fetching the activity to get more information about it (subject to access control):
GET /an-activity HTTP/1.1
Host: domain.example
Authorization: ... # whatever is supported
HTTP/1.1 200 OK
Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://domain.example/an-activity",
"type": "Announce", // probably want to limit your unwrapping to Create and Announce?
"object": {
"id": "https://domain.example/thanks-for-sharing",
"type": "Like",
"object": {
"id": "https://domain.example/the-weather-is-bad",
"type": "Announce",
"object": {
"id": "https://domain.example/miami-weather-today",
"type": "Page",
"name": "Weather in Miami for 2024-10-09",
"url": "https://weather.example/us/fl/mia/2024/10/09"
},
"content": "Hey, watch out for the bad weather..."
},
"content": "thanks for sharing!"
}
}
This outermost activity can be unwrapped if it is a Create
or Announce
without content
. The next level down is a Like
with content
, which in our example “post protocol” is considered to be a “post” because it has content
. So our relay actor refers to this Like in an Announce:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://domain.example/announce-post-with-reference",
"actor": "https://domain.example/the-relay-actor",
"type": "Announce",
"object": "https://domain.example/thanks-for-sharing" // maybe protected by access control
}
Announce with embedded “post” object
Exactly the same as the above section, but embedding the Like “post” instead:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://domain.example/announce-post-with-embed",
"actor": "https://domain.example/the-relay-actor",
"type": "Announce",
"object": {
"id": "https://domain.example/thanks-for-sharing", // if this is subject to access control, then we just leaked some info (subject to verification of authenticity)
"type": "Like",
"object": {
"id": "https://domain.example/the-weather-is-bad",
"type": "Announce",
"object": {
"id": "https://domain.example/miami-weather-today",
"type": "Page",
"name": "Weather in Miami for 2024-10-09",
"url": "https://weather.example/us/fl/mia/2024/10/09"
},
"content": "Hey, watch out for the bad weather..."
},
"content": "thanks for sharing!"
}
}
If the inner “post” is if subject to access control, then we just leaked some info (subject to verification of authenticity).
Forward the activity as-is
Technically any actor can forward from inbox; for a relay actor to do “inbox forwarding”, the recipients need to be defined. This can be defined in the “relay protocol” as “this actor will POST the same payload to its followers, regardless of addressees”, or it can be defined as “the payload MUST address the relay actor’s followers collection”.
In the latter case where the activity we received does not have addressing information embedded, we can fetch it to verify:
GET /an-activity HTTP/1.1
Host: domain.example
Authorization: ... # whatever is supported
HTTP/1.1 200 OK
Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://domain.example/an-activity",
"to": [
"https://domain.example/the-relay-actor",
"https://domain.example/the-relay-actor-followers"
]
}
Now the relay actor knows that the original sender intended for this activity to be forwarded to the relay actor’s followers. It can forward the activity as-is to all of its followers.
POST /some-followers-inbox HTTP/1.1
Host: follower.example
Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"
Signature: <a signature from the relay actor's server/domain>
Forwarded: <a signature from the delivering server/domain>
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://domain.example/an-activity" // protected by access control
}
Forward the activity by reference
If the original sender sent a “minimal activity” to the relay actor, then this is the same case as forwarding as-is.
This option makes the original HTTP signature not usable for forwarding purposes. You need to rely on the fetch-and-verify behavior of downstream actors for this kind of “relay protocol” to be useful.
Security considerations
If a relay actor has the ability to Follow
and to have followers
, then you may encounter a situation where two relay actors follow each other, which can cause infinite reflection if you’re not careful. Therefore, relay actors MUST keep track of activities that they have already relayed, and MUST NOT relay an activity more than once. (Failed deliveries may be retried.)
Other considerations
Note that it’s possible that a “minimal activity” (technically a form of “thin ping”) can lead to the entire activity being discarded by servers that don’t support fetch-to-verify. But that’s mostly what you’d expect in a situation where you care about access control. In general, if you care about access control, you don’t want much more than the id to be passed around.
Variables to consider for defining a “protocol”
Certainly a lot of possibilities for combinatorial explosion here…
- Wrap in Announce, or forward as-is?
- Embed as-is, or use a reference?
- Should the relay actor unwrap Create/Announce to find a “post” object?
- Should it be required to address the relay’s followers, or is addressing the relay enough?
- Can you expect the relay actor to fetch-and-verify?
- If so, then the relay actor should be on the access control list
- Can you expect the downstream followers to fetch-and-verify?
- If so, then the relay actor possibly doesn’t need to be on the access control list
My thoughts
I think the relay actor should generally require less information to operate rather than more information, but having the relay actor have access to certain information can make it more useful.
In general:
- Using
Announce
allows for potentially tracking which relays shared your activity in the shares
collection, if you or your server are one of the recipients of the relayed Announce activity.
- For embedding vs referencing, I think the relay actor should generally defer to whatever the sender intended, so I weakly favor embedding over referencing.
- I think some effort to unwrap Create/Announce (especially Announce) should be considered? You just need a “stop condition”.
- The relay actor ideally doesn’t need to fetch-and-verify anything. This ties back into the embedding vs referencing thing. I think it’s better to punt that responsibility to the downstream followers of the relay if possible.
I also think that it could be very helpful to think about this in terms of what any actor needs to do in order to get their activity or object relayed. So for example, what do you need to do in your client to make sure that you get the desired behavior? How do you formulate the activity? Who do you need to address? The thing about addressing the relay actor’s followers, and whether that should be necessary or not, is basically the kind of thing I’m trying to get at here. If you want to only send to the actor, then you may or may not be implicitly agreeing to certain behavior that you might not be aware of.